第18章 - 实战案例与项目应用

第18章 - 实战案例与项目应用

18.1 综合地图应用开发

18.1.1 项目架构设计

project/
├── src/
│   ├── components/
│   │   ├── MapContainer.js      # 地图容器组件
│   │   ├── LayerPanel.js        # 图层面板
│   │   ├── ToolBar.js           # 工具栏
│   │   └── InfoPanel.js         # 信息面板
│   ├── layers/
│   │   ├── BaseLayer.js         # 底图图层
│   │   ├── BusinessLayer.js     # 业务图层
│   │   └── OverlayLayer.js      # 叠加图层
│   ├── utils/
│   │   ├── mapUtils.js          # 地图工具函数
│   │   ├── styleUtils.js        # 样式工具函数
│   │   └── coordUtils.js        # 坐标工具函数
│   ├── services/
│   │   ├── geoserver.js         # GeoServer服务
│   │   └── dataService.js       # 数据服务
│   └── main.js                  # 入口文件
├── public/
│   └── index.html
└── package.json

18.1.2 完整地图应用示例

// main.js - 综合地图应用
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import XYZ from 'ol/source/XYZ';
import TileWMS from 'ol/source/TileWMS';
import GeoJSON from 'ol/format/GeoJSON';
import { fromLonLat, toLonLat } from 'ol/proj';
import { defaults as defaultControls, ScaleLine, MousePosition, FullScreen } from 'ol/control';
import { defaults as defaultInteractions } from 'ol/interaction';
import { createStringXY } from 'ol/coordinate';
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style';
import Select from 'ol/interaction/Select';
import Draw from 'ol/interaction/Draw';
import Modify from 'ol/interaction/Modify';
import Overlay from 'ol/Overlay';

// ==================== 配置 ====================
const config = {
  center: [116.4074, 39.9042],  // 北京
  zoom: 10,
  minZoom: 3,
  maxZoom: 18,
  geoserverUrl: 'http://localhost:8080/geoserver',
  workspace: 'myworkspace'
};

// ==================== 底图配置 ====================
const baseLayers = {
  osm: new TileLayer({
    title: 'OpenStreetMap',
    type: 'base',
    visible: true,
    source: new XYZ({
      url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      attributions: '© OpenStreetMap contributors'
    })
  }),
  
  tianditu: new TileLayer({
    title: '天地图',
    type: 'base',
    visible: false,
    source: new XYZ({
      url: 'http://t{0-7}.tianditu.gov.cn/vec_w/wmts?' +
        'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&' +
        'STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&' +
        'TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=YOUR_TOKEN'
    })
  }),
  
  gaode: new TileLayer({
    title: '高德地图',
    type: 'base',
    visible: false,
    source: new XYZ({
      url: 'https://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
    })
  })
};

// ==================== 业务图层 ====================
// WMS图层
const wmsLayer = new TileLayer({
  title: 'WMS业务图层',
  visible: true,
  source: new TileWMS({
    url: `${config.geoserverUrl}/wms`,
    params: {
      'LAYERS': `${config.workspace}:layer_name`,
      'TILED': true,
      'FORMAT': 'image/png',
      'TRANSPARENT': true
    },
    serverType: 'geoserver'
  })
});

// 矢量图层
const vectorSource = new VectorSource({
  format: new GeoJSON(),
  url: './data/sample.geojson'
});

const vectorLayer = new VectorLayer({
  title: '矢量数据',
  source: vectorSource,
  style: createFeatureStyle
});

// ==================== 样式函数 ====================
function createFeatureStyle(feature) {
  const geometryType = feature.getGeometry().getType();
  const name = feature.get('name') || '';
  
  const styles = {
    'Point': new Style({
      image: new CircleStyle({
        radius: 8,
        fill: new Fill({ color: 'rgba(255, 0, 0, 0.8)' }),
        stroke: new Stroke({ color: '#fff', width: 2 })
      }),
      text: createTextStyle(name)
    }),
    
    'LineString': new Style({
      stroke: new Stroke({
        color: '#3399CC',
        width: 3
      }),
      text: createTextStyle(name)
    }),
    
    'Polygon': new Style({
      fill: new Fill({ color: 'rgba(51, 153, 204, 0.3)' }),
      stroke: new Stroke({ color: '#3399CC', width: 2 }),
      text: createTextStyle(name)
    })
  };
  
  return styles[geometryType] || styles['Point'];
}

function createTextStyle(text) {
  return new Text({
    text: text,
    font: '14px Microsoft YaHei',
    fill: new Fill({ color: '#333' }),
    stroke: new Stroke({ color: '#fff', width: 3 }),
    offsetY: -15
  });
}

// 高亮样式
const highlightStyle = new Style({
  fill: new Fill({ color: 'rgba(255, 255, 0, 0.4)' }),
  stroke: new Stroke({ color: '#ff0000', width: 3 }),
  image: new CircleStyle({
    radius: 10,
    fill: new Fill({ color: 'rgba(255, 0, 0, 0.8)' }),
    stroke: new Stroke({ color: '#fff', width: 2 })
  })
});

// ==================== 控件配置 ====================
const controls = defaultControls().extend([
  new ScaleLine({
    units: 'metric',
    bar: true,
    steps: 4,
    minWidth: 140
  }),
  new MousePosition({
    coordinateFormat: createStringXY(6),
    projection: 'EPSG:4326',
    className: 'mouse-position',
    undefinedHTML: ' '
  }),
  new FullScreen()
]);

// ==================== 创建地图 ====================
const map = new Map({
  target: 'map',
  layers: [
    baseLayers.osm,
    baseLayers.tianditu,
    baseLayers.gaode,
    wmsLayer,
    vectorLayer
  ],
  view: new View({
    center: fromLonLat(config.center),
    zoom: config.zoom,
    minZoom: config.minZoom,
    maxZoom: config.maxZoom
  }),
  controls: controls,
  interactions: defaultInteractions()
});

// ==================== 弹窗 ====================
const popupContainer = document.getElementById('popup');
const popupContent = document.getElementById('popup-content');
const popupCloser = document.getElementById('popup-closer');

const popup = new Overlay({
  element: popupContainer,
  autoPan: true,
  autoPanAnimation: { duration: 250 }
});
map.addOverlay(popup);

popupCloser.onclick = function() {
  popup.setPosition(undefined);
  popupCloser.blur();
  return false;
};

// ==================== 交互功能 ====================
// 选择交互
const selectInteraction = new Select({
  style: highlightStyle,
  layers: [vectorLayer]
});
map.addInteraction(selectInteraction);

selectInteraction.on('select', function(e) {
  if (e.selected.length > 0) {
    const feature = e.selected[0];
    const coords = e.mapBrowserEvent.coordinate;
    showFeatureInfo(feature, coords);
  }
});

// 绘制交互
let drawInteraction = null;
const drawSource = new VectorSource();
const drawLayer = new VectorLayer({
  source: drawSource,
  style: createFeatureStyle
});
map.addLayer(drawLayer);

function startDraw(type) {
  stopDraw();
  drawInteraction = new Draw({
    source: drawSource,
    type: type
  });
  map.addInteraction(drawInteraction);
  
  drawInteraction.on('drawend', function(e) {
    console.log('绘制完成:', e.feature.getGeometry().getCoordinates());
  });
}

function stopDraw() {
  if (drawInteraction) {
    map.removeInteraction(drawInteraction);
    drawInteraction = null;
  }
}

// 修改交互
const modifyInteraction = new Modify({
  source: drawSource
});
map.addInteraction(modifyInteraction);

// ==================== 工具函数 ====================
function showFeatureInfo(feature, coords) {
  const properties = feature.getProperties();
  let html = '<table class="feature-info">';
  
  for (const key in properties) {
    if (key !== 'geometry') {
      html += `<tr><td><strong>${key}</strong></td><td>${properties[key]}</td></tr>`;
    }
  }
  html += '</table>';
  
  popupContent.innerHTML = html;
  popup.setPosition(coords);
}

function switchBaseLayer(layerName) {
  for (const key in baseLayers) {
    baseLayers[key].setVisible(key === layerName);
  }
}

function zoomToExtent(extent) {
  map.getView().fit(extent, {
    padding: [50, 50, 50, 50],
    duration: 1000
  });
}

function locateFeature(featureId) {
  const feature = vectorSource.getFeatureById(featureId);
  if (feature) {
    const extent = feature.getGeometry().getExtent();
    zoomToExtent(extent);
    selectInteraction.getFeatures().clear();
    selectInteraction.getFeatures().push(feature);
  }
}

// ==================== 事件监听 ====================
map.on('pointermove', function(e) {
  const hit = map.hasFeatureAtPixel(e.pixel, {
    layerFilter: layer => layer === vectorLayer
  });
  map.getTargetElement().style.cursor = hit ? 'pointer' : '';
});

map.on('click', function(e) {
  const lonLat = toLonLat(e.coordinate);
  console.log('点击坐标:', lonLat);
});

// ==================== 导出API ====================
window.mapApp = {
  map,
  switchBaseLayer,
  startDraw,
  stopDraw,
  zoomToExtent,
  locateFeature,
  clearDrawings: () => drawSource.clear()
};

console.log('地图应用初始化完成');

18.2 数据可视化案例

18.2.1 热力图实现

import Heatmap from 'ol/layer/Heatmap';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import { fromLonLat } from 'ol/proj';

// 热力图数据源
const heatmapSource = new VectorSource();

// 添加热力图数据
const heatmapData = [
  { lon: 116.4074, lat: 39.9042, weight: 0.8 },
  { lon: 116.3912, lat: 39.9066, weight: 0.6 },
  { lon: 116.4233, lat: 39.9089, weight: 0.9 },
  // ... 更多数据点
];

heatmapData.forEach(item => {
  const feature = new Feature({
    geometry: new Point(fromLonLat([item.lon, item.lat])),
    weight: item.weight
  });
  heatmapSource.addFeature(feature);
});

// 创建热力图层
const heatmapLayer = new Heatmap({
  source: heatmapSource,
  blur: 15,
  radius: 10,
  weight: feature => feature.get('weight'),
  gradient: ['#00f', '#0ff', '#0f0', '#ff0', '#f00']
});

map.addLayer(heatmapLayer);

18.2.2 聚合点实现

import Cluster from 'ol/source/Cluster';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style';

// 原始数据源
const pointSource = new VectorSource({
  format: new GeoJSON(),
  url: './data/points.geojson'
});

// 聚合数据源
const clusterSource = new Cluster({
  distance: 50,
  minDistance: 20,
  source: pointSource
});

// 聚合样式函数
function clusterStyle(feature) {
  const size = feature.get('features').length;
  
  if (size === 1) {
    // 单个点
    return new Style({
      image: new CircleStyle({
        radius: 8,
        fill: new Fill({ color: '#3399CC' }),
        stroke: new Stroke({ color: '#fff', width: 2 })
      })
    });
  }
  
  // 聚合点
  let radius = Math.min(size * 2 + 10, 40);
  let color = size > 100 ? '#ff0000' : size > 50 ? '#ff9900' : '#3399CC';
  
  return new Style({
    image: new CircleStyle({
      radius: radius,
      fill: new Fill({ color: color }),
      stroke: new Stroke({ color: '#fff', width: 2 })
    }),
    text: new Text({
      text: size.toString(),
      font: 'bold 14px Arial',
      fill: new Fill({ color: '#fff' })
    })
  });
}

const clusterLayer = new VectorLayer({
  source: clusterSource,
  style: clusterStyle
});

map.addLayer(clusterLayer);

// 点击展开聚合
map.on('click', function(e) {
  clusterLayer.getFeatures(e.pixel).then(features => {
    if (features.length > 0) {
      const clusterFeatures = features[0].get('features');
      if (clusterFeatures.length > 1) {
        const extent = new ol.extent.createEmpty();
        clusterFeatures.forEach(f => {
          ol.extent.extend(extent, f.getGeometry().getExtent());
        });
        map.getView().fit(extent, { padding: [50, 50, 50, 50], duration: 500 });
      }
    }
  });
});

18.2.3 轨迹动画实现

import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Point from 'ol/geom/Point';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Style, Stroke, Icon } from 'ol/style';
import { getVectorContext } from 'ol/render';

// 轨迹数据
const trackCoordinates = [
  [116.4074, 39.9042],
  [116.4174, 39.9142],
  [116.4274, 39.9042],
  [116.4374, 39.9142],
  [116.4474, 39.9042]
].map(coord => fromLonLat(coord));

// 轨迹线
const trackLine = new Feature({
  geometry: new LineString(trackCoordinates)
});

const trackSource = new VectorSource({
  features: [trackLine]
});

const trackLayer = new VectorLayer({
  source: trackSource,
  style: new Style({
    stroke: new Stroke({
      color: '#3399CC',
      width: 3
    })
  })
});

map.addLayer(trackLayer);

// 移动标记
const markerFeature = new Feature({
  geometry: new Point(trackCoordinates[0])
});

const markerSource = new VectorSource({
  features: [markerFeature]
});

const markerLayer = new VectorLayer({
  source: markerSource,
  style: new Style({
    image: new Icon({
      src: './images/car.png',
      scale: 0.5,
      rotation: 0
    })
  })
});

map.addLayer(markerLayer);

// 动画控制
let animationProgress = 0;
const animationSpeed = 0.005;
let isAnimating = false;

function animate() {
  if (!isAnimating) return;
  
  animationProgress += animationSpeed;
  
  if (animationProgress >= 1) {
    animationProgress = 0;
  }
  
  const geometry = trackLine.getGeometry();
  const coordinate = geometry.getCoordinateAt(animationProgress);
  markerFeature.getGeometry().setCoordinates(coordinate);
  
  // 计算旋转角度
  const nextProgress = Math.min(animationProgress + 0.01, 1);
  const nextCoordinate = geometry.getCoordinateAt(nextProgress);
  const rotation = Math.atan2(
    nextCoordinate[1] - coordinate[1],
    nextCoordinate[0] - coordinate[0]
  );
  
  markerLayer.setStyle(new Style({
    image: new Icon({
      src: './images/car.png',
      scale: 0.5,
      rotation: -rotation + Math.PI / 2
    })
  }));
  
  requestAnimationFrame(animate);
}

function startAnimation() {
  isAnimating = true;
  animate();
}

function stopAnimation() {
  isAnimating = false;
}

function resetAnimation() {
  animationProgress = 0;
  markerFeature.getGeometry().setCoordinates(trackCoordinates[0]);
}

18.3 空间分析案例

18.3.1 缓冲区分析

import { buffer } from 'ol/sphere';
import Feature from 'ol/Feature';
import Polygon from 'ol/geom/Polygon';
import { fromLonLat, toLonLat } from 'ol/proj';

// 使用 Turf.js 进行缓冲区分析
import * as turf from '@turf/turf';

function createBuffer(feature, distance) {
  const format = new GeoJSON();
  
  // 转换为 GeoJSON
  const geoJsonFeature = format.writeFeatureObject(feature, {
    dataProjection: 'EPSG:4326',
    featureProjection: 'EPSG:3857'
  });
  
  // 创建缓冲区
  const buffered = turf.buffer(geoJsonFeature, distance, { units: 'kilometers' });
  
  // 转换回 OpenLayers Feature
  const bufferFeature = format.readFeature(buffered, {
    dataProjection: 'EPSG:4326',
    featureProjection: 'EPSG:3857'
  });
  
  return bufferFeature;
}

// 使用示例
const pointFeature = new Feature({
  geometry: new Point(fromLonLat([116.4074, 39.9042]))
});

const bufferFeature = createBuffer(pointFeature, 5); // 5公里缓冲区
bufferSource.addFeature(bufferFeature);

18.3.2 空间查询

import { containsExtent, intersects } from 'ol/extent';
import { fromExtent } from 'ol/geom/Polygon';

// 空间查询函数
function spatialQuery(queryGeometry, targetSource, queryType = 'intersects') {
  const results = [];
  const queryExtent = queryGeometry.getExtent();
  
  targetSource.forEachFeature(feature => {
    const featureGeometry = feature.getGeometry();
    const featureExtent = featureGeometry.getExtent();
    
    let match = false;
    
    switch (queryType) {
      case 'intersects':
        // 快速检查范围相交
        if (intersects(queryExtent, featureExtent)) {
          // 精确几何相交检测
          match = queryGeometry.intersectsExtent(featureExtent);
        }
        break;
        
      case 'contains':
        match = containsExtent(queryExtent, featureExtent);
        break;
        
      case 'within':
        match = containsExtent(featureExtent, queryExtent);
        break;
    }
    
    if (match) {
      results.push(feature);
    }
  });
  
  return results;
}

// 框选查询
import DragBox from 'ol/interaction/DragBox';
import { platformModifierKeyOnly } from 'ol/events/condition';

const dragBox = new DragBox({
  condition: platformModifierKeyOnly
});

map.addInteraction(dragBox);

dragBox.on('boxend', function() {
  const extent = dragBox.getGeometry().getExtent();
  const boxGeometry = fromExtent(extent);
  
  const results = spatialQuery(boxGeometry, vectorSource, 'intersects');
  
  console.log('查询结果:', results.length, '个要素');
  
  // 高亮显示结果
  selectInteraction.getFeatures().clear();
  results.forEach(feature => {
    selectInteraction.getFeatures().push(feature);
  });
});

18.3.3 距离测量

import { getLength, getArea } from 'ol/sphere';
import Draw from 'ol/interaction/Draw';
import Overlay from 'ol/Overlay';

// 创建测量图层
const measureSource = new VectorSource();
const measureLayer = new VectorLayer({
  source: measureSource,
  style: new Style({
    fill: new Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
    stroke: new Stroke({
      color: '#ffcc33',
      width: 2
    }),
    image: new CircleStyle({
      radius: 7,
      fill: new Fill({ color: '#ffcc33' })
    })
  })
});
map.addLayer(measureLayer);

// 测量提示框
const measureTooltip = new Overlay({
  element: document.createElement('div'),
  offset: [0, -15],
  positioning: 'bottom-center'
});
map.addOverlay(measureTooltip);

// 格式化长度
function formatLength(line) {
  const length = getLength(line, { projection: 'EPSG:3857' });
  
  if (length > 1000) {
    return (length / 1000).toFixed(2) + ' km';
  } else {
    return length.toFixed(2) + ' m';
  }
}

// 格式化面积
function formatArea(polygon) {
  const area = getArea(polygon, { projection: 'EPSG:3857' });
  
  if (area > 10000) {
    return (area / 1000000).toFixed(2) + ' km²';
  } else {
    return area.toFixed(2) + ' m²';
  }
}

// 开始测量
let measureDraw = null;

function startMeasure(type) {
  stopMeasure();
  
  const geometryType = type === 'length' ? 'LineString' : 'Polygon';
  
  measureDraw = new Draw({
    source: measureSource,
    type: geometryType
  });
  
  map.addInteraction(measureDraw);
  
  let sketch = null;
  
  measureDraw.on('drawstart', function(e) {
    sketch = e.feature;
    
    sketch.getGeometry().on('change', function(evt) {
      const geom = evt.target;
      let output;
      
      if (geom instanceof LineString) {
        output = formatLength(geom);
      } else if (geom instanceof Polygon) {
        output = formatArea(geom);
      }
      
      measureTooltip.getElement().innerHTML = output;
      measureTooltip.setPosition(geom.getLastCoordinate());
    });
  });
  
  measureDraw.on('drawend', function() {
    measureTooltip.setPosition(undefined);
    sketch = null;
  });
}

function stopMeasure() {
  if (measureDraw) {
    map.removeInteraction(measureDraw);
    measureDraw = null;
  }
}

function clearMeasure() {
  measureSource.clear();
}

18.4 GeoServer 集成案例

18.4.1 WFS-T 事务操作

import WFS from 'ol/format/WFS';
import GML from 'ol/format/GML';

const wfsFormat = new WFS();
const gmlFormat = new GML({
  featureNS: 'http://geoserver/namespace',
  featureType: 'layer_name',
  srsName: 'EPSG:3857'
});

// 插入要素
async function insertFeature(feature) {
  const insertNode = wfsFormat.writeTransaction([feature], null, null, {
    featureNS: 'http://geoserver/namespace',
    featurePrefix: 'workspace',
    featureType: 'layer_name',
    srsName: 'EPSG:3857',
    gmlOptions: { srsName: 'EPSG:3857' }
  });
  
  const serializer = new XMLSerializer();
  const xmlString = serializer.serializeToString(insertNode);
  
  try {
    const response = await fetch(`${config.geoserverUrl}/wfs`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/xml' },
      body: xmlString
    });
    
    const result = await response.text();
    console.log('插入成功:', result);
    return true;
  } catch (error) {
    console.error('插入失败:', error);
    return false;
  }
}

// 更新要素
async function updateFeature(feature) {
  const updateNode = wfsFormat.writeTransaction(null, [feature], null, {
    featureNS: 'http://geoserver/namespace',
    featurePrefix: 'workspace',
    featureType: 'layer_name',
    srsName: 'EPSG:3857',
    gmlOptions: { srsName: 'EPSG:3857' }
  });
  
  const serializer = new XMLSerializer();
  const xmlString = serializer.serializeToString(updateNode);
  
  try {
    const response = await fetch(`${config.geoserverUrl}/wfs`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/xml' },
      body: xmlString
    });
    
    const result = await response.text();
    console.log('更新成功:', result);
    return true;
  } catch (error) {
    console.error('更新失败:', error);
    return false;
  }
}

// 删除要素
async function deleteFeature(feature) {
  const deleteNode = wfsFormat.writeTransaction(null, null, [feature], {
    featureNS: 'http://geoserver/namespace',
    featurePrefix: 'workspace',
    featureType: 'layer_name',
    srsName: 'EPSG:3857'
  });
  
  const serializer = new XMLSerializer();
  const xmlString = serializer.serializeToString(deleteNode);
  
  try {
    const response = await fetch(`${config.geoserverUrl}/wfs`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/xml' },
      body: xmlString
    });
    
    const result = await response.text();
    console.log('删除成功:', result);
    return true;
  } catch (error) {
    console.error('删除失败:', error);
    return false;
  }
}

18.4.2 GetFeatureInfo 查询

// WMS GetFeatureInfo 查询
function getFeatureInfo(coordinate) {
  const viewResolution = map.getView().getResolution();
  const wmsSource = wmsLayer.getSource();
  
  const url = wmsSource.getFeatureInfoUrl(
    coordinate,
    viewResolution,
    'EPSG:3857',
    {
      'INFO_FORMAT': 'application/json',
      'FEATURE_COUNT': 10
    }
  );
  
  if (url) {
    fetch(url)
      .then(response => response.json())
      .then(data => {
        if (data.features && data.features.length > 0) {
          showFeatureInfoPopup(data.features, coordinate);
        }
      })
      .catch(error => console.error('GetFeatureInfo 失败:', error));
  }
}

function showFeatureInfoPopup(features, coordinate) {
  let html = '<div class="feature-info-list">';
  
  features.forEach((feature, index) => {
    html += `<div class="feature-item">`;
    html += `<h4>要素 ${index + 1}</h4>`;
    html += '<table>';
    
    for (const key in feature.properties) {
      html += `<tr><td>${key}</td><td>${feature.properties[key]}</td></tr>`;
    }
    
    html += '</table></div>';
  });
  
  html += '</div>';
  
  popupContent.innerHTML = html;
  popup.setPosition(coordinate);
}

// 绑定地图点击事件
map.on('singleclick', function(e) {
  getFeatureInfo(e.coordinate);
});

18.5 移动端适配案例

18.5.1 触屏交互优化

import { defaults as defaultInteractions, PinchZoom, PinchRotate } from 'ol/interaction';

// 移动端交互配置
const mobileInteractions = defaultInteractions({
  altShiftDragRotate: false,
  pinchRotate: true,
  pinchZoom: true
}).extend([
  new PinchZoom(),
  new PinchRotate()
]);

// 创建移动端地图
const mobileMap = new Map({
  target: 'map',
  layers: layers,
  view: view,
  interactions: mobileInteractions,
  controls: [] // 移动端可以隐藏部分控件
});

// 检测设备类型
function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
    navigator.userAgent
  );
}

// 根据设备类型调整配置
if (isMobile()) {
  // 增大点击判定区域
  map.on('click', function(e) {
    const features = map.getFeaturesAtPixel(e.pixel, {
      hitTolerance: 10 // 增大触摸容差
    });
  });
  
  // 调整缩放控件大小
  document.querySelector('.ol-zoom').style.transform = 'scale(1.5)';
}

18.5.2 离线地图支持

// 使用 IndexedDB 缓存瓦片
class TileCache {
  constructor(dbName = 'tile-cache') {
    this.dbName = dbName;
    this.storeName = 'tiles';
    this.db = null;
  }
  
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (e) => {
        const db = e.target.result;
        db.createObjectStore(this.storeName, { keyPath: 'url' });
      };
    });
  }
  
  async get(url) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.get(url);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result?.data);
    });
  }
  
  async set(url, data) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.put({ url, data, timestamp: Date.now() });
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve();
    });
  }
}

// 自定义瓦片加载器
const tileCache = new TileCache();
await tileCache.init();

const cachedSource = new XYZ({
  url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
  tileLoadFunction: async function(imageTile, src) {
    // 尝试从缓存获取
    let blob = await tileCache.get(src);
    
    if (!blob) {
      // 从网络获取
      try {
        const response = await fetch(src);
        blob = await response.blob();
        await tileCache.set(src, blob);
      } catch (error) {
        console.error('瓦片加载失败:', src);
        return;
      }
    }
    
    imageTile.getImage().src = URL.createObjectURL(blob);
  }
});

18.6 性能优化实践

18.6.1 大数据量渲染优化

import WebGLPointsLayer from 'ol/layer/WebGLPoints';

// WebGL 渲染大量点
const webglLayer = new WebGLPointsLayer({
  source: new VectorSource({
    url: './data/large-points.geojson',
    format: new GeoJSON()
  }),
  style: {
    symbol: {
      symbolType: 'circle',
      size: ['interpolate', ['linear'], ['zoom'], 5, 4, 15, 12],
      color: ['match', ['get', 'type'],
        'A', '#ff0000',
        'B', '#00ff00',
        'C', '#0000ff',
        '#999999'
      ],
      opacity: 0.8
    }
  }
});

map.addLayer(webglLayer);

18.6.2 懒加载与分页

import { bbox as bboxStrategy } from 'ol/loadingstrategy';

// 基于范围的懒加载
const lazySource = new VectorSource({
  format: new GeoJSON(),
  url: function(extent) {
    return `${config.apiUrl}/features?` +
      `bbox=${extent.join(',')}&` +
      `srs=EPSG:3857`;
  },
  strategy: bboxStrategy
});

// 分页加载
class PaginatedSource extends VectorSource {
  constructor(options) {
    super(options);
    this.apiUrl = options.apiUrl;
    this.pageSize = options.pageSize || 1000;
    this.currentPage = 0;
    this.hasMore = true;
  }
  
  async loadMore() {
    if (!this.hasMore) return;
    
    const response = await fetch(
      `${this.apiUrl}?page=${this.currentPage}&size=${this.pageSize}`
    );
    const data = await response.json();
    
    if (data.features.length < this.pageSize) {
      this.hasMore = false;
    }
    
    const format = new GeoJSON();
    const features = format.readFeatures(data, {
      dataProjection: 'EPSG:4326',
      featureProjection: 'EPSG:3857'
    });
    
    this.addFeatures(features);
    this.currentPage++;
    
    return features.length;
  }
}

18.7 本章小结

本章通过实战案例展示了 OpenLayers 在实际项目中的应用:

  1. 综合地图应用:完整的项目架构和代码组织
  2. 数据可视化:热力图、聚合点、轨迹动画
  3. 空间分析:缓冲区、空间查询、距离测量
  4. GeoServer 集成:WFS-T 事务、GetFeatureInfo
  5. 移动端适配:触屏交互、离线缓存
  6. 性能优化:WebGL 渲染、懒加载

最佳实践总结

  • 模块化组织代码,便于维护和扩展
  • 合理使用图层分组和管理
  • 根据数据量选择合适的渲染方式
  • 实现完善的错误处理和用户反馈
  • 考虑移动端兼容性和性能优化

← 上一章:性能优化与最佳实践 | 返回目录

posted @ 2026-01-08 13:40  我才是银古  阅读(2)  评论(0)    收藏  举报