第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 在实际项目中的应用:
- 综合地图应用:完整的项目架构和代码组织
- 数据可视化:热力图、聚合点、轨迹动画
- 空间分析:缓冲区、空间查询、距离测量
- GeoServer 集成:WFS-T 事务、GetFeatureInfo
- 移动端适配:触屏交互、离线缓存
- 性能优化:WebGL 渲染、懒加载
最佳实践总结
- 模块化组织代码,便于维护和扩展
- 合理使用图层分组和管理
- 根据数据量选择合适的渲染方式
- 实现完善的错误处理和用户反馈
- 考虑移动端兼容性和性能优化

浙公网安备 33010602011771号