
4.1 基础标注
📦 基于 OpenLayers 10.5.0+ 最新 API✅ Context7 文档验证通过🔄 包含最新标注特性
🎯 学习目标
本章节将介绍 OpenLayers 基础标注功能,包括文本标注、图标标记和基础样式配置。完成本章学习后,你将掌握:
- 文本标注的创建和样式配置
- 图标标记的使用和自定义
- 基础交互功能的实现
- 标注的动态管理和更新
🌟 基础标注概述
基础标注是地图应用中最常用的功能,包括在地图上添加文本标签、图标标记等。OpenLayers 10.x 提供了强大而灵活的标注系统。
📝 文本标注
创建文本标注
javascript
import { Map, View, Feature } from 'ol';
import { Point } from 'ol/geom';
import { Vector as VectorLayer } from 'ol/layer';
import { Vector as VectorSource } from 'ol/source';
import { Style, Text, Fill, Stroke } from 'ol/style';
import { fromLonLat } from 'ol/proj';
// 创建文本标注要素
const createTextAnnotation = (coordinate, text, options = {}) => {
const feature = new Feature({
geometry: new Point(fromLonLat(coordinate)),
name: text,
type: 'text-annotation'
});
// ✅ 使用现代化的文本样式
const textStyle = new Style({
text: new Text({
text: text,
font: options.font || '14px Arial',
fill: new Fill({
color: options.fillColor || '#000'
}),
stroke: new Stroke({
color: options.strokeColor || '#fff',
width: options.strokeWidth || 2
}),
offsetX: options.offsetX || 0,
offsetY: options.offsetY || 0,
textAlign: options.textAlign || 'center',
textBaseline: options.textBaseline || 'middle'
})
});
feature.setStyle(textStyle);
return feature;
};
// 使用示例
const textAnnotations = [
createTextAnnotation([116.4074, 39.9042], '北京', {
font: '16px bold Arial',
fillColor: '#ff0000',
strokeColor: '#ffffff',
strokeWidth: 3
}),
createTextAnnotation([121.4737, 31.2304], '上海', {
font: '14px Arial',
fillColor: '#0066cc'
})
];
动态文本样式
javascript
// 根据缩放级别调整文本样式
const createDynamicTextStyle = (feature, resolution) => {
const zoom = Math.round(Math.log2(156543.03392 / resolution));
let fontSize = 12;
let strokeWidth = 2;
if (zoom > 10) {
fontSize = 16;
strokeWidth = 3;
} else if (zoom > 8) {
fontSize = 14;
strokeWidth = 2;
}
return new Style({
text: new Text({
text: feature.get('name'),
font: `${fontSize}px Arial`,
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff',
width: strokeWidth
})
})
});
};
// 应用动态样式
const textLayer = new VectorLayer({
source: new VectorSource({
features: textAnnotations
}),
style: createDynamicTextStyle
});
🎯 图标标记
基础图标标记
javascript
import { Icon, Circle, RegularShape } from 'ol/style';
// 创建图标标记
const createIconMarker = (coordinate, iconSrc, options = {}) => {
const feature = new Feature({
geometry: new Point(fromLonLat(coordinate)),
type: 'icon-marker'
});
const iconStyle = new Style({
image: new Icon({
src: iconSrc,
scale: options.scale || 1,
anchor: options.anchor || [0.5, 1],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
rotation: options.rotation || 0,
opacity: options.opacity || 1
})
});
feature.setStyle(iconStyle);
return feature;
};
// 使用示例
const iconMarkers = [
createIconMarker([116.4074, 39.9042], '/icons/marker-red.png', {
scale: 1.2,
anchor: [0.5, 1]
}),
createIconMarker([121.4737, 31.2304], '/icons/marker-blue.png', {
scale: 1.0,
rotation: Math.PI / 4 // 45度旋转
})
];
几何图形标记
javascript
// 创建圆形标记
const createCircleMarker = (coordinate, options = {}) => {
const feature = new Feature({
geometry: new Point(fromLonLat(coordinate)),
type: 'circle-marker'
});
const circleStyle = new Style({
image: new Circle({
radius: options.radius || 8,
fill: new Fill({
color: options.fillColor || '#ff0000'
}),
stroke: new Stroke({
color: options.strokeColor || '#ffffff',
width: options.strokeWidth || 2
})
})
});
feature.setStyle(circleStyle);
return feature;
};
// 创建正多边形标记
const createShapeMarker = (coordinate, options = {}) => {
const feature = new Feature({
geometry: new Point(fromLonLat(coordinate)),
type: 'shape-marker'
});
const shapeStyle = new Style({
image: new RegularShape({
points: options.points || 5, // 五角星
radius: options.radius || 10,
radius2: options.radius2 || 5, // 内半径
angle: options.angle || 0,
fill: new Fill({
color: options.fillColor || '#ffff00'
}),
stroke: new Stroke({
color: options.strokeColor || '#000000',
width: options.strokeWidth || 1
})
})
});
feature.setStyle(shapeStyle);
return feature;
};
🔧 标注管理
标注图层管理器
javascript
class AnnotationManager {
constructor(map) {
this.map = map;
this.layers = new Map();
this.features = new Map();
}
// 创建标注图层
createLayer(layerId, options = {}) {
const source = new VectorSource();
const layer = new VectorLayer({
source: source,
style: options.style,
zIndex: options.zIndex || 100,
...options
});
layer.set('id', layerId);
layer.set('type', 'annotation');
this.layers.set(layerId, layer);
this.map.addLayer(layer);
return layer;
}
// 添加标注
addAnnotation(layerId, annotation) {
const layer = this.layers.get(layerId);
if (layer) {
const feature = this.createFeature(annotation);
layer.getSource().addFeature(feature);
this.features.set(annotation.id, feature);
return feature;
}
return null;
}
// 创建要素
createFeature(annotation) {
const { id, coordinate, type, style, properties } = annotation;
const feature = new Feature({
geometry: new Point(fromLonLat(coordinate)),
id: id,
type: type,
...properties
});
if (style) {
feature.setStyle(this.createStyle(style));
}
return feature;
}
// 创建样式
createStyle(styleConfig) {
const { text, icon, circle, shape } = styleConfig;
const styleOptions = {};
if (text) {
styleOptions.text = new Text({
text: text.content,
font: text.font || '14px Arial',
fill: new Fill({ color: text.fillColor || '#000' }),
stroke: new Stroke({
color: text.strokeColor || '#fff',
width: text.strokeWidth || 2
}),
offsetX: text.offsetX || 0,
offsetY: text.offsetY || 0
});
}
if (icon) {
styleOptions.image = new Icon({
src: icon.src,
scale: icon.scale || 1,
anchor: icon.anchor || [0.5, 1]
});
}
if (circle) {
styleOptions.image = new Circle({
radius: circle.radius || 8,
fill: new Fill({ color: circle.fillColor || '#ff0000' }),
stroke: new Stroke({
color: circle.strokeColor || '#fff',
width: circle.strokeWidth || 2
})
});
}
return new Style(styleOptions);
}
// 移除标注
removeAnnotation(annotationId) {
const feature = this.features.get(annotationId);
if (feature) {
// 从所有图层中查找并移除
this.layers.forEach(layer => {
const source = layer.getSource();
if (source.hasFeature(feature)) {
source.removeFeature(feature);
}
});
this.features.delete(annotationId);
return true;
}
return false;
}
// 更新标注
updateAnnotation(annotationId, updates) {
const feature = this.features.get(annotationId);
if (feature) {
if (updates.coordinate) {
feature.getGeometry().setCoordinates(fromLonLat(updates.coordinate));
}
if (updates.style) {
feature.setStyle(this.createStyle(updates.style));
}
if (updates.properties) {
Object.keys(updates.properties).forEach(key => {
feature.set(key, updates.properties[key]);
});
}
return true;
}
return false;
}
// 清空图层
clearLayer(layerId) {
const layer = this.layers.get(layerId);
if (layer) {
layer.getSource().clear();
return true;
}
return false;
}
// 获取标注
getAnnotation(annotationId) {
return this.features.get(annotationId);
}
// 获取图层中的所有标注
getLayerAnnotations(layerId) {
const layer = this.layers.get(layerId);
if (layer) {
return layer.getSource().getFeatures();
}
return [];
}
}
// 使用示例
const annotationManager = new AnnotationManager(map);
// 创建标注图层
annotationManager.createLayer('markers', {
zIndex: 100
});
annotationManager.createLayer('labels', {
zIndex: 101
});
// 添加标注
annotationManager.addAnnotation('markers', {
id: 'marker-1',
coordinate: [116.4074, 39.9042],
type: 'icon-marker',
style: {
icon: {
src: '/icons/marker-red.png',
scale: 1.2
}
},
properties: {
name: '北京',
description: '中国首都'
}
});
annotationManager.addAnnotation('labels', {
id: 'label-1',
coordinate: [116.4074, 39.9042],
type: 'text-annotation',
style: {
text: {
content: '北京',
font: '16px bold Arial',
fillColor: '#ff0000',
strokeColor: '#ffffff',
offsetY: -30
}
}
});
🖱️ 基础交互
点击事件处理
javascript
// ✅ 使用 Context7 验证的最新事件处理方法
const setupAnnotationInteractions = (map, annotationManager) => {
// 点击事件
map.on('click', (event) => {
// 使用最新的要素检测方法
const features = map.getFeaturesAtPixel(event.pixel, {
hitTolerance: 10, // CSS 像素
layerFilter: (layer) => {
return layer.get('type') === 'annotation';
}
});
if (features.length > 0) {
const feature = features[0];
handleAnnotationClick(feature, event.coordinate);
}
});
// 鼠标悬停效果
map.on('pointermove', (event) => {
const features = map.getFeaturesAtPixel(event.pixel, {
hitTolerance: 5,
layerFilter: (layer) => layer.get('type') === 'annotation'
});
if (features.length > 0) {
map.getTargetElement().style.cursor = 'pointer';
showTooltip(features[0], event.coordinate);
} else {
map.getTargetElement().style.cursor = '';
hideTooltip();
}
});
};
// 处理标注点击
const handleAnnotationClick = (feature, coordinate) => {
const properties = feature.getProperties();
console.log('点击标注:', properties);
// 显示详细信息
showAnnotationDetails(feature, coordinate);
};
// 显示工具提示
let tooltipElement = null;
const showTooltip = (feature, coordinate) => {
if (!tooltipElement) {
tooltipElement = document.createElement('div');
tooltipElement.className = 'annotation-tooltip';
document.body.appendChild(tooltipElement);
}
const name = feature.get('name') || '未命名标注';
tooltipElement.innerHTML = name;
tooltipElement.style.display = 'block';
// 转换坐标到屏幕位置
const pixel = map.getPixelFromCoordinate(coordinate);
tooltipElement.style.left = pixel[0] + 'px';
tooltipElement.style.top = (pixel[1] - 30) + 'px';
};
const hideTooltip = () => {
if (tooltipElement) {
tooltipElement.style.display = 'none';
}
};
📱 响应式设计
移动端优化
javascript
// 检测设备类型
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
};
// 创建响应式标注样式
const createResponsiveStyle = (baseStyle, isMobileDevice) => {
const scale = isMobileDevice ? 1.5 : 1.0;
const fontSize = isMobileDevice ? '16px' : '14px';
return {
...baseStyle,
icon: baseStyle.icon ? {
...baseStyle.icon,
scale: (baseStyle.icon.scale || 1) * scale
} : undefined,
text: baseStyle.text ? {
...baseStyle.text,
font: fontSize + ' Arial'
} : undefined
};
};
// 应用响应式样式
const mobileOptimized = isMobile();
const responsiveAnnotations = annotations.map(annotation => ({
...annotation,
style: createResponsiveStyle(annotation.style, mobileOptimized)
}));
🎨 扁平样式格式 (Flat Style Format)
⚠️ Context7 重要更正:扁平样式格式的正确用法
javascript
// ⚠️ 重要说明:扁平样式格式主要用于 WebGL 图层
// 对于普通的 Vector 图层,仍需要使用传统的 Style 对象
// ✅ WebGL 图层中的扁平样式格式
import { WebGLVectorLayer } from 'ol/layer';
const webglLayer = new WebGLVectorLayer({
source: vectorSource,
style: {
// ✅ 这是正确的扁平样式格式用法
'circle-radius': 8,
'circle-fill-color': '#ff0000',
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2,
'text-value': 'Label',
'text-font': '14px Arial',
'text-fill-color': '#000000',
'text-stroke-color': '#ffffff',
'text-stroke-width': 2
}
});
// ❌ 错误用法:不能直接用于普通 Vector 图层
const vectorLayer = new VectorLayer({
source: vectorSource,
style: flatStyle // ❌ 这样不会工作
});
// ✅ 普通 Vector 图层仍需要传统样式
const vectorLayer = new VectorLayer({
source: vectorSource,
style: new Style({
image: new Circle({
radius: 8,
fill: new Fill({ color: '#ff0000' }),
stroke: new Stroke({ color: '#ffffff', width: 2 })
}),
text: new Text({
text: 'Label',
font: '14px Arial',
fill: new Fill({ color: '#000000' }),
stroke: new Stroke({ color: '#ffffff', width: 2 })
})
})
});
// 扁平样式示例集合
const flatStyleExamples = {
// 点样式
point: {
'circle-radius': 6,
'circle-fill-color': '#0066cc',
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2
},
// 线样式
line: {
'stroke-color': '#ff0000',
'stroke-width': 3,
'stroke-line-dash': [5, 5]
},
// 面样式
polygon: {
'fill-color': 'rgba(0, 255, 0, 0.3)',
'stroke-color': '#00ff00',
'stroke-width': 2
},
// 文本样式
text: {
'text-value': 'Sample Text',
'text-font': 'bold 16px Arial',
'text-fill-color': '#333333',
'text-stroke-color': '#ffffff',
'text-stroke-width': 3,
'text-offset-x': 0,
'text-offset-y': -20
}
};
// 动态扁平样式生成器
class FlatStyleGenerator {
static generatePointStyle(options = {}) {
return {
'circle-radius': options.radius || 8,
'circle-fill-color': options.fillColor || '#0066cc',
'circle-stroke-color': options.strokeColor || '#ffffff',
'circle-stroke-width': options.strokeWidth || 2
};
}
static generateTextStyle(text, options = {}) {
return {
'text-value': text,
'text-font': options.font || '14px Arial',
'text-fill-color': options.fillColor || '#000000',
'text-stroke-color': options.strokeColor || '#ffffff',
'text-stroke-width': options.strokeWidth || 2,
'text-offset-x': options.offsetX || 0,
'text-offset-y': options.offsetY || 0
};
}
static combineStyles(...styles) {
return Object.assign({}, ...styles);
}
}
// 使用示例
const pointWithText = FlatStyleGenerator.combineStyles(
FlatStyleGenerator.generatePointStyle({
radius: 10,
fillColor: '#ff6600'
}),
FlatStyleGenerator.generateTextStyle('重要位置', {
font: 'bold 16px Arial',
offsetY: -25
})
);
// ✅ Context7 修复:正确的扁平样式转换方法
const convertFlatStyleToTraditional = (flatStyleConfig) => {
const styleOptions = {};
// 处理圆形样式
if (flatStyleConfig['circle-radius']) {
styleOptions.image = new Circle({
radius: flatStyleConfig['circle-radius'],
fill: flatStyleConfig['circle-fill-color'] ? new Fill({
color: flatStyleConfig['circle-fill-color']
}) : undefined,
stroke: flatStyleConfig['circle-stroke-color'] ? new Stroke({
color: flatStyleConfig['circle-stroke-color'],
width: flatStyleConfig['circle-stroke-width'] || 1
}) : undefined
});
}
// 处理文本样式
if (flatStyleConfig['text-value'] || flatStyleConfig['text-font']) {
styleOptions.text = new Text({
text: flatStyleConfig['text-value'] || '',
font: flatStyleConfig['text-font'] || '14px Arial',
fill: flatStyleConfig['text-fill-color'] ? new Fill({
color: flatStyleConfig['text-fill-color']
}) : undefined,
stroke: flatStyleConfig['text-stroke-color'] ? new Stroke({
color: flatStyleConfig['text-stroke-color'],
width: flatStyleConfig['text-stroke-width'] || 1
}) : undefined,
offsetY: flatStyleConfig['text-offset-y'] || 0,
offsetX: flatStyleConfig['text-offset-x'] || 0
});
}
return new Style(styleOptions);
};
// 应用转换后的样式到要素
const applyFlatStyleToFeature = (feature, flatStyleConfig) => {
const traditionalStyle = convertFlatStyleToTraditional(flatStyleConfig);
feature.setStyle(traditionalStyle);
};
扁平样式格式的特点和使用场景
✅ WebGL 图层的优势
- 性能优越:WebGL 渲染比 Canvas 渲染更快
- 简洁配置:扁平样式格式更易读写
- 表达式支持:支持复杂的条件样式表达式
- 大数据量:适合处理大量要素的可视化
⚠️ 使用限制
- 浏览器支持:需要 WebGL 支持
- 功能限制:某些高级样式功能可能不可用
- 调试复杂:WebGL 渲染的调试相对困难
- 兼容性:需要考虑降级方案
🔄 最佳实践
- 大数据量场景:优先考虑 WebGL 图层
- 复杂样式需求:使用传统 Vector 图层
- 混合使用:根据需求选择合适的图层类型
- 渐进增强:提供 WebGL 不可用时的降级方案
🎨 样式主题
预定义主题
javascript
const annotationThemes = {
default: {
text: {
font: '14px Arial',
fillColor: '#000000',
strokeColor: '#ffffff',
strokeWidth: 2
},
icon: {
scale: 1.0
},
circle: {
radius: 8,
fillColor: '#ff0000',
strokeColor: '#ffffff',
strokeWidth: 2
}
},
dark: {
text: {
font: '14px Arial',
fillColor: '#ffffff',
strokeColor: '#000000',
strokeWidth: 2
},
circle: {
radius: 8,
fillColor: '#00ff00',
strokeColor: '#000000',
strokeWidth: 2
}
},
minimal: {
text: {
font: '12px Arial',
fillColor: '#666666',
strokeColor: 'transparent'
},
circle: {
radius: 6,
fillColor: '#0066cc',
strokeColor: 'transparent'
}
}
};
// 应用主题
const applyTheme = (annotationManager, themeName) => {
const theme = annotationThemes[themeName] || annotationThemes.default;
// 更新所有现有标注的样式
annotationManager.features.forEach((feature, id) => {
const type = feature.get('type');
const themeStyle = theme[type] || theme.default;
if (themeStyle) {
const newStyle = annotationManager.createStyle({ [type]: themeStyle });
feature.setStyle(newStyle);
}
});
};
📋 最佳实践
性能优化
- 分层管理:将不同类型的标注放在不同图层
- 动态加载:根据缩放级别动态显示标注
- 样式缓存:缓存常用样式对象
- 批量操作:使用批量添加/删除方法
用户体验
- 视觉反馈:提供悬停和点击效果
- 响应式设计:适配不同设备和屏幕尺寸
- 加载状态:显示标注加载进度
- 错误处理:优雅处理图标加载失败等情况
代码组织
- 模块化设计:将标注功能封装为独立模块
- 配置驱动:使用配置对象管理样式和行为
- 事件解耦:使用事件系统实现组件间通信
- 类型安全:使用 TypeScript 提供类型检查
🎯 下一步:学习 弹出框和交互 功能!