Skip to content

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 图层的优势

  1. 性能优越:WebGL 渲染比 Canvas 渲染更快
  2. 简洁配置:扁平样式格式更易读写
  3. 表达式支持:支持复杂的条件样式表达式
  4. 大数据量:适合处理大量要素的可视化

⚠️ 使用限制

  1. 浏览器支持:需要 WebGL 支持
  2. 功能限制:某些高级样式功能可能不可用
  3. 调试复杂:WebGL 渲染的调试相对困难
  4. 兼容性:需要考虑降级方案

🔄 最佳实践

  1. 大数据量场景:优先考虑 WebGL 图层
  2. 复杂样式需求:使用传统 Vector 图层
  3. 混合使用:根据需求选择合适的图层类型
  4. 渐进增强:提供 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);
    }
  });
};

📋 最佳实践

性能优化

  1. 分层管理:将不同类型的标注放在不同图层
  2. 动态加载:根据缩放级别动态显示标注
  3. 样式缓存:缓存常用样式对象
  4. 批量操作:使用批量添加/删除方法

用户体验

  1. 视觉反馈:提供悬停和点击效果
  2. 响应式设计:适配不同设备和屏幕尺寸
  3. 加载状态:显示标注加载进度
  4. 错误处理:优雅处理图标加载失败等情况

代码组织

  1. 模块化设计:将标注功能封装为独立模块
  2. 配置驱动:使用配置对象管理样式和行为
  3. 事件解耦:使用事件系统实现组件间通信
  4. 类型安全:使用 TypeScript 提供类型检查

🎯 下一步:学习 弹出框和交互 功能!

如有转载或 CV 的请标注本站原文地址