diff --git a/skills/l7/COMPLETENESS_REPORT.md b/skills/l7/COMPLETENESS_REPORT.md new file mode 100644 index 0000000..179d649 --- /dev/null +++ b/skills/l7/COMPLETENESS_REPORT.md @@ -0,0 +1,365 @@ +# L7 Skills 文档完备性分析报告 + +生成时间:2026年1月26日 + +## 📊 概览 + +本报告基于 L7 官方 API 文档(site/docs/api)与当前 Skills 文档(skills/references)的对比分析。 + +### 现有 Skills 文档统计 + +**总计:20 个文档** + +#### 按目录分类: + +- **Core(核心)**: 3 个 + - scene.md (场景初始化) + - scene-lifecycle.md (场景生命周期) + - scene-methods.md (场景方法) + +- **Data(数据)**: 4 个 + - source-geojson.md + - source-csv.md + - source-json.md + - source-parser.md + +- **Layers(图层)**: 6 个 + - point.md (点图层) + - line.md (线图层) + - polygon.md (面图层) + - heatmap.md (热力图) + - image.md (图片图层) + - raster.md (栅格图层) + +- **Visual(视觉)**: 2 个 + - mapping.md (视觉映射) + - style.md (样式配置) + +- **Interaction(交互)**: 3 个 + - events.md (事件处理) + - popup.md (弹窗) + - components.md (组件) + +- **Animation(动画)**: 1 个 + - layer-animation.md (图层动画) + +- **Performance(性能)**: 1 个 + - optimization.md (性能优化) + +--- + +## ✅ 已覆盖的核心功能 + +### 1. Scene 场景 ✅ + +- ✅ 场景初始化 +- ✅ 场景生命周期 +- ✅ 场景方法(完整的 API) + +### 2. 基础图层 ✅ + +- ✅ PointLayer(点图层) +- ✅ LineLayer(线图层) +- ✅ PolygonLayer(面图层) +- ✅ HeatmapLayer(热力图) +- ✅ ImageLayer(图片图层) +- ✅ RasterLayer(栅格图层) + +### 3. 数据源 ✅ + +- ✅ GeoJSON +- ✅ CSV +- ✅ JSON +- ✅ Parser(解析器) + +### 4. 视觉编码 ✅ + +- ✅ 颜色、大小、形状映射 +- ✅ 样式配置 + +### 5. 交互功能 ✅ + +- ✅ 事件处理 +- ✅ Popup +- ✅ Components(Marker、Controls、Legend) + +### 6. 动画和性能 ✅ + +- ✅ 图层动画 +- ✅ 性能优化 + +--- + +## ❌ 缺失的重要功能 + +### 🔴 高优先级(推荐补充) + +#### 1. 地图引擎配置(Map) + +**官方文档位置**: `site/docs/api/map/` + +缺失内容: + +- ❌ **GaodeMap**(高德地图)- 最常用的底图 +- ❌ **Mapbox**(Mapbox 地图) +- ❌ **MapLibre**(开源地图) +- ❌ **Map**(独立地图引擎) +- ❌ BMap(百度地图) +- ❌ Tencent(腾讯地图) +- ❌ Tianditu(天地图) +- ❌ Leaflet + +**推荐新增文档**: + +- `skills/references/core/map-types.md` - 地图类型配置(已在 index.md 中引用但未实现) + +#### 2. 瓦片图层(Tile Layers) + +**官方文档位置**: `site/docs/api/tile/` + +缺失内容: + +- ❌ **VectorTileLayer**(矢量瓦片)- 重要的大数据渲染方案 +- ❌ **RasterTileLayer**(栅格瓦片) +- ❌ **GeoJSONVTTileLayer**(GeoJSON 切片) +- ❌ **TileDebugLayer**(瓦片调试) + +**推荐新增文档**: + +- `skills/references/layers/tile-vector.md` +- `skills/references/layers/tile-raster.md` + +#### 3. 数据源补充 + +**官方文档位置**: `site/docs/api/source/` + +缺失内容: + +- ❌ **MVT**(Mapbox Vector Tile)- 矢量瓦片数据源 +- ❌ **Image** 数据源 +- ❌ **Raster** 数据源 +- ❌ **RasterTile** 数据源 +- ❌ **NDI** 数据源(遥感影像) +- ❌ **RGB** 数据源(遥感影像) + +**推荐新增文档**: + +- `skills/references/data/source-mvt.md` +- `skills/references/data/source-raster.md` + +#### 4. 图层通用特性 + +**官方文档位置**: `site/docs/api/base_layer/` 和各图层的子页面 + +缺失内容: + +- ❌ **BaseLayer**(图层基类)- 所有图层的通用方法 +- ❌ **Layer Options**(图层配置) +- ❌ **Layer Methods**(图层方法:show/hide/fitBounds/setIndex 等) +- ❌ **Layer Events**(图层事件:click/mousemove/mouseout 等) + +每个图层类型都有详细的子文档: + +- ❌ color.md(颜色映射详解) +- ❌ size.md(大小映射详解) +- ❌ shape.md(形状类型详解) +- ❌ scale.md(数据映射详解) +- ❌ animate.md(动画配置详解) +- ❌ style.md(样式配置详解) +- ❌ source.md(数据源配置详解) +- ❌ options.md(图层配置详解) + +**推荐新增文档**: + +- `skills/references/layers/base-layer.md` - 图层基类和通用方法 +- `skills/references/layers/layer-events.md` - 图层事件详解 + +#### 5. 高级组件 + +**官方文档位置**: `site/docs/api/component/` + +部分缺失: + +- ✅ Marker(已包含在 components.md) +- ✅ Popup(已有独立文档) +- ✅ Control(已包含在 components.md) +- ❌ **MarkerLayer**(Marker 统一管理) +- ❌ **LayerPopup**(图层绑定弹窗) +- ❌ **Zoom** 控件(缩放控制) +- ❌ **Scale** 控件(比例尺) +- ❌ **Logo** 控件 +- ❌ **Fullscreen** 控件(全屏控制) +- ❌ **ExportImage** 控件(导出图片) +- ❌ **MapTheme** 控件(地图主题切换) +- ❌ **MouseLocation** 控件(鼠标位置显示) +- ❌ **Geolocate** 控件(定位控制) +- ❌ **LayerSwitch** 控件(图层切换) +- ❌ **Swipe** 控件(卷帘对比) + +**推荐新增文档**: + +- `skills/references/interaction/controls.md` - 各类控件详解 +- `skills/references/interaction/layer-popup.md` - 图层弹窗 +- `skills/references/interaction/marker-layer.md` - MarkerLayer + +### 🟡 中优先级(可选补充) + +#### 6. React 组件 + +**官方文档位置**: `site/docs/api/react/` + +- ❌ React 绑定和组件封装 + +**推荐新增文档**: + +- `skills/references/frameworks/react.md` + +#### 7. 其他特殊图层 + +**官方文档位置**: `site/docs/api/other/` + +- ❌ **MaskLayer**(遮罩图层) +- ❌ **WindLayer**(风场图层) +- ❌ **CanvasLayer**(Canvas 图层) +- ❌ **CityBuildingLayer**(城市建筑) +- ❌ **GeometryLayer**(几何图层) +- ❌ **EarthLayer**(地球图层) + +**推荐新增文档**: + +- `skills/references/layers/other-layers.md`(已在 index.md 引用但内容不完整) + +#### 8. 高级特性 + +**官方文档位置**: `site/docs/api/experiment/` 和 `site/docs/api/debug/` + +- ❌ 实验性功能 +- ❌ 调试工具 + +### 🟢 低优先级(补充性内容) + +#### 9. 纹理(Texture) + +- ❌ 线图层纹理配置(`line_layer/texture.md`) + +#### 10. 教程内容 + +**官方文档位置**: `site/docs/tutorial/` + +当前 skills 主要关注 API,教程性内容较少。 + +--- + +## 📋 具体建议补充清单 + +### 第一批(核心必备,共 5 个) + +1. ✅ `skills/references/core/map-types.md` - 地图引擎配置(高德、Mapbox、MapLibre、Map) +2. ✅ `skills/references/layers/base-layer.md` - 图层基类和通用方法 +3. ✅ `skills/references/layers/tile-vector.md` - 矢量瓦片图层 +4. ✅ `skills/references/data/source-mvt.md` - MVT 数据源 +5. ✅ `skills/references/interaction/controls.md` - 控件详解(Zoom、Scale、Fullscreen 等) + +### 第二批(功能完善,共 5 个) + +6. `skills/references/layers/layer-events.md` - 图层事件详解 +7. `skills/references/interaction/layer-popup.md` - 图层弹窗 +8. `skills/references/interaction/marker-layer.md` - MarkerLayer +9. `skills/references/layers/tile-raster.md` - 栅格瓦片图层 +10. `skills/references/data/source-raster.md` - 栅格数据源 + +### 第三批(深度扩展,共 5 个) + +11. `skills/references/visual/color-mapping.md` - 颜色映射详解 +12. `skills/references/visual/size-mapping.md` - 大小映射详解 +13. `skills/references/visual/shape-types.md` - 形状类型详解 +14. `skills/references/layers/mask.md` - 遮罩图层 +15. `skills/references/frameworks/react.md` - React 集成 + +--- + +## 📊 完备性评分 + +| 类别 | 完备度 | 评分 | +| -------------------------------------- | ---------------- | ---------- | +| 核心场景(Scene) | ███████████ 100% | ⭐⭐⭐⭐⭐ | +| 基础图层(Point/Line/Polygon/Heatmap) | ██████████░ 95% | ⭐⭐⭐⭐⭐ | +| 数据源(GeoJSON/CSV/JSON) | ████████░░░ 75% | ⭐⭐⭐⭐ | +| 地图引擎配置 | ░░░░░░░░░░░ 0% | ⭐ | +| 瓦片图层 | ░░░░░░░░░░░ 0% | ⭐ | +| 视觉编码 | ████████░░░ 80% | ⭐⭐⭐⭐ | +| 交互组件 | ██████░░░░░ 60% | ⭐⭐⭐ | +| 动画 | ████████░░░ 80% | ⭐⭐⭐⭐ | +| 性能优化 | ████████░░░ 80% | ⭐⭐⭐⭐ | +| 框架集成 | ░░░░░░░░░░░ 0% | ⭐ | + +**总体完备度:约 57%** + +--- + +## 🎯 优先级行动建议 + +### 立即补充(Week 1) + +1. **地图引擎配置** - 这是使用 L7 的第一步,非常重要 +2. **图层基类文档** - 帮助理解所有图层的通用特性 +3. **控件详解** - 完善交互功能文档 + +### 短期补充(Week 2-3) + +4. **矢量瓦片** - 大数据渲染的核心方案 +5. **MVT 数据源** - 配合矢量瓦片使用 +6. **图层事件** - 完善交互功能 + +### 中期补充(Month 2) + +7. 各图层的详细子文档(color/size/shape/scale) +8. 高级组件(LayerPopup、MarkerLayer 等) +9. 特殊图层(Mask、Wind 等) + +### 长期补充(Month 3+) + +10. React 框架集成 +11. 实验性功能 +12. 调试工具 + +--- + +## 💡 文档结构建议 + +当前 skills 文档结构清晰,建议保持: + +``` +skills/references/ +├── core/ # 核心功能(Scene、Map) +├── data/ # 数据源 +├── layers/ # 图层类型 +├── visual/ # 视觉编码 +├── interaction/ # 交互功能 +├── animation/ # 动画 +├── performance/ # 性能优化 +└── frameworks/ # 框架集成(新增) +``` + +--- + +## 📝 总结 + +**优点**: + +- ✅ 核心场景(Scene)文档非常完善,包含生命周期和方法 +- ✅ 基础图层(6 种主要图层)都已覆盖 +- ✅ 基础数据源(GeoJSON/CSV/JSON)齐全 +- ✅ 文档结构清晰,遵循统一规范 +- ✅ 代码示例丰富,实用性强 + +**不足**: + +- ❌ 缺少地图引擎配置文档(这是使用 L7 的第一步) +- ❌ 缺少瓦片图层(大数据场景的核心方案) +- ❌ 控件文档不够详细(只有概述,缺少各控件的详细用法) +- ❌ 缺少图层通用特性文档(BaseLayer) +- ❌ 缺少框架集成文档(React) + +**建议**: +优先补充前 5 个文档(地图引擎、图层基类、矢量瓦片、MVT、控件详解),可将完备度提升至 75% 以上。 diff --git a/skills/l7/README.md b/skills/l7/README.md new file mode 100644 index 0000000..8122c25 --- /dev/null +++ b/skills/l7/README.md @@ -0,0 +1,213 @@ +# L7 Skill Library + +> 为 AntV L7 地理空间可视化引擎设计的结构化技能知识库,遵循 skill-creator 最佳实践。 + +## 🎯 设计原则 + +基于 [skill-creator](https://github.com/anthropics/skills) 的最佳实践: + +1. **渐进式披露** (Progressive Disclosure) + - SKILL.md: 概览和快速入门 (~200 lines) + - references/: 详细文档,按需加载 + - metadata/: 机器可读的依赖和标签 + +2. **按领域组织** (Domain Organization) + - references/core/: 核心功能(场景初始化、地图类型) + - references/data/: 数据处理(GeoJSON、CSV、解析器) + - references/layers/: 图层类型(点、线、面、热力图等) + - references/interaction/: 交互组件(事件、Popup、Controls) + - references/animation/: 动画效果(图层动画、轨迹动画) + - references/performance/: 性能优化 + +3. **精简高效** (Concise and Efficient) + - 避免冗余,信息只存在一个地方 + - 优先代码示例而非冗长解释 + - 详细内容移至 references,保持主文件精简 + +## 📁 目录结构 + +``` +.skills/ +├── SKILL.md # 主入口:概览 + 快速开始 + 导航 +├── index.md # 技能索引:场景查找 + 文档导航 +├── README.md # 本文件:使用说明 +├── references/ # 详细文档(按需加载) +│ ├── core/ +│ │ ├── scene.md # Scene 完整文档 +│ │ ├── scene-methods.md # Scene 方法详解 +│ │ ├── scene-lifecycle.md # 场景生命周期 +│ │ └── map-types.md # 地图类型配置 +│ ├── data/ +│ │ ├── geojson.md # GeoJSON 数据处理 +│ │ ├── csv.md # CSV 数据处理 +│ │ ├── json.md # JSON 数据处理 +│ │ ├── source-mvt.md # MVT 瓦片数据源 +│ │ └── parser.md # 数据解析配置 +│ ├── layers/ +│ │ ├── base-layer.md # 基础图层 API +│ │ ├── point.md # 点图层 +│ │ ├── line.md # 线图层 +│ │ ├── polygon.md # 面图层 +│ │ ├── heatmap.md # 热力图 +│ │ ├── image.md # 图片图层 +│ │ └── tile-vector.md # 矢量瓦片图层 +│ ├── interaction/ +│ │ ├── events.md # 事件处理 +│ │ ├── popup.md # Popup 组件 +│ │ ├── controls.md # 控件组件 +│ │ └── components.md # Marker/Legend +│ ├── animation/ +│ │ └── layer-animation.md # 图层动画和轨迹动画 +│ ├── performance/ +│ │ └── optimization.md # 性能优化指南 +└── metadata/ + ├── skill-dependency.json # 技能依赖关系 + ├── skill-tags.json # 中英文标签 + └── version-compatibility.json +``` + +## 🚀 快速开始 + +### 对于 AI 模型 + +**三级加载系统**: + +1. **始终加载**: SKILL.md (~200 lines) + - 获取概览和快速入门 + - 查看文档导航表 + +2. **按需加载**: references/\*.md + - 根据用户需求选择具体文档 + - 示例: "显示点位" → 加载 `references/layers/point.md` + +3. **辅助信息**: metadata/\*.json + - 检查依赖关系 + - 搜索相关标签 + +### 文档选择策略 + +| 用户请求 | 加载文档 | +| ---------- | ------------------------------------------------------- | +| "创建地图" | references/core/scene.md | +| "显示点位" | references/layers/point.md + references/data/geojson.md | +| "热力图" | references/layers/heatmap.md | +| "添加交互" | references/interaction/events.md | +| "性能慢" | references/performance/optimization.md | + +### 技能组合模式 + +复杂需求需要组合多个 references: + +``` +地图可视化 = scene.md + polygon.md + point.md + events.md + popup.md +轨迹动画 = scene.md + line.md + layer-animation.md +热力分析 = scene.md + heatmap.md + parser.md +``` + +## 📖 Reference 文件格式 + +每个 reference 文件遵循统一结构: + +```markdown +# 标题 + +## 目录 (对于 >100 行的文件) + +## 快速示例 + +## 详细配置 + +## 使用场景 + +## 常见问题 + +## 相关文档 +``` + +## 🔍 检索策略 + +### 按标签检索 + +使用 `metadata/skill-tags.json`: + +```json +{ + "point-layer": ["point", "scatter", "bubble", "点", "散点", "气泡"], + "scene-initialization": ["scene", "map", "init", "场景", "地图", "初始化"] +} +``` + +### 按依赖检索 + +使用 `metadata/skill-dependency.json`: + +```json +{ + "point-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "color-mapping"], + "nextSteps": ["event-handling", "popup"] + } +} +``` + +## 💡 最佳实践 + +### 避免重复加载 + +``` +❌ 不要同时加载 SKILL.md 和所有 references +✅ 先读 SKILL.md,根据需求加载特定 references +``` + +### 组合使用文档 + +``` +❌ 不要在单个 reference 中重复基础概念 +✅ 通过交叉引用链接相关文档 +``` + +### 优先示例代码 + +``` +❌ 避免冗长的文字解释 +✅ 提供清晰的代码示例和注释 +``` + +## 🔧 维护指南 + +### 添加新 Reference + +1. 确定所属领域(core/data/layers/etc.) +2. 创建文件到对应 `references/` 子目录 +3. 更新 `SKILL.md` 的导航表 +4. 更新 `index.md` 的文档列表 +5. 添加标签到 `metadata/skill-tags.json` +6. 添加依赖到 `metadata/skill-dependency.json` + +### 更新现有 Reference + +1. 保持文件结构一致 +2. 确保交叉引用链接有效 +3. 更新相关的 metadata 文件 +4. 记录版本变化和 breaking changes + +## 📊 统计信息 + +- **核心文档**: 4 个(场景、方法、生命周期、地图类型) +- **数据处理**: 5 个(GeoJSON、CSV、JSON、MVT、解析器) +- **图层类型**: 7 个(基础、点、线、面、热力、图片、瓦片) +- **交互控制**: 4 个(事件、Popup、控件、组件) +- **动画效果**: 1 个(图层动画) +- **性能优化**: 1 个 +- **总计**: 22 个核心文档 + +## 🔗 相关资源 + +- **官方文档**: https://l7.antv.antgroup.com/ +- **GitHub**: https://github.com/antvis/L7 +- **Skill Creator**: https://github.com/anthropics/skills + +--- + +**注意**: 本技能库设计用于 AI 模型代码生成,建议配合向量检索系统使用以获得最佳效果。 diff --git a/skills/l7/SKILL.md b/skills/l7/SKILL.md new file mode 100644 index 0000000..80f37a3 --- /dev/null +++ b/skills/l7/SKILL.md @@ -0,0 +1,193 @@ +--- +name: antv-l7 +description: | + Comprehensive guide for AntV L7 geospatial visualization library. Use when users need to: + (1) Create interactive maps with WebGL rendering + (2) Visualize geographic data (points, lines, polygons, heatmaps) + (3) Build location-based data dashboards + (4) Add map layers, interactions, or animations + (5) Process and display GeoJSON, CSV, or other spatial data + (6) Integrate maps with AMap (GaodeMap), Mapbox, Maplibre, or standalone L7 Map + (7) Optimize performance for large-scale geographic datasets +license: MIT +--- + +# AntV L7 Geospatial Visualization + +AntV L7 是基于 WebGL 的大规模地理空间数据可视化引擎,支持多种地图底图和丰富的可视化图层类型。 + +## Quick Start + +创建最简单的 L7 地图应用: + +```javascript +import { Scene, PointLayer } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +// 1. 初始化场景 +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + style: 'light', + }), +}); + +// 2. 添加图层 +scene.on('loaded', () => { + const pointLayer = new PointLayer() + .source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(pointLayer); +}); +``` + +## Core Workflow + +L7 的典型开发流程: + +``` +1. 场景初始化 (Scene) → 2. 数据准备 → 3. 创建图层 (Layer) → 4. 添加交互 → 5. 优化性能 +``` + +## 📚 Reference Documentation + +详细文档按领域组织,根据需要加载: + +### 基础功能 (references/core/) + +- **[scene.md](references/core/scene.md)** - Scene 初始化、生命周期、方法 +- **[map-types.md](references/core/map-types.md)** - GaodeMap、Mapbox、Maplibre、Map 的配置 + +### 数据处理 (references/data/) + +- **[geojson.md](references/data/geojson.md)** - GeoJSON 格式、解析、转换 +- **[csv.md](references/data/csv.md)** - CSV 数据加载和处理 +- **[json.md](references/data/json.md)** - JSON 数据、OD 数据、路径数据 +- **[parser.md](references/data/parser.md)** - Parser 配置、Transform 转换 + +### 图层类型 (references/layers/) + +- **[point.md](references/layers/point.md)** - 点图层:散点、气泡、3D 柱状 +- **[line.md](references/layers/line.md)** - 线图层:路径、弧线、流线 +- **[polygon.md](references/layers/polygon.md)** - 面图层:填充、3D 建筑、choropleth +- **[heatmap.md](references/layers/heatmap.md)** - 热力图:密度分布、网格热力 +- **[image.md](references/layers/image.md)** - 图片图层:卫星图、航拍图、平面图 +- **[raster.md](references/layers/raster.md)** - 栅格瓦片图层:XYZ/TMS 瓦片服务 +- **[other-layers.md](references/layers/other-layers.md)** - 其他图层类型 + +### 视觉映射 (references/visual/) + +- **[mapping.md](references/visual/mapping.md)** - 颜色、大小、形状映射 +- **[style.md](references/visual/style.md)** - 透明度、描边、纹理等样式 + +### 交互组件 (references/interaction/) + +- **[events.md](references/interaction/events.md)** - 点击、悬停、选中事件 +- **[components.md](references/interaction/components.md)** - Popup、Marker、Controls、Legend + +### 动画效果 (references/animation/) + +- **[layer-animation.md](references/animation/layer-animation.md)** - 图层动画、轨迹动画 + +### 性能优化 (references/performance/) + +- **[optimization.md](references/performance/optimization.md)** - 数据过滤、聚合、图层管理 + +## 使用指南 + +### 按用户需求选择文档 + +| 用户请求示例 | 加载的文档 | +| -------------- | -------------------------------- | +| "创建一个地图" | core/scene.md | +| "显示点位数据" | layers/point.md, data/geojson.md | +| "绘制路径" | layers/line.md | +| "热力图" | layers/heatmap.md | +| "添加点击事件" | interaction/events.md | +| "显示弹窗" | interaction/components.md | + +### 技能组合模式 + +复杂需求需要组合多个技能: + +``` +城市可视化 = scene + polygon + point + events + popup +轨迹动画 = scene + line + animation +热力分析 = scene + heatmap + data/json +``` + +### 依赖检查 + +使用 `metadata/skill-dependency.json` 检查技能依赖关系: + +```json +{ + "point-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "color-mapping"], + "nextSteps": ["event-handling", "popup"] + } +} +``` + +## 版本信息 + +- **当前版本**: L7 2.x +- **浏览器支持**: Chrome ≥60, Firefox ≥60, Safari ≥12 +- **坐标系**: WGS84 (地理坐标) / Plane coordinates (独立 Map) +- **底图**: 高德地图、Mapbox、Maplibre、L7 Map (独立) + +## 最佳实践 + +1. **场景初始化优先**: 始终从创建 Scene 开始 +2. **数据格式规范**: 优先使用 GeoJSON 标准格式 +3. **性能优先**: 大数据量时使用数据过滤和聚合 +4. **渐进增强**: 先实现基础功能,再添加交互和动画 +5. **错误处理**: 添加事件监听和数据验证 + +## 快速参考 + +### 常用导入 + +```javascript +// 核心 +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +// 图层 +import { PointLayer, LineLayer, PolygonLayer, HeatmapLayer } from '@antv/l7'; + +// 组件 +import { Popup, Marker } from '@antv/l7'; +``` + +### 地图样式选项 + +- `'light'` - 浅色风格 +- `'dark'` - 深色风格 +- `'normal'` - 标准风格 +- `'satellite'` - 卫星影像 +- `'blank'` - 空白底图(独立 Map) + +### 坐标格式 + +```javascript +[经度, 纬度]; // [120.19, 30.26] +// 经度: -180 ~ 180 +// 纬度: -90 ~ 90 +``` + +## 元数据 + +- **skill-dependency.json** - 技能依赖关系图 +- **skill-tags.json** - 中英文标签检索 +- **version-compatibility.json** - 版本兼容性信息 + +查看 [index.md](index.md) 获取完整技能列表和导航。 diff --git a/skills/l7/index.md b/skills/l7/index.md new file mode 100644 index 0000000..6dee970 --- /dev/null +++ b/skills/l7/index.md @@ -0,0 +1,69 @@ +# L7 技能索引(Skill Index) + +> 从这里开始,快速查找和组合 L7 技能文档 + +## 📚 按领域查找 + +### 1. 核心功能 Core + +- [scene.md](references/core/scene.md) - Scene 初始化、生命周期、方法 +- [map-types.md](references/core/map-types.md) - 地图类型配置 + +### 2. 数据处理 Data + +- [geojson.md](references/data/geojson.md) - GeoJSON 格式和解析 +- [csv.md](references/data/csv.md) - CSV 数据加载 +- [json.md](references/data/json.md) - JSON 数据源 +- [parser.md](references/data/parser.md) - 数据解析配置 + +### 3. 图层类型 Layers + +- [point.md](references/layers/point.md) - 点图层 +- [line.md](references/layers/line.md) - 线图层 +- [polygon.md](references/layers/polygon.md) - 面图层 +- [heatmap.md](references/layers/heatmap.md) - 热力图 +- [image.md](references/layers/image.md) - 图片图层 +- [raster.md](references/layers/raster.md) - 栅格瓦片图层 +- [other-layers.md](references/layers/other-layers.md) - 其他图层 + +### 4. 视觉映射 Visual + +- [mapping.md](references/visual/mapping.md) - 颜色、大小、形状映射 +- [style.md](references/visual/style.md) - 样式配置 + +### 5. 交互组件 Interaction + +- [events.md](references/interaction/events.md) - 事件处理 +- [popup.md](references/interaction/popup.md) - Popup 弹窗 +- [components.md](references/interaction/components.md) - Marker、Controls、Legend + +### 6. 动画效果 Animation + +- [layer-animation.md](references/animation/layer-animation.md) - 图层动画、轨迹动画 + +### 7. 性能优化 Performance + +- [optimization.md](references/performance/optimization.md) - 数据过滤、聚合、图层管理 + +### 🎯 按场景查找 + +| 用户需求 | 推荐文档 | 难度 | +| -------- | --------------------------------------------------------------------------------- | ------ | +| 创建地图 | [scene.md](references/core/scene.md) | ⭐ | +| 显示点位 | [point.md](references/layers/point.md) + [geojson.md](references/data/geojson.md) | ⭐ | +| 绘制路径 | [line.md](references/layers/line.md) | ⭐ | +| 区域填充 | [polygon.md](references/layers/polygon.md) | ⭐ | +| 热力图 | [heatmap.md](references/layers/heatmap.md) | ⭐⭐ | +| 点击事件 | [events.md](references/interaction/events.md) | ⭐⭐ | +| 显示弹窗 | [popup.md](references/interaction/popup.md) | ⭐⭐ | +| 轨迹动画 | [layer-animation.md](references/animation/layer-animation.md) | ⭐⭐ | +| 性能优化 | [optimization.md](references/performance/optimization.md) | ⭐⭐⭐ | + +--- + +## 📖 相关资源 + +- **主文档**:[SKILL.md](SKILL.md) - 快速入门与概览 +- **依赖关系**:[skill-dependency.json](metadata/skill-dependency.json) +- **标签检索**:[skill-tags.json](metadata/skill-tags.json) +- **版本兼容性**:[version-compatibility.json](metadata/version-compatibility.json) diff --git a/skills/l7/metadata/skill-dependency.json b/skills/l7/metadata/skill-dependency.json new file mode 100644 index 0000000..940a927 --- /dev/null +++ b/skills/l7/metadata/skill-dependency.json @@ -0,0 +1,122 @@ +{ + "scene-initialization": { + "requires": [], + "optional": [], + "conflicts": [], + "nextSteps": ["point-layer", "line-layer", "polygon-layer"] + }, + "point-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "color-mapping", "size-mapping"], + "conflicts": [], + "nextSteps": ["event-handling", "popup"] + }, + "line-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "color-mapping", "trajectory-animation"], + "conflicts": [], + "nextSteps": ["event-handling", "popup"] + }, + "polygon-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "color-mapping"], + "conflicts": [], + "nextSteps": ["event-handling", "popup", "highlight-select"] + }, + "heatmap-layer": { + "requires": ["scene-initialization"], + "optional": ["source-geojson", "aggregation"], + "conflicts": [], + "nextSteps": [] + }, + "event-handling": { + "requires": ["scene-initialization"], + "optional": ["popup", "marker", "highlight-select"], + "conflicts": [], + "nextSteps": ["popup", "highlight-select"] + }, + "popup": { + "requires": ["scene-initialization"], + "optional": ["event-handling"], + "conflicts": [], + "nextSteps": [] + }, + "marker": { + "requires": ["scene-initialization"], + "optional": [], + "conflicts": [], + "nextSteps": [] + }, + "color-mapping": { + "requires": [], + "optional": ["categorical-scale", "quantitative-scale"], + "conflicts": [], + "nextSteps": [] + }, + "size-mapping": { + "requires": [], + "optional": ["quantitative-scale"], + "conflicts": [], + "nextSteps": [] + }, + "layer-animation": { + "requires": [], + "optional": [], + "conflicts": [], + "nextSteps": [] + }, + "trajectory-animation": { + "requires": ["line-layer"], + "optional": ["layer-animation"], + "conflicts": [], + "nextSteps": [] + }, + "vector-tile": { + "requires": ["scene-initialization"], + "optional": ["performance-optimization"], + "conflicts": [], + "nextSteps": [] + }, + "data-filtering": { + "requires": [], + "optional": [], + "conflicts": [], + "nextSteps": [] + }, + "aggregation": { + "requires": [], + "optional": ["heatmap-layer"], + "conflicts": [], + "nextSteps": [] + }, + "city-visualization": { + "requires": ["scene-initialization", "polygon-layer", "point-layer", "event-handling", "popup"], + "optional": ["line-layer", "controls"], + "conflicts": [], + "nextSteps": [] + }, + "trajectory-visualization": { + "requires": ["scene-initialization", "line-layer", "trajectory-animation"], + "optional": ["marker", "popup"], + "conflicts": [], + "nextSteps": [] + }, + "district-visualization": { + "requires": ["scene-initialization", "polygon-layer", "event-handling"], + "optional": ["popup", "legend", "highlight-select"], + "conflicts": [], + "nextSteps": [] + }, + "heatmap-analysis": { + "requires": ["scene-initialization", "heatmap-layer"], + "optional": ["legend", "controls"], + "conflicts": [], + "nextSteps": [] + }, + "od-flow-visualization": { + "requires": ["scene-initialization", "line-layer"], + "optional": ["trajectory-animation", "point-layer"], + "conflicts": [], + "nextSteps": [] + } +} diff --git a/skills/l7/metadata/skill-tags.json b/skills/l7/metadata/skill-tags.json new file mode 100644 index 0000000..4410f6c --- /dev/null +++ b/skills/l7/metadata/skill-tags.json @@ -0,0 +1,40 @@ +{ + "scene-initialization": ["scene", "map", "init", "setup", "create", "基础", "初始化"], + "point-layer": ["point", "scatter", "bubble", "circle", "点", "散点", "气泡"], + "line-layer": ["line", "path", "arc", "route", "线", "路径", "弧线", "轨迹"], + "polygon-layer": ["polygon", "fill", "area", "region", "面", "区域", "填充"], + "heatmap-layer": ["heatmap", "热力图", "密度", "分布"], + "raster-layer": ["raster", "image", "栅格", "影像"], + "image-layer": ["image", "picture", "图片"], + "color-mapping": ["color", "颜色", "映射", "着色", "配色"], + "size-mapping": ["size", "大小", "尺寸", "映射"], + "shape-mapping": ["shape", "形状", "符号"], + "style-config": ["style", "样式", "配置"], + "event-handling": ["event", "click", "mouse", "事件", "点击", "交互"], + "highlight-select": ["highlight", "select", "高亮", "选中"], + "mouse-control": ["mouse", "cursor", "鼠标", "控制"], + "popup": ["popup", "tooltip", "info", "弹窗", "提示", "信息框"], + "marker": ["marker", "标注", "图标", "pin"], + "controls": ["control", "zoom", "scale", "控件", "缩放", "比例尺"], + "legend": ["legend", "图例"], + "layer-animation": ["animation", "动画", "效果"], + "trajectory-animation": ["trajectory", "path", "轨迹", "路径动画"], + "quantitative-scale": ["scale", "quantitative", "比例尺", "定量"], + "categorical-scale": ["scale", "categorical", "比例尺", "定性", "分类"], + "vector-tile": ["tile", "mvt", "瓦片", "矢量"], + "raster-tile": ["tile", "raster", "瓦片", "栅格"], + "data-filtering": ["filter", "过滤", "筛选", "性能"], + "layer-management": ["layer", "manage", "图层管理", "层级"], + "aggregation": ["aggregate", "cluster", "聚合", "聚类"], + "mask-layer": ["mask", "clip", "遮罩", "裁剪"], + "multi-basemap": ["basemap", "底图", "切换"], + "custom-layer": ["custom", "extend", "自定义", "扩展"], + "city-visualization": ["city", "building", "城市", "建筑", "场景"], + "trajectory-visualization": ["trajectory", "path", "轨迹", "场景"], + "district-visualization": ["district", "region", "行政区", "区域", "场景"], + "heatmap-analysis": ["heatmap", "analysis", "热力", "分析", "场景"], + "od-flow-visualization": ["od", "flow", "migration", "流向", "迁徙", "场景"], + "data-not-showing": ["问题", "不显示", "debug", "troubleshoot"], + "performance-issues": ["问题", "性能", "卡顿", "优化"], + "style-not-working": ["问题", "样式", "不生效"] +} diff --git a/skills/l7/metadata/version-compatibility.json b/skills/l7/metadata/version-compatibility.json new file mode 100644 index 0000000..1afd4a6 --- /dev/null +++ b/skills/l7/metadata/version-compatibility.json @@ -0,0 +1,65 @@ +{ + "l7Version": "2.x", + "skills": { + "scene-initialization": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": { + "2.0.0": "API 重构,不兼容 1.x 版本" + } + }, + "point-layer": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": {} + }, + "line-layer": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": {} + }, + "polygon-layer": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": {} + }, + "heatmap-layer": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": {} + }, + "mask-layer": { + "minVersion": "2.7.2", + "deprecated": false, + "notes": "需要在 Scene 初始化时设置 stencil: true" + }, + "vector-tile": { + "minVersion": "2.5.0", + "deprecated": false, + "notes": "需要在 Scene 初始化时设置 stencil: true" + }, + "layer-animation": { + "minVersion": "2.0.0", + "deprecated": false, + "breaking_changes": { + "2.9.0": "动画配置参数调整" + } + } + }, + "dependencies": { + "@antv/l7": "^2.0.0", + "@antv/l7-maps": "^2.0.0" + }, + "browsers": { + "chrome": ">=60", + "firefox": ">=60", + "safari": ">=12", + "edge": ">=79" + }, + "notes": [ + "L7 2.x 版本不兼容 1.x", + "建议使用 2.11.0 及以上版本", + "WebGL 1.0 最低要求", + "部分功能需要 WebGL 2.0 支持" + ] +} diff --git a/skills/l7/references/animation/layer-animation.md b/skills/l7/references/animation/layer-animation.md new file mode 100644 index 0000000..2cfea55 --- /dev/null +++ b/skills/l7/references/animation/layer-animation.md @@ -0,0 +1,362 @@ +# Layer Animation Guide + +L7 图层动画和轨迹动画完整指南。 + +## 图层动画 + +### 数据更新动画 + +```javascript +// 基础数据更新 +layer.setData(newData); + +// 带动画的数据更新 +layer.animate(true); // 开启动画 +layer.setData(newData); +``` + +### 属性动画 + +```javascript +// 大小动画 +layer.size('value', [5, 20]).animate({ + enable: true, + interval: 0.1, // 时间间隔 + duration: 2, // 动画时长(秒) + trailLength: 0.5, // 拖尾长度 +}); + +// 颜色动画 +layer.color('type', { + values: ['#5B8FF9', '#5AD8A6'], + animate: { + duration: 2000, + repeat: true, + }, +}); +``` + +## 轨迹动画 + +### LineLayer 轨迹动画 + +```javascript +import { LineLayer } from '@antv/l7'; + +const pathData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { name: '路线1' }, + geometry: { + type: 'LineString', + coordinates: [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28], + [120.22, 30.29], + ], + }, + }, + ], +}; + +const lineLayer = new LineLayer().source(pathData).shape('line').size(3).color('#5B8FF9').animate({ + enable: true, + interval: 0.5, // 速度 + trailLength: 0.3, // 拖尾长度 + duration: 4, // 完整动画时长 +}); + +scene.addLayer(lineLayer); +``` + +### PointLayer 运动点 + +```javascript +// 运动的点 +const pointLayer = new PointLayer() + .source(pathData) + .shape('circle') + .size(10) + .color('#FF6B3B') + .animate({ + enable: true, + interval: 0.5, + duration: 4, + }); +``` + +### 组合使用:路径 + 运动点 + +```javascript +// 路径 +const pathLayer = new LineLayer().source(pathData).shape('line').size(2).color('#ccc'); + +// 运动的点 +const movingPoint = new PointLayer() + .source(pathData) + .shape('circle') + .size(8) + .color('#FF6B3B') + .animate({ + enable: true, + interval: 0.5, + duration: 4, + }); + +// 轨迹线(拖尾) +const trailLine = new LineLayer().source(pathData).shape('line').size(3).color('#5B8FF9').animate({ + enable: true, + interval: 0.5, + trailLength: 0.5, + duration: 4, +}); + +scene.addLayer(pathLayer); +scene.addLayer(trailLine); +scene.addLayer(movingPoint); +``` + +## ArcLayer 弧线动画 + +```javascript +import { LineLayer } from '@antv/l7'; + +const odData = [ + { + from_lng: 120.19, + from_lat: 30.26, + to_lng: 121.47, + to_lat: 31.23, + value: 100, + }, +]; + +const arcLayer = new LineLayer() + .source(odData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc') + .size(2) + .color('#5B8FF9') + .animate({ + enable: true, + interval: 0.3, + trailLength: 0.4, + duration: 3, + }) + .style({ + opacity: 0.8, + }); + +scene.addLayer(arcLayer); +``` + +## 时序数据动画 + +### 时间轴控制 + +```javascript +// 带时间戳的数据 +const timeSeriesData = [ + { lng: 120.19, lat: 30.26, time: 1609459200000, value: 10 }, + { lng: 120.2, lat: 30.27, time: 1609545600000, value: 20 }, + { lng: 120.21, lat: 30.28, time: 1609632000000, value: 30 }, +]; + +let currentTime = timeSeriesData[0].time; + +function updateVisualization(time) { + const filteredData = timeSeriesData.filter((d) => d.time <= time); + layer.setData(filteredData); +} + +// 播放动画 +let animationTimer = setInterval(() => { + currentTime += 86400000; // 增加一天 + updateVisualization(currentTime); + + if (currentTime >= timeSeriesData[timeSeriesData.length - 1].time) { + clearInterval(animationTimer); + } +}, 100); +``` + +## 相机动画 + +### 飞行动画 + +```javascript +// 飞到指定位置 +scene.flyTo({ + center: [120.19, 30.26], + zoom: 12, + pitch: 45, + bearing: 30, + duration: 2000, // 动画时长(毫秒) +}); +``` + +### 环绕动画 + +```javascript +let bearing = 0; + +function rotate() { + bearing = (bearing + 0.5) % 360; + scene.setBearing(bearing); + requestAnimationFrame(rotate); +} + +rotate(); +``` + +## 动画控制 + +### 开始/暂停/重置 + +```javascript +// 开始动画 +layer.animate(true); + +// 暂停动画 +layer.animate(false); + +// 重置并重新开始 +layer.animate(false); +layer.animate(true); +``` + +### 动画事件 + +```javascript +layer.on('animatestart', () => { + console.log('动画开始'); +}); + +layer.on('animateend', () => { + console.log('动画结束'); +}); +``` + +## 性能优化 + +### 1. 降低数据密度 + +```javascript +// 对复杂路径进行简化 +import * as turf from '@turf/turf'; + +const simplified = turf.simplify(pathData, { + tolerance: 0.01, + highQuality: false, +}); +``` + +### 2. 控制动画数量 + +```javascript +// 限制同时播放的动画数量 +const MAX_ANIMATIONS = 10; + +if (scene.getLayers().filter((l) => l.isAnimating()).length < MAX_ANIMATIONS) { + layer.animate(true); +} +``` + +### 3. 使用 requestAnimationFrame + +```javascript +function animate() { + // 更新状态 + updateData(); + + requestAnimationFrame(animate); +} + +animate(); +``` + +## 完整示例:出租车轨迹 + +```javascript +import { Scene, LineLayer, PointLayer } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 13, + }), +}); + +// 轨迹数据 +const trajectoryData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { taxi_id: 'T001', speed: 40 }, + geometry: { + type: 'LineString', + coordinates: [ + [120.19, 30.26], + [120.195, 30.265], + [120.2, 30.27], + [120.205, 30.275], + [120.21, 30.28], + ], + }, + }, + ], +}; + +scene.on('loaded', () => { + // 1. 历史路径(灰色) + const historyPath = new LineLayer().source(trajectoryData).shape('line').size(3).color('#ddd'); + + // 2. 运动轨迹(蓝色,带拖尾) + const movingTrail = new LineLayer() + .source(trajectoryData) + .shape('line') + .size(4) + .color('#5B8FF9') + .animate({ + enable: true, + interval: 0.3, + trailLength: 0.4, + duration: 6, + }); + + // 3. 运动的点(出租车) + const taxi = new PointLayer() + .source(trajectoryData) + .shape('circle') + .size(12) + .color('#FF6B3B') + .animate({ + enable: true, + interval: 0.3, + duration: 6, + }); + + scene.addLayer(historyPath); + scene.addLayer(movingTrail); + scene.addLayer(taxi); +}); +``` + +## 相关文档 + +- [line.md](../layers/line.md) - LineLayer 详细配置 +- [point.md](../layers/point.md) - PointLayer 详细配置 +- [events.md](../interaction/events.md) - 事件处理 diff --git a/skills/l7/references/core/map-types.md b/skills/l7/references/core/map-types.md new file mode 100644 index 0000000..2226048 --- /dev/null +++ b/skills/l7/references/core/map-types.md @@ -0,0 +1,580 @@ +--- +skill_id: map-types +skill_name: 地图引擎配置 +category: core +difficulty: beginner +tags: [map, gaode, mapbox, maplibre, map-engine] +dependencies: [] +version: 2.x +--- + +# 地图引擎配置 + +## 技能描述 + +L7 支持多种地图引擎作为底图,包括高德地图、Mapbox、MapLibre 以及独立的 Map 引擎。选择合适的地图引擎是创建 L7 可视化项目的第一步。 + +## 何时使用 + +- 🗺️ **高德地图(GaodeMap)**:国内项目,需要国内地图服务和 POI 数据 +- 🌍 **Mapbox**:国际项目,需要精美的国际地图样式 +- 🆓 **MapLibre**:开源项目,离线部署,自定义地图服务 +- 📐 **Map(独立引擎)**:室内地图、游戏地图、不需要地理底图的场景 + +## 前置条件 + +- 已安装 `@antv/l7` +- 已安装对应的地图库(如 `@antv/l7-maps`) + +## 地图引擎对比 + +| 特性 | GaodeMap | Mapbox | MapLibre | Map | +| ---------- | -------- | ---------- | ---------- | ---------- | +| 国内服务 | ✅ 优秀 | ❌ 需翻墙 | ✅ 可用 | ✅ 不依赖 | +| 国际服务 | ❌ 较弱 | ✅ 优秀 | ✅ 优秀 | ✅ 不依赖 | +| 需要 Token | ✅ 是 | ✅ 是 | ❌ 否 | ❌ 否 | +| 离线部署 | ❌ 否 | ❌ 否 | ✅ 是 | ✅ 是 | +| POI 数据 | ✅ 丰富 | ✅ 有 | ❌ 无 | ❌ 无 | +| 自定义样式 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 坐标系统 | 经纬度 | 经纬度 | 经纬度 | 平面坐标 | +| 适用场景 | 国内地理 | 国际地理 | 自建服务 | 非地理场景 | + +## 代码示例 + +### 1. 高德地图(GaodeMap)- 推荐国内使用 + +高德地图是国内最常用的地图服务,提供丰富的 POI 数据和路网信息。 + +#### 基础用法 + +```javascript +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', // 地图样式: dark | light | normal | satellite + center: [120.19, 30.26], // 中心点 [经度, 纬度] + pitch: 0, // 倾斜角度 0-60 + zoom: 10, // 缩放级别 0-22 + token: 'your-amap-key', // 高德地图 Key(可选,建议配置) + }), +}); + +scene.on('loaded', () => { + console.log('地图加载完成'); + // 添加图层 +}); +``` + +#### 申请高德地图 Token + +1. 访问 [高德开放平台](https://lbs.amap.com/) +2. 注册账号并创建应用 +3. 获取 Web 端(JS API)Key +4. 在代码中配置 `token` 参数 + +⚠️ **注意**:虽然 token 是可选的,但建议配置以避免服务限制。 + +#### 地图样式 + +高德地图支持多种内置样式: + +```javascript +// 暗色主题(适合数据可视化) +map: new GaodeMap({ style: 'dark' }); + +// 亮色主题 +map: new GaodeMap({ style: 'light' }); + +// 标准主题 +map: new GaodeMap({ style: 'normal' }); + +// 卫星影像 +map: new GaodeMap({ style: 'satellite' }); + +// 自定义样式(使用高德平台的自定义样式) +map: new GaodeMap({ + style: 'amap://styles/你的样式ID?isPublic=true', +}); +``` + +#### 3D 视角配置 + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.19, 30.26], + zoom: 14, + pitch: 45, // 倾斜角度,用于 3D 效果 + bearing: 30, // 旋转角度 + }), +}); +``` + +#### 传入已有高德地图实例 + +如果项目中已经创建了高德地图实例,可以直接传入: + +```javascript +// 先创建高德地图实例 +const map = new AMap.Map('map', { + viewMode: '3D', // 3D 模式 + resizeEnable: true, + zoom: 11, + center: [116.397428, 39.90923], +}); + +// 传入 L7 Scene +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + mapInstance: map, // 传入地图实例 + }), +}); +``` + +⚠️ **注意**: + +- Scene 的 id 参数需要与地图容器一致 +- 需要自行引入高德地图 API +- 建议设置 `viewMode: '3D'`(高德 2.0 支持 2D 模式) + +#### 使用高德地图插件 + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [116.475, 39.99], + zoom: 13, + plugin: ['AMap.ToolBar', 'AMap.LineSearch'], // 注册插件 + }), +}); + +scene.on('loaded', () => { + // 使用插件 + window.AMap.plugin(['AMap.ToolBar', 'AMap.LineSearch'], () => { + // 添加工具条 + scene.map.addControl(new AMap.ToolBar()); + + // 使用公交线路搜索 + const linesearch = new AMap.LineSearch({ + pageIndex: 1, + city: '北京', + extensions: 'all', + }); + }); +}); +``` + +### 2. 独立地图引擎(Map)- 推荐无底图场景 + +Map 是 L7 内置的独立地图引擎,完全不依赖第三方地图服务,适合室内地图、游戏地图等场景。 + +#### 基础用法 + +```javascript +import { Scene } from '@antv/l7'; +import { Map } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [0, 0], // 使用平面坐标,不是经纬度 + zoom: 3, + style: 'blank', // 空白背景 + minZoom: 0, + maxZoom: 18, + }), +}); + +scene.on('loaded', () => { + // Map 使用平面坐标系统 + const data = [ + { x: 100, y: 100, value: 10 }, + { x: 200, y: 200, value: 20 }, + { x: 300, y: 150, value: 15 }, + ]; + + const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'x', // 平面 X 坐标 + y: 'y', // 平面 Y 坐标 + }, + }) + .shape('circle') + .size(10) + .color('value', ['#ffffcc', '#800026']); + + scene.addLayer(pointLayer); +}); +``` + +#### Map 的特点 + +✅ **优势**: + +- 无需第三方地图服务(高德、Mapbox)Key +- 完全离线可用 +- 使用平面坐标系统,不受经纬度限制 +- 适合室内地图、游戏地图、抽象数据可视化 +- 可以自由添加瓦片底图 + +❌ **限制**: + +- 没有内置地理数据和 POI +- 需要自己提供底图(或使用空白背景) + +#### 添加自定义瓦片底图 + +```javascript +import { Scene, RasterLayer } from '@antv/l7'; +import { Map } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [120.19, 30.26], + zoom: 10, + }), +}); + +scene.on('loaded', () => { + // 添加高德地图瓦片作为底图 + const layer = new RasterLayer(); + layer.source( + 'https://webrd0{1-3}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', + { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 2, + maxZoom: 18, + }, + }, + ); + scene.addLayer(layer); +}); +``` + +#### 室内地图示例 + +```javascript +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [0, 0], + zoom: 4, + style: 'blank', + }), +}); + +scene.on('loaded', () => { + // 加载室内地图数据(平面坐标) + fetch('/indoor-map.json') + .then((res) => res.json()) + .then((data) => { + const polygonLayer = new PolygonLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', // 平面坐标数组 + }, + }) + .shape('fill') + .color('type', { + 会议室: '#4575b4', + 办公区: '#74add1', + 休息区: '#fee090', + }) + .style({ + opacity: 0.8, + }); + + scene.addLayer(polygonLayer); + }); +}); +``` + +### 3. Mapbox(国际项目) + +```javascript +import { Scene } from '@antv/l7'; +import { Mapbox } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Mapbox({ + style: 'mapbox://styles/mapbox/streets-v11', // Mapbox 样式 + center: [120.19, 30.26], + zoom: 10, + token: 'your-mapbox-token', // Mapbox Token(必需) + }), +}); +``` + +#### 申请 Mapbox Token + +1. 访问 [Mapbox 官网](https://www.mapbox.com/) +2. 注册账号 +3. 在 Account 页面获取 Access Token +4. 配置 `token` 参数 + +### 4. MapLibre(开源方案) + +```javascript +import { Scene } from '@antv/l7'; +import { Maplibre } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Maplibre({ + style: 'https://your-server.com/style.json', // 自定义样式 JSON + center: [120.19, 30.26], + zoom: 10, + }), +}); +``` + +MapLibre 完全开源,不需要 Token,适合自建地图服务。 + +## 地图配置参数 + +所有地图引擎支持的通用配置参数: + +| 参数 | 类型 | 默认值 | 说明 | +| --------- | ---------------- | ------- | --------------------------------- | +| `center` | [number, number] | [0, 0] | 地图中心点 [经度, 纬度] 或 [x, y] | +| `zoom` | number | 0 | 缩放级别 (0-22) | +| `pitch` | number | 0 | 倾斜角度 (0-60) | +| `bearing` | number | 0 | 旋转角度 (0-360) | +| `minZoom` | number | 0 | 最小缩放级别 | +| `maxZoom` | number | 22 | 最大缩放级别 | +| `style` | string | 'light' | 地图样式 | + +### 高德地图特有参数 + +| 参数 | 类型 | 说明 | +| ------------- | -------- | ------------------------ | +| `token` | string | 高德地图 Key(推荐配置) | +| `plugin` | string[] | 要加载的高德插件数组 | +| `mapInstance` | AMap.Map | 已有的高德地图实例 | + +### Mapbox 特有参数 + +| 参数 | 类型 | 说明 | +| ------- | ------ | --------------------------- | +| `token` | string | Mapbox Access Token(必需) | + +## 选择建议 + +### 国内项目推荐:GaodeMap + +```javascript +✅ 优势: +- 国内服务稳定快速 +- 丰富的 POI 和路网数据 +- 支持多种地图样式 +- 完善的中文文档 + +❌ 限制: +- 需要申请 Token +- 国际数据相对较弱 +``` + +### 无底图场景推荐:Map + +```javascript +✅ 优势: +- 完全离线可用 +- 不需要任何 Token +- 使用灵活的平面坐标 +- 可自定义底图 + +❌ 限制: +- 没有内置地理数据 +- 需要自己处理坐标系统 +``` + +### 国际项目推荐:Mapbox 或 MapLibre + +```javascript +✅ Mapbox 优势: +- 精美的地图样式 +- 全球数据完善 +- 强大的自定义能力 + +✅ MapLibre 优势: +- 完全开源免费 +- 不需要 Token +- 支持离线部署 +``` + +## 实际应用场景 + +### 1. 国内城市可视化(使用 GaodeMap) + +```javascript +import { Scene, PointLayer } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [116.404, 39.915], // 北京 + zoom: 10, + token: 'your-amap-key', + }), +}); + +scene.on('loaded', () => { + // 显示 POI 数据 + const poiLayer = new PointLayer() + .source(poiData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('circle') + .size(8) + .color('category', ['#FF6B6B', '#4ECDC4', '#95E1D3']); + + scene.addLayer(poiLayer); +}); +``` + +### 2. 室内导航(使用 Map) + +```javascript +import { Scene, LineLayer } from '@antv/l7'; +import { Map } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [0, 0], + zoom: 5, + style: 'blank', + }), +}); + +scene.on('loaded', () => { + // 加载室内地图 + fetch('/indoor-layout.json') + .then((res) => res.json()) + .then((data) => { + // 使用平面坐标绘制室内布局 + const layoutLayer = new PolygonLayer() + .source(data) + .shape('fill') + .color('roomType', colorMap) + .style({ opacity: 0.6 }); + + scene.addLayer(layoutLayer); + }); +}); +``` + +### 3. 游戏地图(使用 Map) + +```javascript +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [500, 500], // 游戏世界中心 + zoom: 4, + style: 'blank', + minZoom: 2, + maxZoom: 8, + }), +}); + +scene.on('loaded', () => { + // 绘制游戏元素(使用平面坐标) + const playerLayer = new PointLayer() + .source(players, { + parser: { + type: 'json', + x: 'posX', + y: 'posY', + }, + }) + .shape('player-icon') + .size(20); + + scene.addLayer(playerLayer); +}); +``` + +## 常见问题 + +### Q: 如何选择地图引擎? + +A: + +- **国内地理项目** → GaodeMap +- **室内地图/游戏** → Map +- **国际项目** → Mapbox 或 MapLibre +- **自建服务/离线** → Map 或 MapLibre + +### Q: 高德地图不配置 Token 会怎样? + +A: 可以正常使用,但可能有请求限制。建议申请 Token 以获得更稳定的服务。 + +### Q: Map 引擎可以显示地理地图吗? + +A: 可以,通过 RasterLayer 加载地图瓦片服务作为底图即可。 + +### Q: 坐标系统有什么区别? + +A: + +- **GaodeMap/Mapbox/MapLibre**:使用经纬度坐标(WGS84) + - 经度范围:-180 ~ 180 + - 纬度范围:-90 ~ 90 +- **Map**:使用平面坐标 + - 任意数值范围 + - 适合非地理场景 + +### Q: 切换地图引擎会影响图层代码吗? + +A: 基本不会。L7 统一了不同底图的接口,图层代码基本相同。唯一区别是坐标系统(经纬度 vs 平面坐标)。 + +### Q: 可以在运行时切换地图样式吗? + +A: 可以,使用 `scene.setMapStyle(style)` 方法: + +```javascript +// 切换为暗色主题 +scene.setMapStyle('dark'); + +// 切换为自定义样式 +scene.setMapStyle('amap://styles/your-style-id'); +``` + +## 注意事项 + +⚠️ **Token 安全**:不要在公开的代码仓库中暴露 Token,建议使用环境变量 + +⚠️ **坐标系统**:确保数据坐标系统与地图引擎匹配 + +⚠️ **网络依赖**:GaodeMap、Mapbox 需要网络连接;Map 可完全离线 + +⚠️ **性能考虑**:Map 引擎在大数据量时性能更好,因为没有底图渲染开销 + +## 相关技能 + +- [场景初始化](./scene.md) +- [场景生命周期](./scene-lifecycle.md) +- [场景方法](./scene-methods.md) +- [点图层](../layers/point.md) + +## 在线示例 + +- [高德地图示例](https://l7.antv.antgroup.com/examples/tutorial/map#amap) +- [Map 引擎示例](https://l7.antv.antgroup.com/examples/tutorial/map#map) diff --git a/skills/l7/references/core/scene-lifecycle.md b/skills/l7/references/core/scene-lifecycle.md new file mode 100644 index 0000000..618fb06 --- /dev/null +++ b/skills/l7/references/core/scene-lifecycle.md @@ -0,0 +1,476 @@ +--- +skill_id: scene-lifecycle +skill_name: 场景生命周期管理 +category: core +difficulty: intermediate +tags: [scene, lifecycle, events, destroy] +dependencies: [scene-initialization] +version: 2.x +--- + +# 场景生命周期管理 + +## 技能描述 + +管理 L7 场景的完整生命周期,包括场景加载、运行时事件监听、以及场景销毁。理解和正确使用生命周期事件对于构建稳定的地图应用至关重要。 + +## 何时使用 + +- ✅ 需要在场景加载完成后执行初始化操作 +- ✅ 监听地图的缩放、平移等交互事件 +- ✅ 监听容器大小变化,实现响应式布局 +- ✅ 页面卸载或组件销毁时清理资源 +- ✅ 调试时需要开启/关闭实时渲染 + +## 前置条件 + +- 已完成[场景初始化](./scene.md) + +## 核心概念 + +### 生命周期阶段 + +L7 场景的生命周期分为三个主要阶段: + +1. **初始化阶段**:创建 Scene 实例 +2. **运行阶段**:场景加载完成,可以添加图层、监听事件 +3. **销毁阶段**:场景被销毁,释放资源 + +``` +创建 Scene → loaded 事件 → 添加图层/监听事件 → 销毁 Scene +``` + +## 代码示例 + +### 场景加载事件 + +#### loaded 事件 - 场景初始化完成 + +最重要的生命周期事件,所有图层和组件都应该在此事件后添加。 + +```javascript +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + }), +}); + +// 场景加载完成后执行 +scene.on('loaded', () => { + console.log('场景已加载完成,可以添加图层'); + + // 在这里添加图层 + const pointLayer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(pointLayer); +}); +``` + +#### resize 事件 - 容器大小变化 + +当地图容器大小改变时触发,适用于响应式布局。 + +```javascript +scene.on('resize', () => { + console.log('地图容器大小已改变'); + // 可以在这里重新布局或调整图层 +}); +``` + +**常见场景**: + +```javascript +// 监听窗口大小变化,自动调整地图 +window.addEventListener('resize', () => { + // Scene 会自动触发 resize 事件 +}); +``` + +### 地图交互事件 + +#### 缩放事件 + +```javascript +// 缩放级别更改后触发 +scene.on('zoomchange', (e) => { + console.log('当前缩放级别:', scene.getZoom()); +}); + +// 缩放开始时触发 +scene.on('zoomstart', () => { + console.log('开始缩放'); +}); + +// 缩放停止时触发 +scene.on('zoomend', () => { + console.log('缩放结束'); +}); +``` + +#### 地图移动事件 + +```javascript +// 地图平移时触发 +scene.on('mapmove', () => { + console.log('地图正在移动'); +}); + +// 地图平移开始时触发 +scene.on('movestart', () => { + console.log('开始移动'); +}); + +// 地图移动结束后触发(包括平移和缩放导致的中心点变化) +scene.on('moveend', () => { + const center = scene.getCenter(); + console.log('移动结束,当前中心点:', center); +}); +``` + +#### 拖拽事件 + +```javascript +// 开始拖拽地图时触发 +scene.on('dragstart', (e) => { + console.log('开始拖拽'); +}); + +// 拖拽地图过程中触发 +scene.on('dragging', (e) => { + console.log('正在拖拽'); +}); + +// 停止拖拽地图时触发 +scene.on('dragend', (e) => { + console.log('拖拽结束'); +}); +``` + +### 鼠标事件 + +```javascript +// 鼠标点击 +scene.on('click', (e) => { + console.log('点击位置:', e.lngLat); +}); + +// 鼠标双击 +scene.on('dblclick', (e) => { + console.log('双击位置:', e.lngLat); +}); + +// 鼠标移动 +scene.on('mousemove', (e) => { + // 高频事件,注意性能 +}); + +// 鼠标右键 +scene.on('contextmenu', (e) => { + e.preventDefault(); // 阻止默认右键菜单 + console.log('右键点击位置:', e.lngLat); +}); + +// 鼠标进入/离开地图容器 +scene.on('mouseover', () => { + console.log('鼠标进入地图'); +}); + +scene.on('mouseout', () => { + console.log('鼠标离开地图'); +}); + +// 鼠标按下/抬起 +scene.on('mousedown', (e) => { + console.log('鼠标按下'); +}); + +scene.on('mouseup', (e) => { + console.log('鼠标抬起'); +}); + +// 鼠标滚轮 +scene.on('mousewheel', (e) => { + console.log('鼠标滚轮缩放'); +}); +``` + +### 移除事件监听 + +```javascript +// 定义事件处理函数 +const handleClick = (e) => { + console.log('点击位置:', e.lngLat); +}; + +// 绑定事件 +scene.on('click', handleClick); + +// 移除事件监听 +scene.off('click', handleClick); +``` + +### 场景销毁 + +#### destroy 方法 + +离开页面或不再需要地图时,必须调用 destroy 方法释放资源。 + +```javascript +// 销毁场景 +scene.destroy(); +``` + +**React 示例**: + +```javascript +import { useEffect } from 'react'; +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +function MapComponent() { + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + }), + }); + + scene.on('loaded', () => { + // 添加图层 + }); + + // 组件卸载时销毁场景 + return () => { + scene.destroy(); + }; + }, []); + + return
; +} +``` + +**Vue 示例**: + +```javascript +import { onMounted, onBeforeUnmount } from 'vue'; +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +export default { + setup() { + let scene = null; + + onMounted(() => { + scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + }), + }); + + scene.on('loaded', () => { + // 添加图层 + }); + }); + + onBeforeUnmount(() => { + if (scene) { + scene.destroy(); + } + }); + + return {}; + }, +}; +``` + +#### destroy 事件 + +场景销毁时触发的事件。 + +```javascript +scene.on('destroy', () => { + console.log('场景已销毁'); + // 可以在这里执行额外的清理工作 +}); + +scene.destroy(); +``` + +### 调试相关事件 + +#### 开启/关闭实时渲染 + +L7 默认按需重绘以节省资源。调试时可以开启实时渲染,便于使用 SpectorJS 等工具捕捉帧渲染。 + +```javascript +// 开启实时渲染(用于调试) +scene.startAnimate(); + +// 停止实时渲染 +scene.stopAnimate(); +``` + +#### WebGL 上下文丢失 + +```javascript +scene.on('webglcontextlost', () => { + console.error('WebGL 上下文丢失'); + // 可以提示用户刷新页面 +}); +``` + +## 实际应用场景 + +### 1. 响应式地图 + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + }), +}); + +scene.on('loaded', () => { + addLayers(); +}); + +// 容器大小变化时重新布局 +scene.on('resize', () => { + // 根据新尺寸调整图层或控件 + updateLayout(); +}); +``` + +### 2. 缩放级别联动 + +```javascript +scene.on('loaded', () => { + const layer = new PointLayer().source(data).shape('circle').size(10).color('#5B8FF9'); + + scene.addLayer(layer); +}); + +// 根据缩放级别调整图层显示 +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + + if (zoom < 10) { + // 小级别显示聚合数据 + layer.hide(); + clusterLayer.show(); + } else { + // 大级别显示详细数据 + layer.show(); + clusterLayer.hide(); + } +}); +``` + +### 3. 地图移动加载数据 + +```javascript +scene.on('moveend', () => { + const bounds = scene.getBounds(); + + // 根据可视范围加载数据 + fetchDataInBounds(bounds).then((data) => { + layer.setData(data); + }); +}); +``` + +### 4. 交互提示 + +```javascript +scene.on('loaded', () => { + const layer = new PointLayer().source(data).shape('circle').size(10).color('#5B8FF9'); + + scene.addLayer(layer); + + // 图层点击事件 + layer.on('click', (e) => { + console.log('点击了点:', e.feature.properties); + }); +}); + +// 全局点击事件 +scene.on('click', (e) => { + console.log('点击了地图:', e.lngLat); +}); +``` + +## 常见问题 + +### Q: 为什么必须在 loaded 事件后添加图层? + +A: 场景需要完成初始化(包括 WebGL 上下文、地图底图等)才能正确渲染图层。在 loaded 之前添加图层可能导致渲染失败。 + +### Q: 如何避免内存泄漏? + +A: 确保在页面/组件卸载时调用 `scene.destroy()`,并移除所有事件监听器。 + +```javascript +// ❌ 错误:忘记销毁 +useEffect(() => { + const scene = new Scene({...}); + // 没有返回清理函数 +}, []); + +// ✅ 正确:销毁场景 +useEffect(() => { + const scene = new Scene({...}); + return () => scene.destroy(); +}, []); +``` + +### Q: 事件监听器会自动移除吗? + +A: 调用 `scene.destroy()` 时会自动移除所有事件监听器。但如果需要临时移除某个监听器,应该手动调用 `scene.off()`。 + +### Q: resize 事件什么时候触发? + +A: 当地图容器的 DOM 元素大小改变时触发。通常由以下情况引起: + +- 窗口大小改变 +- 父容器大小改变 +- CSS 样式动态修改 + +### Q: 场景销毁后还能重新使用吗? + +A: 不能。销毁后的 Scene 实例无法恢复,需要重新创建新的 Scene 实例。 + +## 注意事项 + +⚠️ **性能优化**:避免在高频事件(mousemove、mapmove)中执行复杂计算 + +⚠️ **内存管理**:确保销毁场景时同时销毁所有图层和组件 + +⚠️ **事件顺序**:某些事件有先后顺序,如 zoomstart → zoomchange → zoomend + +⚠️ **异步操作**:在 loaded 事件中进行异步数据加载时注意错误处理 + +## 相关技能 + +- [场景初始化](./scene.md) +- [场景方法](./scene-methods.md) +- [事件处理](../interaction/events.md) +- [点图层](../layers/point.md) + +## 在线示例 + +查看更多示例:[L7 官方示例](https://l7.antv.antgroup.com/examples) diff --git a/skills/l7/references/core/scene-methods.md b/skills/l7/references/core/scene-methods.md new file mode 100644 index 0000000..e31afad --- /dev/null +++ b/skills/l7/references/core/scene-methods.md @@ -0,0 +1,827 @@ +--- +skill_id: scene-methods +skill_name: 场景方法 +category: core +difficulty: intermediate +tags: [scene, methods, api, map-control] +dependencies: [scene-initialization] +version: 2.x +--- + +# 场景方法 + +## 技能描述 + +掌握 L7 Scene 提供的各种方法,包括图层管理、控件管理、地图操作、坐标转换、资源管理等核心功能。 + +## 何时使用 + +- ✅ 动态添加/移除图层 +- ✅ 添加地图控件(缩放、比例尺等) +- ✅ 控制地图视角(中心点、缩放、旋转) +- ✅ 坐标系统转换 +- ✅ 管理全局图片资源 +- ✅ 添加 Popup 和 Marker +- ✅ 导出地图图片 + +## 前置条件 + +- 已完成[场景初始化](./scene.md) + +## 图层管理方法 + +### addLayer(layer): void + +将图层添加到场景中。 + +```javascript +import { Scene, PointLayer } from '@antv/l7'; + +const scene = new Scene({...}); + +scene.on('loaded', () => { + const pointLayer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(pointLayer); +}); +``` + +### getLayers(): ILayer[] + +获取所有图层。 + +```javascript +const layers = scene.getLayers(); +console.log('图层数量:', layers.length); + +layers.forEach((layer) => { + console.log('图层ID:', layer.id); +}); +``` + +### getLayer(id: string): ILayer + +根据图层 ID 获取图层。 + +```javascript +const layer = scene.getLayer('layer-id'); +if (layer) { + layer.show(); +} +``` + +### getLayerByName(name: string): ILayer + +根据图层名称获取图层。 + +```javascript +const layer = new PointLayer({ name: 'myPointLayer' }) + .source(data) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); + +// 通过名称获取 +const foundLayer = scene.getLayerByName('myPointLayer'); +``` + +### removeLayer(layer: ILayer): void + +移除并销毁图层。 + +```javascript +const layer = scene.getLayer('layer-id'); +scene.removeLayer(layer); +// 图层已被销毁,不能再使用 +``` + +⚠️ **注意**:移除图层的同时会自动销毁图层,释放资源。 + +### removeAllLayer(): void + +移除并销毁所有图层。 + +```javascript +scene.removeAllLayer(); +// 所有图层已被移除和销毁 +``` + +## 控件管理方法 + +### addControl(control: IControl): void + +添加控件到场景。 + +```javascript +import { Scene, Zoom, Scale } from '@antv/l7'; + +const scene = new Scene({...}); + +// 添加缩放控件 +const zoomControl = new Zoom({ + position: 'topright' +}); +scene.addControl(zoomControl); + +// 添加比例尺控件 +const scaleControl = new Scale({ + position: 'bottomleft' +}); +scene.addControl(scaleControl); +``` + +### removeControl(control: IControl): void + +移除控件。 + +```javascript +const zoomControl = new Zoom({ position: 'topright' }); +scene.addControl(zoomControl); + +// 移除控件 +scene.removeControl(zoomControl); +``` + +### getControlByName(name: string): IControl + +根据控件名称获取控件。 + +```javascript +const zoomControl = new Zoom({ + name: 'myZoom', + position: 'topright', +}); +scene.addControl(zoomControl); + +// 通过名称获取 +const control = scene.getControlByName('myZoom'); +``` + +## Popup 管理方法 + +### addPopup(popup: Popup): void + +添加 Popup 弹窗。 + +```javascript +import { Popup } from '@antv/l7'; + +const popup = new Popup({ + offsets: [0, 20], + closeButton: true, +}) + .setLnglat([120.19, 30.26]) + .setHTML('
这是一个 Popup
'); + +scene.addPopup(popup); +``` + +### removePopup(popup: Popup): void + +移除 Popup 弹窗。 + +```javascript +scene.removePopup(popup); +``` + +## Marker 管理方法 + +### addMarker(marker: IMarker): void + +添加 Marker 标记。 + +```javascript +import { Marker } from '@antv/l7'; + +const el = document.createElement('div'); +el.className = 'marker-custom'; +el.innerHTML = '📍'; + +const marker = new Marker({ element: el }).setLnglat([120.19, 30.26]); + +scene.addMarker(marker); +``` + +### addMarkerLayer(layer: IMarkerLayer): void + +添加 MarkerLayer 统一管理多个 Marker。 + +```javascript +import { MarkerLayer } from '@antv/l7'; + +const markerLayer = new MarkerLayer(); + +data.forEach((item) => { + const el = document.createElement('div'); + el.textContent = item.name; + + const marker = new Marker({ element: el }).setLnglat([item.lng, item.lat]); + + markerLayer.addMarker(marker); +}); + +scene.addMarkerLayer(markerLayer); +``` + +### removeMarkerLayer(layer: IMarkerLayer): void + +移除 MarkerLayer。 + +```javascript +scene.removeMarkerLayer(markerLayer); +``` + +### removeAllMarkers(): void + +移除所有 Marker。 + +```javascript +scene.removeAllMarkers(); +``` + +## 地图视角控制方法 + +### getZoom(): number + +获取当前缩放级别。 + +```javascript +const zoom = scene.getZoom(); +console.log('当前缩放级别:', zoom); +``` + +### setZoom(zoom: number): void + +设置缩放级别(0-22)。 + +```javascript +scene.setZoom(12); +``` + +### zoomIn(): void + +地图放大一级。 + +```javascript +scene.zoomIn(); +``` + +### zoomOut(): void + +地图缩小一级。 + +```javascript +scene.zoomOut(); +``` + +### getCenter(): ILngLat + +获取地图中心点。 + +```javascript +const center = scene.getCenter(); +console.log('中心点:', center); // { lng: 120.19, lat: 30.26 } +``` + +### setCenter(center: [number, number], options?: ICameraOptions): void + +设置地图中心点。 + +```javascript +// 基础用法 +scene.setCenter([120.19, 30.26]); + +// 带偏移的中心点 +scene.setCenter([120.19, 30.26], { + padding: { + top: 100, + bottom: 50, + left: 100, + right: 100, + }, +}); + +// 数组形式的 padding +scene.setCenter([120.19, 30.26], { + padding: [100, 50, 100, 100], // top, right, bottom, left +}); + +// 单个数值(四边相同) +scene.setCenter([120.19, 30.26], { + padding: 50, +}); +``` + +### setZoomAndCenter(zoom: number, center: [number, number]): void + +同时设置缩放级别和中心点。 + +```javascript +scene.setZoomAndCenter(12, [120.19, 30.26]); +``` + +### getPitch(): number + +获取地图倾斜角度(0-60)。 + +```javascript +const pitch = scene.getPitch(); +console.log('倾斜角度:', pitch); +``` + +### setPitch(pitch: number): void + +设置地图倾斜角度(用于 3D 效果)。 + +```javascript +scene.setPitch(45); // 设置为 45 度倾斜 +``` + +### setRotation(rotation: number): void + +设置地图顺时针旋转角度(0-360)。 + +```javascript +scene.setRotation(90); // 旋转 90 度 +``` + +### panTo(lnglat: [number, number]): void + +地图平移到指定经纬度。 + +```javascript +scene.panTo([120.19, 30.26]); +``` + +### panBy(x: number, y: number): void + +以像素为单位平移地图。 + +```javascript +// 向右平移 100px,向下平移 50px +scene.panBy(100, 50); + +// 向左平移 100px,向上平移 50px +scene.panBy(-100, -50); +``` + +### fitBounds(bounds: [[number, number], [number, number]], options?: IOptions): void + +地图缩放到指定范围。 + +```javascript +// 基础用法 +scene.fitBounds([ + [112, 32], // 西南角 [minLng, minLat] + [114, 35], // 东北角 [maxLng, maxLat] +]); + +// 带动画 +scene.fitBounds( + [ + [112, 32], + [114, 35], + ], + { animate: true }, +); +``` + +## 地图样式和状态方法 + +### setMapStyle(style: string): void + +设置地图样式。 + +```javascript +// L7 内置样式 +scene.setMapStyle('dark'); // 暗色 +scene.setMapStyle('light'); // 亮色 +scene.setMapStyle('normal'); // 正常 + +// Mapbox 样式 +scene.setMapStyle('mapbox://styles/mapbox/streets-v11'); + +// 高德样式 +scene.setMapStyle('amap://styles/2a09079c3daac9420ee53b67307a8006?isPublic=true'); +``` + +### setMapStatus(options: IStatusOptions): void + +设置地图交互状态。 + +```javascript +scene.setMapStatus({ + dragEnable: true, // 是否允许拖拽 + keyboardEnable: true, // 是否允许键盘操作 + doubleClickZoom: true, // 是否允许双击缩放 + zoomEnable: true, // 是否允许缩放 + rotateEnable: true, // 是否允许旋转 + showIndoorMap: false, // 是否显示室内地图 + resizeEnable: true, // 是否自动调整大小 +}); + +// 禁用所有交互 +scene.setMapStatus({ + dragEnable: false, + zoomEnable: false, + rotateEnable: false, +}); +``` + +## 容器和尺寸方法 + +### getContainer(): HTMLElement | null + +获取地图容器 DOM 元素。 + +```javascript +const container = scene.getContainer(); +console.log('容器宽度:', container.offsetWidth); +console.log('容器高度:', container.offsetHeight); +``` + +### getSize(): [number, number] + +获取地图容器宽高。 + +```javascript +const [width, height] = scene.getSize(); +console.log(`容器尺寸: ${width} x ${height}`); +``` + +## 坐标转换方法 + +### lngLatToContainer(lnglat: [number, number]): IPoint + +经纬度转换为容器像素坐标。 + +```javascript +const point = scene.lngLatToContainer([120.19, 30.26]); +console.log('容器坐标:', point); // { x: 256, y: 128 } +``` + +### containerToLngLat(point: [number, number]): ILngLat + +容器像素坐标转换为经纬度。 + +```javascript +const lnglat = scene.containerToLngLat([256, 128]); +console.log('经纬度:', lnglat); // { lng: 120.19, lat: 30.26 } +``` + +### lngLatToPixel(lnglat: [number, number]): IPoint + +经纬度转换为屏幕像素坐标。 + +```javascript +const pixel = scene.lngLatToPixel([120.19, 30.26]); +console.log('像素坐标:', pixel); // { x: 512, y: 384 } +``` + +### pixelToLngLat(pixel: [number, number]): ILngLat + +屏幕像素坐标转换为经纬度。 + +```javascript +const lnglat = scene.pixelToLngLat([512, 384]); +console.log('经纬度:', lnglat); // { lng: 120.19, lat: 30.26 } +``` + +**坐标系统说明**: + +- **容器坐标**:相对于地图容器左上角的坐标,原点 (0, 0) 在容器左上角 +- **像素坐标**:地图的绝对像素坐标,考虑了地图的缩放和平移 + +## 全局资源管理方法 + +### addImage(id: string, img: HTMLImageElement | string | File): void + +添加全局图片资源,供图层使用。 + +```javascript +// 添加网络图片 +scene.addImage('marker-icon', 'https://example.com/marker.png'); + +// 添加本地图片元素 +const img = document.getElementById('my-image'); +scene.addImage('custom-icon', img); + +// 在图层中使用 +const layer = new PointLayer().source(data).shape('marker-icon').size(20); +``` + +### hasImage(id: string): boolean + +判断是否已添加某个图片资源。 + +```javascript +if (!scene.hasImage('marker-icon')) { + scene.addImage('marker-icon', 'https://example.com/marker.png'); +} +``` + +### removeImage(id: string): void + +删除全局图片资源。 + +```javascript +scene.removeImage('marker-icon'); +``` + +### addFontFace(fontFamily: string, fontPath: string): void + +添加字体文件(用于 iconfont)。 + +```javascript +const fontFamily = 'iconfont'; +const fontPath = '//at.alicdn.com/t/font_2534097_iiet9d3nekn.woff2?t=1620444089776'; + +scene.addFontFace(fontFamily, fontPath); + +// 在图层中使用 +const layer = new PointLayer().source(data).shape('icon', 'text').style({ + fontFamily: 'iconfont', + iconfont: true, +}); +``` + +### addIconFont(name: string, unicode: string): void + +添加 iconfont 映射。 + +```javascript +scene.addIconFont('home', ''); +scene.addIconFont('location', ''); + +// 在数据中使用名称 +const data = [ + { lng: 120, lat: 30, icon: 'home' }, + { lng: 121, lat: 31, icon: 'location' }, +]; + +const layer = new PointLayer().source(data).shape('icon', 'text').style({ + fontFamily: 'iconfont', + iconfont: true, +}); +``` + +### addIconFonts(options: Array<[string, string]>): void + +批量添加 iconfont 映射。 + +```javascript +scene.addIconFonts([ + ['home', ''], + ['location', ''], + ['star', ''], +]); +``` + +## 静态方法 + +### Scene.addProtocol(protocol: string, handler: Function) + +添加自定义数据协议(用于加载特殊格式瓦片)。 + +```javascript +// 自定义协议 +Scene.addProtocol('custom', (params, callback) => { + fetch(`https://${params.url.split('://')[1]}`) + .then((response) => { + if (response.status === 200) { + response.arrayBuffer().then((buffer) => { + callback(null, buffer, null, null); + }); + } else { + callback(new Error(`加载失败: ${response.statusText}`)); + } + }) + .catch((error) => { + callback(new Error(error)); + }); + + return { cancel: () => {} }; +}); + +// 使用自定义协议 +const source = new Source('custom://your-tile-url/{z}/{x}/{y}', { + parser: { + type: 'mvt', + tileSize: 256, + }, +}); +``` + +**PMTiles 示例**: + +```javascript +import * as pmtiles from 'pmtiles'; + +const protocol = new pmtiles.Protocol(); +Scene.addProtocol('pmtiles', protocol.tile); + +const source = new Source('pmtiles://https://example.com/tiles.pmtiles', { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 14, + }, +}); +``` + +### Scene.removeProtocol(protocol: string) + +删除自定义协议。 + +```javascript +Scene.removeProtocol('custom'); +``` + +## 导出和调试方法 + +### exportMap(type?: 'png' | 'jpg'): string + +导出地图为图片(仅导出可视化层,不包含底图)。 + +```javascript +// 导出为 PNG +const pngDataURL = scene.exportMap('png'); + +// 导出为 JPG +const jpgDataURL = scene.exportMap('jpg'); + +// 下载图片 +const link = document.createElement('a'); +link.download = 'map.png'; +link.href = pngDataURL; +link.click(); +``` + +### getPointSizeRange(): Float32Array + +获取当前设备支持的 WebGL 点精灵大小范围。 + +```javascript +const [minSize, maxSize] = scene.getPointSizeRange(); +console.log(`点大小范围: ${minSize} - ${maxSize}`); +``` + +### startAnimate(): void + +开启实时渲染(用于调试)。 + +```javascript +scene.startAnimate(); +// 便于使用 SpectorJS 等工具捕捉帧渲染 +``` + +### stopAnimate(): void + +停止实时渲染。 + +```javascript +scene.stopAnimate(); +``` + +## 实际应用场景 + +### 1. 动态切换图层 + +```javascript +const layers = { + point: new PointLayer()..., + line: new LineLayer()..., + polygon: new PolygonLayer()... +}; + +function showLayer(type) { + // 移除所有图层 + scene.removeAllLayer(); + + // 添加指定图层 + scene.addLayer(layers[type]); +} + +// 切换图层 +showLayer('point'); +``` + +### 2. 地图导航 + +```javascript +function flyTo(city) { + const cities = { + beijing: [116.404, 39.915], + shanghai: [121.473, 31.23], + guangzhou: [113.264, 23.129], + }; + + scene.setCenter(cities[city]); + scene.setZoom(12); +} + +// 飞到北京 +flyTo('beijing'); +``` + +### 3. 数据范围适配 + +```javascript +function fitData(data) { + const lngs = data.map((d) => d.lng); + const lats = data.map((d) => d.lat); + + const bounds = [ + [Math.min(...lngs), Math.min(...lats)], + [Math.max(...lngs), Math.max(...lats)], + ]; + + scene.fitBounds(bounds); +} +``` + +### 4. 响应式控件 + +```javascript +function updateControls() { + const [width] = scene.getSize(); + + // 移动端隐藏部分控件 + if (width < 768) { + scene.removeControl(zoomControl); + } else { + scene.addControl(zoomControl); + } +} + +scene.on('resize', updateControls); +``` + +## 常见问题 + +### Q: 图层添加后看不到? + +A: 检查: + +1. 是否在 `loaded` 事件后添加 +2. 图层数据是否正确 +3. 图层样式是否配置 +4. 地图中心和缩放级别是否合适 + +### Q: 坐标转换结果不准确? + +A: 确保在地图加载完成后进行坐标转换,并且使用正确的坐标系统。 + +### Q: 如何禁用地图交互? + +A: 使用 `setMapStatus` 方法: + +```javascript +scene.setMapStatus({ + dragEnable: false, + zoomEnable: false, + rotateEnable: false, +}); +``` + +### Q: 如何监听地图属性变化? + +A: 使用生命周期事件: + +```javascript +scene.on('zoomchange', () => { + console.log('缩放级别:', scene.getZoom()); +}); + +scene.on('moveend', () => { + console.log('中心点:', scene.getCenter()); +}); +``` + +## 注意事项 + +⚠️ **内存管理**:使用 `removeLayer` 会自动销毁图层,无需手动调用 `layer.destroy()` + +⚠️ **坐标系统**:区分容器坐标和像素坐标的使用场景 + +⚠️ **资源管理**:及时移除不需要的全局资源(图片、字体) + +⚠️ **导出限制**:`exportMap` 只能导出 L7 图层,不包含底图 + +## 相关技能 + +- [场景初始化](./scene.md) +- [场景生命周期](./scene-lifecycle.md) +- [图层管理](../layers/point.md) +- [交互组件](../interaction/components.md) +- [事件处理](../interaction/events.md) + +## 在线示例 + +查看更多示例:[L7 官方示例](https://l7.antv.antgroup.com/examples) diff --git a/skills/l7/references/core/scene.md b/skills/l7/references/core/scene.md new file mode 100644 index 0000000..dcfa7e4 --- /dev/null +++ b/skills/l7/references/core/scene.md @@ -0,0 +1,357 @@ +--- +skill_id: scene-initialization +skill_name: 场景初始化 +category: core +difficulty: beginner +tags: [scene, initialization, map, setup] +dependencies: [] +version: 2.x +--- + +# 场景初始化 + +## 技能描述 + +创建 L7 场景对象(Scene),这是使用 L7 的第一步。Scene 是包含地图、图层、组件的全局容器。 + +## 何时使用 + +- 开始任何 L7 可视化项目 +- 需要在页面中嵌入地图 +- 需要管理多个图层 + +## 前置条件 + +- HTML 页面中存在用于地图渲染的 DOM 容器 +- 已安装 `@antv/l7` 和地图底图库(如 `@antv/l7-maps`) + +## 输入参数 + +### 必需参数 + +| 参数 | 类型 | 说明 | +| ----- | --------------------- | ------------------------------------------------ | +| `id` | string \| HTMLElement | DOM 容器 ID 或 DOM 元素 | +| `map` | MapInstance | 地图实例 (GaodeMap \| Mapbox \| Maplibre \| Map) | + +### 地图配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +| --------- | ---------------- | ------- | ----------------------------------------------- | +| `style` | string | 'light' | 地图样式 (dark \| light \| normal \| satellite) | +| `center` | [number, number] | [0, 0] | 地图中心点 [经度, 纬度] | +| `zoom` | number | 0 | 缩放级别 (0-22) | +| `pitch` | number | 0 | 倾斜角度 (0-60) | +| `bearing` | number | 0 | 旋转角度 (0-360) | +| `minZoom` | number | 0 | 最小缩放级别 | +| `maxZoom` | number | 22 | 最大缩放级别 | + +### 场景配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +| ----------------------- | ------- | ------------ | ---------------------------- | +| `logoVisible` | boolean | true | 是否显示 L7 Logo | +| `logoPosition` | string | 'bottomleft' | Logo 位置 | +| `antialias` | boolean | true | 是否开启抗锯齿 | +| `stencil` | boolean | false | 是否开启裁剪(遮罩功能需要) | +| `preserveDrawingBuffer` | boolean | false | 是否保留缓冲区数据 | + +## 输出 + +返回 `Scene` 实例,可以用于: + +- 添加图层: `scene.addLayer(layer)` +- 添加组件: `scene.addControl(control)` +- 监听事件: `scene.on('loaded', callback)` +- 获取地图: `scene.getMapService()` + +## 代码示例 + +### 基础用法 - 高德地图 + +```javascript +import { Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +// 确保 HTML 中有对应的容器 +//
+ +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.19382669582967, 30.258134], + pitch: 0, + zoom: 12, + }), +}); + +// 等待场景加载完成 +scene.on('loaded', () => { + console.log('地图加载完成'); + // 在这里添加图层 +}); +``` + +### 使用 Mapbox + +```javascript +import { Scene } from '@antv/l7'; +import { Mapbox } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Mapbox({ + style: 'mapbox://styles/mapbox/streets-v11', + center: [120.19, 30.26], + zoom: 10, + token: 'your-mapbox-token', + }), +}); +``` + +### 使用 MapLibre(离线部署) + +```javascript +import { Scene } from '@antv/l7'; +import { Maplibre } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Maplibre({ + style: 'https://your-server.com/style.json', + center: [120.19, 30.26], + zoom: 10, + }), +}); +``` + +### 使用独立地图 Map(无第三方依赖) + +Map 是 L7 内置的独立地图引擎,不依赖任何第三方地图服务,适合室内地图、游戏地图等场景。 + +```javascript +import { Scene } from '@antv/l7'; +import { Map } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new Map({ + center: [0, 0], // 使用平面坐标 + zoom: 3, + style: 'blank', // 空白背景 + }), +}); + +scene.on('loaded', () => { + // Map 使用平面坐标系统,不是经纬度 + const data = [ + { x: 100, y: 100, value: 10 }, + { x: 200, y: 200, value: 20 }, + ]; + + const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'x', + y: 'y', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(pointLayer); +}); +``` + +**Map 的特点**: + +- ✅ 无需第三方地图服务(高德、Mapbox)Key +- ✅ 完全离线可用 +- ✅ 使用平面坐标系统 +- ✅ 适合室内地图、游戏地图、抽象数据可视化 + +### 3D 视角配置 + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.19, 30.26], + zoom: 14, + pitch: 45, // 倾斜角度,用于 3D 效果 + bearing: 30, // 旋转角度 + }), +}); +``` + +### 自定义 Logo 位置 + +```javascript +const scene = new Scene({ + id: 'map', + logoVisible: true, + logoPosition: 'bottomright', // bottomleft | topright | bottomleft | topleft + map: new GaodeMap({ + style: 'light', + center: [120.19, 30.26], + zoom: 10, + }), +}); +``` + +### 开启遮罩功能 + +```javascript +const scene = new Scene({ + id: 'map', + stencil: true, // 必须开启才能使用 Mask 功能 + map: new GaodeMap({ + style: 'dark', + center: [120.19, 30.26], + zoom: 10, + }), +}); +``` + +### 使用 DOM 元素 + +```javascript +const mapContainer = document.getElementById('my-map'); + +const scene = new Scene({ + id: mapContainer, + map: new GaodeMap({ + center: [120.19, 30.26], + zoom: 10, + }), +}); +``` + +## HTML 页面配置 + +```html + + + + + L7 地图 + + + +
+ + + +``` + +## 常见问题 + +**地理地图**(GaodeMap、Mapbox、Maplibre)使用 **WGS84** 坐标系统(经纬度): + +- 经度范围: -180 ~ 180 +- 纬度范围: -90 ~ 90 +- 格式: [经度, 纬度] 或 [lng, lat] + +中国常用城市坐标参考: + +- 北京: [116.404, 39.915] +- 上海: [121.473, 31.230] +- 杭州: [120.155, 30.274] +- 深圳: [114.057, 22.543] + +**独立地图**(Map)使用**平面坐标系统**: + +- X 轴:横向坐标(任意数值) +- Y 轴:纵向坐标(任意数值) +- 格式: [x, y] +- 适合室内地图、游戏地图等非地理场景 + document.addEventListener('DOMContentLoaded', () => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({...}) + }); + }); + +```` + +### 2. 高德地图 Key 配置 + +高德地图需要申请 Key: + +```javascript +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + zoom: 10, + token: 'your-amap-key' // 高德地图 Key + }) +}); +```` + +### 3. 地图未显示 + +**检查清单**: + +- ✅ 容器是否有高度(必须设置具体高度,不能为 0) +- ✅ 是否正确引入了底图库 +- ✅ 控制台是否有错误信息 +- ✅ 网络是否正常(在线底图需要联网) + +### 4. 坐标系统 + +L7 使用 **WGS84** 坐标系统(经纬度): + +- 经度范围: -180 ~ 180 +- 纬度范围: -90 ~ 90 +- 格式: [经度, 纬度] 或 [lng, lat] + +中国常用城市坐标参考: + +- 北京: [116.404, 39.915] +- 上海: [121.473, 31.230] +- 杭州: [120.155, 30.274] +- 深圳: [114.057, 22.543] + +## 场景生命周期事件 + +```javascript +// 场景加载完成 +scene.on('loaded', () => { + console.log('场景加载完成'); +}); + +// 场景销毁 +scene.on('destroy', () => { + console.log('场景已销毁'); +}); +``` + +## 相关技能 + +- [场景生命周期管理](./scene-lifecycle.md) +- [场景方法](./scene-methods.md) +- [添加图层](../layers/point.md) + +## 下一步 + +场景初始化完成后,你可以: + +1. [添加点图层显示数据](../layers/point.md) +2. [添加交互控件](../interaction/components.md) +3. [处理用户交互事件](../interaction/events.md) diff --git a/skills/l7/references/data/source-csv.md b/skills/l7/references/data/source-csv.md new file mode 100644 index 0000000..44339cc --- /dev/null +++ b/skills/l7/references/data/source-csv.md @@ -0,0 +1,566 @@ +--- +skill_id: source-csv +skill_name: CSV 数据源 +category: data +difficulty: beginner +tags: [csv, data, source, table, 数据源, 表格] +dependencies: [] +version: 2.x +--- + +# CSV 数据源 + +## 技能描述 + +使用 CSV(逗号分隔值)格式的数据作为图层数据源,适合处理表格数据。 + +## 何时使用 + +- ✅ 数据来自 Excel、数据库导出 +- ✅ 简单的点位数据 +- ✅ 统计数据、业务数据 +- ✅ 需要在 Excel 中编辑数据 +- ✅ 数据量较大但结构简单 + +## CSV 格式说明 + +CSV 是一种简单的文本格式,每行代表一条记录,字段之间用逗号分隔。 + +### 基本格式 + +```csv +lng,lat,name,value,type +120.19,30.26,杭州,100,city +121.47,31.23,上海,200,city +116.40,39.91,北京,300,capital +``` + +**格式要求**: + +- 第一行为字段名(表头) +- 每行数据字段数量一致 +- 使用逗号分隔字段 +- 字段值包含逗号时需要用引号包裹 + +## 代码示例 + +### 基础用法 - 点数据 + +```javascript +import { PointLayer } from '@antv/l7'; + +const csvData = `lng,lat,name,value,type +120.19,30.26,杭州,100,city +121.47,31.23,上海,200,city +116.40,39.91,北京,300,capital`; + +const pointLayer = new PointLayer() + .source(csvData, { + parser: { + type: 'csv', + x: 'lng', // 经度字段 + y: 'lat', // 纬度字段 + }, + }) + .shape('circle') + .size('value', [5, 20]) + .color('type', { + city: '#5B8FF9', + capital: '#FF6B3B', + }); + +scene.addLayer(pointLayer); +``` + +### 从文件加载 CSV + +```javascript +fetch('/data/cities.csv') + .then((res) => res.text()) + .then((csvText) => { + const layer = new PointLayer() + .source(csvText, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(layer); + }); +``` + +### 使用 fetch 异步加载 + +```javascript +async function loadCSVData() { + const response = await fetch('/data/cities.csv'); + const csvText = await response.text(); + + const layer = new PointLayer() + .source(csvText, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(8) + .color('#5B8FF9'); + + scene.addLayer(layer); +} + +scene.on('loaded', () => { + loadCSVData(); +}); +``` + +### 自定义分隔符 + +```javascript +// 使用制表符分隔的 TSV 文件 +const tsvData = `lng\tlat\tname\tvalue +120.19\t30.26\t杭州\t100 +121.47\t31.23\t上海\t200`; + +const layer = new PointLayer() + .source(tsvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + delimiter: '\t', // 指定分隔符为制表符 + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +### 处理带引号的字段 + +```javascript +const csvData = `lng,lat,name,address,value +120.19,30.26,杭州,"浙江省,杭州市",100 +121.47,31.23,上海,"上海市,黄浦区",200`; + +const layer = new PointLayer() + .source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +### 数据类型转换 + +```javascript +const csvData = `lng,lat,name,value,active +120.19,30.26,杭州,100,true +121.47,31.23,上海,200,false`; + +const layer = new PointLayer() + .source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 转换数据类型 + item.value = Number(item.value); + item.active = item.active === 'true'; + return item; + }, + }, + ], + }) + .shape('circle') + .size('value', [5, 20]) + .color('active', ['#999', '#5B8FF9']); + +scene.addLayer(layer); +``` + +### OD 数据(起点-终点) + +```javascript +const odCsvData = `from_lng,from_lat,to_lng,to_lat,count,type +120.19,30.26,121.47,31.23,100,train +120.19,30.26,116.40,39.91,200,plane`; + +const arcLayer = new LineLayer() + .source(odCsvData, { + parser: { + type: 'csv', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc') + .size('count', [1, 5]) + .color('type', { + train: '#5B8FF9', + plane: '#FF6B3B', + }); + +scene.addLayer(arcLayer); +``` + +## Parser 配置选项 + +### 必需参数 + +| 参数 | 类型 | 说明 | +| ------ | ------ | ----------------------- | +| `type` | string | 必须设置为 'csv' | +| `x` | string | 经度字段名(或 X 坐标) | +| `y` | string | 纬度字段名(或 Y 坐标) | + +### 可选参数 + +| 参数 | 类型 | 默认值 | 说明 | +| ----------- | ------ | ------ | ----------------------- | +| `x1` | string | - | 终点经度字段(OD 数据) | +| `y1` | string | - | 终点纬度字段(OD 数据) | +| `delimiter` | string | ',' | 字段分隔符 | + +## 数据示例 + +### 点位数据 + +```csv +lng,lat,name,type,value,date +120.19382669582967,30.258134,店铺A,restaurant,100,2024-01-01 +121.473701,31.230416,店铺B,cafe,200,2024-01-02 +116.404,39.915,店铺C,restaurant,150,2024-01-03 +``` + +### 轨迹数据(路径点) + +```csv +lng,lat,time,speed,status +120.19,30.26,2024-01-01 10:00:00,60,normal +120.20,30.27,2024-01-01 10:05:00,55,normal +120.21,30.28,2024-01-01 10:10:00,45,slow +``` + +### 统计数据(区域) + +```csv +region_id,region_name,center_lng,center_lat,population,gdp,area +330100,杭州市,120.19,30.26,1200,18000,16850 +310100,上海市,121.47,31.23,2400,38000,6340 +``` + +## 从 Excel 导出 CSV + +### Excel 导出步骤 + +1. 打开 Excel 文件 +2. 点击"文件" → "另存为" +3. 选择"CSV UTF-8(逗号分隔)(.csv)" +4. 保存文件 + +### 注意事项 + +- ✅ 确保第一行是字段名 +- ✅ 经纬度列名要明确(如 lng, lat) +- ✅ 使用 UTF-8 编码避免中文乱码 +- ✅ 日期格式要统一 + +## 常见问题 + +### 1. 中文乱码 + +**原因**: CSV 文件编码不是 UTF-8 + +**解决方案**: + +```javascript +// 在读取时指定编码 +fetch('/data/cities.csv') + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const decoder = new TextDecoder('utf-8'); + const csvText = decoder.decode(buffer); + + layer.source(csvText, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + }); + }); +``` + +或使用工具转换编码: + +```bash +# 使用 iconv 转换编码 +iconv -f GB2312 -t UTF-8 input.csv > output.csv +``` + +### 2. 数据不显示 + +**检查清单**: + +- ✅ 字段名是否正确(区分大小写) +- ✅ 坐标值是否为数字 +- ✅ 是否有空行或格式错误 +- ✅ parser 配置是否正确 + +```javascript +// 调试:打印解析后的数据 +layer.on('add', () => { + console.log('图层数据:', layer.getSource().data); +}); +``` + +### 3. 数字被当作字符串 + +CSV 中所有值默认都是字符串,需要手动转换: + +```javascript +layer.source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 转换为数字类型 + item.value = Number(item.value); + item.population = parseInt(item.population); + item.price = parseFloat(item.price); + return item; + }, + }, + ], +}); +``` + +### 4. 特殊字符处理 + +包含逗号、引号、换行符的字段需要特殊处理: + +```csv +name,description,value +"产品A","价格:1,000元",100 +"产品B","说明:包含""引号""",200 +``` + +## 性能优化 + +### 1. 大文件处理 + +```javascript +// 使用 Web Worker 处理大文件 +const worker = new Worker('csv-parser-worker.js'); + +worker.postMessage({ csvText: largeCSVData }); + +worker.onmessage = (e) => { + const data = e.data; + layer.source(data, { + parser: { + type: 'json', // Worker 已经解析过,使用 json + x: 'lng', + y: 'lat', + }, + }); +}; +``` + +### 2. 数据抽稀 + +```javascript +layer.source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'filter', + callback: (item, index) => { + // 只显示每 10 条数据 + return index % 10 === 0; + }, + }, + ], +}); +``` + +## CSV 转 GeoJSON + +```javascript +function csvToGeoJSON(csvText, xField, yField) { + const lines = csvText.trim().split('\n'); + const headers = lines[0].split(','); + + const features = lines.slice(1).map((line) => { + const values = line.split(','); + const properties = {}; + + headers.forEach((header, i) => { + properties[header] = values[i]; + }); + + return { + type: 'Feature', + properties: properties, + geometry: { + type: 'Point', + coordinates: [parseFloat(properties[xField]), parseFloat(properties[yField])], + }, + }; + }); + + return { + type: 'FeatureCollection', + features: features, + }; +} + +// 使用 +const csvText = `lng,lat,name,value +120.19,30.26,杭州,100 +121.47,31.23,上海,200`; + +const geojson = csvToGeoJSON(csvText, 'lng', 'lat'); + +layer.source(geojson, { + parser: { type: 'geojson' }, +}); +``` + +## 使用第三方库 + +### PapaParse(推荐) + +```javascript +import Papa from 'papaparse'; + +Papa.parse(csvText, { + header: true, // 第一行作为字段名 + dynamicTyping: true, // 自动类型转换 + complete: (results) => { + layer.source(results.data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); + }, +}); +``` + +### D3.js + +```javascript +import * as d3 from 'd3'; + +d3.csv('/data/cities.csv').then((data) => { + layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); +}); +``` + +## 最佳实践 + +### 1. 字段命名规范 + +```csv +# 推荐:使用英文字段名 +lng,lat,name,value,type + +# 不推荐:使用中文字段名 +经度,纬度,名称,数值,类型 +``` + +### 2. 数据验证 + +```javascript +function validateCSVData(data) { + return data.every((item) => { + return ( + !isNaN(item.lng) && + !isNaN(item.lat) && + item.lng >= -180 && + item.lng <= 180 && + item.lat >= -90 && + item.lat <= 90 + ); + }); +} +``` + +### 3. 错误处理 + +```javascript +fetch('/data/cities.csv') + .then((res) => { + if (!res.ok) { + throw new Error('Failed to load CSV file'); + } + return res.text(); + }) + .then((csvText) => { + layer.source(csvText, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, + }); + }) + .catch((error) => { + console.error('Error loading CSV:', error); + }); +``` + +## 相关技能 + +- [GeoJSON 数据源](./source-geojson.md) +- [JSON 数据源](./source-json.md) +- [数据解析配置](./source-parser.md) +- [点图层](../layers/point.md) +- [线图层](../layers/line.md) + +## 参考资源 + +- [RFC 4180 - CSV 规范](https://tools.ietf.org/html/rfc4180) +- [PapaParse - CSV 解析库](https://www.papaparse.com/) +- [D3.js - CSV 函数](https://github.com/d3/d3-dsv) diff --git a/skills/l7/references/data/source-geojson.md b/skills/l7/references/data/source-geojson.md new file mode 100644 index 0000000..3421631 --- /dev/null +++ b/skills/l7/references/data/source-geojson.md @@ -0,0 +1,518 @@ +--- +skill_id: source-geojson +skill_name: GeoJSON 数据源 +category: data +difficulty: beginner +tags: [geojson, data, source, geo, 数据源] +dependencies: [] +version: 2.x +--- + +# GeoJSON 数据源 + +## 技能描述 + +使用 GeoJSON 格式的地理数据作为图层数据源,这是 L7 最常用的数据格式。 + +## 何时使用 + +- ✅ 处理地理空间数据(点、线、面) +- ✅ 使用标准的地理数据格式 +- ✅ 数据包含几何信息和属性信息 +- ✅ 从 GIS 系统导出的数据 +- ✅ 行政区划、建筑、道路等矢量数据 + +## GeoJSON 格式说明 + +GeoJSON 是一种用于编码各种地理数据结构的格式,基于 JSON。 + +### 基本结构 + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "属性名称", + "value": 100 + }, + "geometry": { + "type": "Point", + "coordinates": [120.19, 30.26] + } + } + ] +} +``` + +### 几何类型 + +| 类型 | 说明 | coordinates 格式 | +| ----------------- | ---- | -------------------------------------------- | +| `Point` | 点 | `[lng, lat]` | +| `LineString` | 线 | `[[lng, lat], [lng, lat], ...]` | +| `Polygon` | 面 | `[[[lng, lat], [lng, lat], ...]]` | +| `MultiPoint` | 多点 | `[[lng, lat], [lng, lat], ...]` | +| `MultiLineString` | 多线 | `[[[lng, lat], ...], [[lng, lat], ...]]` | +| `MultiPolygon` | 多面 | `[[[[lng, lat], ...]], [[[lng, lat], ...]]]` | + +## 代码示例 + +### 基础用法 - 点数据 + +```javascript +import { PointLayer } from '@antv/l7'; + +const pointData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + name: '杭州', + population: 1200, + type: 'city', + }, + geometry: { + type: 'Point', + coordinates: [120.19, 30.26], + }, + }, + { + type: 'Feature', + properties: { + name: '上海', + population: 2400, + type: 'city', + }, + geometry: { + type: 'Point', + coordinates: [121.47, 31.23], + }, + }, + ], +}; + +const pointLayer = new PointLayer() + .source(pointData, { + parser: { + type: 'geojson', + }, + }) + .shape('circle') + .size('population', [5, 20]) + .color('type', ['#5B8FF9', '#5AD8A6']); + +scene.addLayer(pointLayer); +``` + +### 线数据 - LineString + +```javascript +import { LineLayer } from '@antv/l7'; + +const lineData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + name: '路线1', + type: 'highway', + }, + geometry: { + type: 'LineString', + coordinates: [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28], + ], + }, + }, + ], +}; + +const lineLayer = new LineLayer() + .source(lineData, { + parser: { + type: 'geojson', + }, + }) + .shape('line') + .size(3) + .color('type', ['#5B8FF9', '#5AD8A6']); + +scene.addLayer(lineLayer); +``` + +### 面数据 - Polygon + +```javascript +import { PolygonLayer } from '@antv/l7'; + +const polygonData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + name: '浙江省', + adcode: '330000', + gdp: 82553, + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [118.0, 28.0], + [122.0, 28.0], + [122.0, 31.0], + [118.0, 31.0], + [118.0, 28.0], + ], + ], + }, + }, + ], +}; + +const polygonLayer = new PolygonLayer() + .source(polygonData, { + parser: { + type: 'geojson', + }, + }) + .shape('fill') + .color('gdp', ['#FFF5B8', '#FFAB5C', '#FF6B3B', '#CC2B12']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(polygonLayer); +``` + +### 从 URL 加载 GeoJSON + +```javascript +const layer = new PolygonLayer(); + +fetch('https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json') + .then((res) => res.json()) + .then((data) => { + layer + .source(data, { + parser: { + type: 'geojson', + }, + }) + .shape('fill') + .color('name', ['#5B8FF9', '#5AD8A6', '#5D7092']) + .style({ + opacity: 0.8, + }); + + scene.addLayer(layer); + }); +``` + +### 多面 - MultiPolygon + +```javascript +const multiPolygonData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + name: '浙江省(含岛屿)', + }, + geometry: { + type: 'MultiPolygon', + coordinates: [ + // 主陆地 + [ + [ + [118.0, 28.0], + [122.0, 28.0], + [122.0, 31.0], + [118.0, 31.0], + [118.0, 28.0], + ], + ], + // 岛屿1 + [ + [ + [122.1, 30.0], + [122.2, 30.0], + [122.2, 30.1], + [122.1, 30.1], + [122.1, 30.0], + ], + ], + ], + }, + }, + ], +}; + +const layer = new PolygonLayer() + .source(multiPolygonData, { + parser: { + type: 'geojson', + }, + }) + .shape('fill') + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +### 带孔洞的面 + +```javascript +const polygonWithHole = { + type: 'Feature', + properties: { name: '带孔洞的面' }, + geometry: { + type: 'Polygon', + coordinates: [ + // 外环(逆时针) + [ + [118.0, 28.0], + [122.0, 28.0], + [122.0, 31.0], + [118.0, 31.0], + [118.0, 28.0], + ], + // 内环/孔洞(顺时针) + [ + [119.0, 29.0], + [119.0, 30.0], + [121.0, 30.0], + [121.0, 29.0], + [119.0, 29.0], + ], + ], + }, +}; +``` + +## 数据转换 + +### 从普通数组转换为 GeoJSON + +```javascript +// 原始数据 +const rawData = [ + { lng: 120.19, lat: 30.26, name: '杭州', value: 100 }, + { lng: 121.47, lat: 31.23, name: '上海', value: 200 }, +]; + +// 转换为 GeoJSON +const geojson = { + type: 'FeatureCollection', + features: rawData.map((item) => ({ + type: 'Feature', + properties: { + name: item.name, + value: item.value, + }, + geometry: { + type: 'Point', + coordinates: [item.lng, item.lat], + }, + })), +}; + +layer.source(geojson, { + parser: { type: 'geojson' }, +}); +``` + +### 使用工具库转换 + +```javascript +// 使用 turf.js +import * as turf from '@turf/turf'; + +const point = turf.point([120.19, 30.26], { name: '杭州' }); +const line = turf.lineString( + [ + [120, 30], + [121, 31], + ], + { name: '路线' }, +); +const polygon = turf.polygon( + [ + [ + [118, 28], + [122, 28], + [122, 31], + [118, 31], + [118, 28], + ], + ], + { name: '区域' }, +); + +layer.source(point, { + parser: { type: 'geojson' }, +}); +``` + +## 常见问题 + +### 1. 数据不显示 + +**检查清单**: + +- ✅ coordinates 格式是否正确(经度在前,纬度在后) +- ✅ 坐标范围是否合理(经度 -180~180,纬度 -90~90) +- ✅ GeoJSON 结构是否完整 +- ✅ parser 类型是否设置为 'geojson' + +```javascript +// 正确的格式 +coordinates: [120.19, 30.26]; // [经度, 纬度] + +// 错误的格式 +coordinates: [30.26, 120.19]; // [纬度, 经度] ❌ +``` + +### 2. 面不闭合 + +多边形的首尾坐标必须相同: + +```javascript +// 正确 - 闭合 +coordinates: [ + [ + [118.0, 28.0], + [122.0, 28.0], + [122.0, 31.0], + [118.0, 31.0], + [118.0, 28.0], // 与第一个点相同 + ], +]; + +// 错误 - 未闭合 +coordinates: [ + [ + [118.0, 28.0], + [122.0, 28.0], + [122.0, 31.0], + [118.0, 31.0], // 缺少闭合点 ❌ + ], +]; +``` + +### 3. 数据格式验证 + +使用在线工具验证 GeoJSON 格式: + +- [geojson.io](http://geojson.io/) +- [GeoJSONLint](https://geojsonlint.com/) + +### 4. 性能优化 + +对于复杂的 GeoJSON 数据: + +```javascript +// 简化几何形状 +layer.source(data, { + parser: { + type: 'geojson', + }, + transforms: [ + { + type: 'simplify', + tolerance: 0.01, // 简化容差 + }, + ], +}); +``` + +## GeoJSON 规范 + +### Feature 必需字段 + +```javascript +{ + "type": "Feature", // 必需 + "geometry": {}, // 必需 + "properties": {} // 可选,但通常包含 +} +``` + +### FeatureCollection 结构 + +```javascript +{ + "type": "FeatureCollection", // 必需 + "features": [] // 必需,Feature 数组 +} +``` + +### 坐标顺序 + +- **经度**(Longitude)在前,范围: -180 ~ 180 +- **纬度**(Latitude)在后,范围: -90 ~ 90 +- 格式: `[经度, 纬度]` 或 `[lng, lat]` + +### 环绕方向 + +- **外环**: 逆时针 +- **内环**(孔洞): 顺时针 + +## 最佳实践 + +### 1. 数据结构清晰 + +```javascript +// 推荐:属性语义化 +{ + "type": "Feature", + "properties": { + "id": "330100", + "name": "杭州市", + "type": "city", + "population": 1200, + "gdp": 18000 + }, + "geometry": {...} +} +``` + +### 2. 合理使用属性 + +```javascript +// 在 properties 中存储可视化需要的数据 +layer.color('type', {...}) +layer.size('population', [5, 20]) +``` + +### 3. 数据分层 + +```javascript +// 不同类型的数据使用不同图层 +const cityLayer = new PointLayer().source(cityGeoJSON); +const provinceLayer = new PolygonLayer().source(provinceGeoJSON); +``` + +## 相关技能 + +- [JSON 数据源](./source-json.md) +- [CSV 数据源](./source-csv.md) +- [数据解析配置](./source-parser.md) +- [点图层](../layers/point.md) +- [线图层](../layers/line.md) +- [面图层](../layers/polygon.md) + +## 参考资源 + +- [GeoJSON 规范](https://datatracker.ietf.org/doc/html/rfc7946) +- [Turf.js - GeoJSON 工具库](https://turfjs.org/) +- [geojson.io - 在线编辑器](http://geojson.io/) diff --git a/skills/l7/references/data/source-json.md b/skills/l7/references/data/source-json.md new file mode 100644 index 0000000..0948ce8 --- /dev/null +++ b/skills/l7/references/data/source-json.md @@ -0,0 +1,704 @@ +--- +skill_id: source-json +skill_name: JSON 数据源 +category: data +difficulty: beginner +tags: [json, data, source, object, 数据源] +dependencies: [] +version: 2.x +--- + +# JSON 数据源 + +## 技能描述 + +使用 JSON(JavaScript Object Notation)格式的数据作为图层数据源,适合结构化的业务数据。 + +## 何时使用 + +- ✅ API 返回的数据 +- ✅ 前端 JavaScript 对象 +- ✅ 业务数据、统计数据 +- ✅ 配置文件数据 +- ✅ 不需要地理拓扑结构的简单数据 + +## JSON 格式说明 + +JSON 是一种轻量级的数据交换格式,易于阅读和编写。 + +### 基本格式 + +```json +[ + { + "lng": 120.19, + "lat": 30.26, + "name": "杭州", + "value": 100, + "type": "city" + }, + { + "lng": 121.47, + "lat": 31.23, + "name": "上海", + "value": 200, + "type": "city" + } +] +``` + +## 代码示例 + +### 基础用法 - 点数据 + +```javascript +import { PointLayer } from '@antv/l7'; + +const data = [ + { lng: 120.19, lat: 30.26, name: '杭州', value: 100, type: 'city' }, + { lng: 121.47, lat: 31.23, name: '上海', value: 200, type: 'city' }, + { lng: 116.4, lat: 39.91, name: '北京', value: 300, type: 'capital' }, +]; + +const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', // 经度字段 + y: 'lat', // 纬度字段 + }, + }) + .shape('circle') + .size('value', [5, 20]) + .color('type', { + city: '#5B8FF9', + capital: '#FF6B3B', + }); + +scene.addLayer(pointLayer); +``` + +### 从 API 加载数据 + +```javascript +fetch('/api/cities') + .then((res) => res.json()) + .then((data) => { + const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(layer); + }); +``` + +### 异步加载数据 + +```javascript +async function loadData() { + const response = await fetch('/api/data'); + const data = await response.json(); + + const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'longitude', + y: 'latitude', + }, + }) + .shape('circle') + .size(8) + .color('#5B8FF9'); + + scene.addLayer(layer); +} + +scene.on('loaded', () => { + loadData(); +}); +``` + +### 嵌套对象数据 + +```javascript +const data = [ + { + location: { + lng: 120.19, + lat: 30.26, + }, + info: { + name: '杭州', + population: 1200, + }, + metrics: { + gdp: 18000, + growth: 0.08, + }, + }, +]; + +const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'location.lng', // 支持嵌套字段 + y: 'location.lat', + }, + }) + .shape('circle') + .size('info.population', [5, 20]) + .color('metrics.growth', ['#FFF5B8', '#FFAB5C', '#FF6B3B']); + +scene.addLayer(layer); +``` + +### 路径数据(LineString) + +```javascript +const pathData = [ + { + name: '路线1', + type: 'route', + path: [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28], + ], + }, + { + name: '路线2', + type: 'route', + path: [ + [121.47, 31.23], + [121.48, 31.24], + [121.49, 31.25], + ], + }, +]; + +const lineLayer = new LineLayer() + .source(pathData, { + parser: { + type: 'json', + coordinates: 'path', // 指定路径坐标字段 + }, + }) + .shape('line') + .size(3) + .color('type', ['#5B8FF9', '#5AD8A6']); + +scene.addLayer(lineLayer); +``` + +### OD 数据(起点-终点) + +```javascript +const odData = [ + { + from: { lng: 120.19, lat: 30.26 }, + to: { lng: 121.47, lat: 31.23 }, + count: 100, + type: 'migration', + }, + { + from: { lng: 120.19, lat: 30.26 }, + to: { lng: 116.4, lat: 39.91 }, + count: 200, + type: 'migration', + }, +]; + +// 方式1: 使用嵌套字段 +const arcLayer = new LineLayer() + .source(odData, { + parser: { + type: 'json', + x: 'from.lng', + y: 'from.lat', + x1: 'to.lng', + y1: 'to.lat', + }, + }) + .shape('arc') + .size('count', [1, 5]) + .color('#5B8FF9'); + +// 方式2: 先转换数据结构 +const transformedData = odData.map((item) => ({ + from_lng: item.from.lng, + from_lat: item.from.lat, + to_lng: item.to.lng, + to_lat: item.to.lat, + count: item.count, +})); + +const arcLayer2 = new LineLayer() + .source(transformedData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc') + .size('count', [1, 5]) + .color('#5B8FF9'); + +scene.addLayer(arcLayer); +``` + +### 使用坐标数组 + +```javascript +const data = [ + { + coordinates: [120.19, 30.26], + name: '点位1', + value: 100, + }, + { + coordinates: [121.47, 31.23], + name: '点位2', + value: 200, + }, +]; + +const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', // 直接使用坐标数组 + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +## Parser 配置选项 + +### 点数据配置 + +| 参数 | 类型 | 说明 | +| ------------- | ------ | -------------------------- | +| `type` | string | 必须设置为 'json' | +| `x` | string | 经度字段名(支持嵌套路径) | +| `y` | string | 纬度字段名(支持嵌套路径) | +| `coordinates` | string | 坐标数组字段(替代 x, y) | + +### OD 数据配置 + +| 参数 | 类型 | 说明 | +| ---- | ------ | -------- | +| `x` | string | 起点经度 | +| `y` | string | 起点纬度 | +| `x1` | string | 终点经度 | +| `y1` | string | 终点纬度 | + +### 路径数据配置 + +| 参数 | 类型 | 说明 | +| ------------- | ------ | ---------------- | +| `coordinates` | string | 路径坐标数组字段 | + +## 数据转换 + +### 自定义数据转换 + +```javascript +const rawData = [{ lon: 120.19, lat: 30.26, name: '杭州', val: '100' }]; + +const layer = new PointLayer() + .source(rawData, { + parser: { + type: 'json', + x: 'lon', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 字段名映射 + item.value = Number(item.val); + delete item.val; + + // 数据清洗 + if (item.value < 0) { + item.value = 0; + } + + return item; + }, + }, + ], + }) + .shape('circle') + .size('value', [5, 20]) + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +### 过滤数据 + +```javascript +const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'filter', + callback: (item) => { + // 只显示值大于 100 的数据 + return item.value > 100; + }, + }, + ], + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); +``` + +### JSON 转 GeoJSON + +```javascript +function jsonToGeoJSON(data, xField, yField) { + return { + type: 'FeatureCollection', + features: data.map((item) => ({ + type: 'Feature', + properties: { ...item }, + geometry: { + type: 'Point', + coordinates: [item[xField], item[yField]], + }, + })), + }; +} + +// 使用 +const jsonData = [{ lng: 120.19, lat: 30.26, name: '杭州', value: 100 }]; + +const geojson = jsonToGeoJSON(jsonData, 'lng', 'lat'); + +layer.source(geojson, { + parser: { type: 'geojson' }, +}); +``` + +## 常见问题 + +### 1. 字段名不匹配 + +**问题**: 数据字段名与 parser 配置不一致 + +**解决方案**: + +```javascript +// 检查字段名 +console.log('数据结构:', data[0]); + +// 确保字段名正确 +layer.source(data, { + parser: { + type: 'json', + x: 'longitude', // 注意大小写 + y: 'latitude', + }, +}); +``` + +### 2. 嵌套字段访问 + +**问题**: 无法访问嵌套对象的字段 + +**解决方案**: + +```javascript +// 使用点号访问嵌套字段 +layer.source(data, { + parser: { + type: 'json', + x: 'location.coordinates.lng', + y: 'location.coordinates.lat', + }, +}); + +// 或者先展平数据 +const flatData = data.map((item) => ({ + lng: item.location.coordinates.lng, + lat: item.location.coordinates.lat, + ...item.properties, +})); +``` + +### 3. 数据类型错误 + +**问题**: 坐标值是字符串而不是数字 + +**解决方案**: + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 转换为数字 + item.lng = Number(item.lng); + item.lat = Number(item.lat); + item.value = parseFloat(item.value); + return item; + }, + }, + ], +}); +``` + +### 4. 异步数据加载失败 + +**解决方案**: + +```javascript +async function loadDataSafely() { + try { + const response = await fetch('/api/data'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!Array.isArray(data) || data.length === 0) { + console.warn('No data returned'); + return; + } + + const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + + scene.addLayer(layer); + } catch (error) { + console.error('Failed to load data:', error); + } +} +``` + +## 性能优化 + +### 1. 数据分页加载 + +```javascript +let currentPage = 0; +const pageSize = 1000; + +async function loadPage(page) { + const response = await fetch(`/api/data?page=${page}&size=${pageSize}`); + const data = await response.json(); + + if (data.length > 0) { + const layer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(5) + .color('#5B8FF9'); + + scene.addLayer(layer); + } +} + +// 按需加载 +loadPage(currentPage); +``` + +### 2. 数据缓存 + +```javascript +const dataCache = new Map(); + +async function loadDataWithCache(url) { + if (dataCache.has(url)) { + return dataCache.get(url); + } + + const response = await fetch(url); + const data = await response.json(); + + dataCache.set(url, data); + return data; +} +``` + +### 3. 数据压缩 + +```javascript +// 服务端返回压缩数据 +async function loadCompressedData() { + const response = await fetch('/api/data.gz', { + headers: { + 'Accept-Encoding': 'gzip', + }, + }); + + const data = await response.json(); + // 使用数据 +} +``` + +## 数据验证 + +```javascript +function validateData(data) { + if (!Array.isArray(data)) { + throw new Error('Data must be an array'); + } + + const errors = []; + + data.forEach((item, index) => { + // 检查必需字段 + if (!item.lng || !item.lat) { + errors.push(`Item ${index}: missing lng or lat`); + } + + // 检查坐标范围 + if (item.lng < -180 || item.lng > 180) { + errors.push(`Item ${index}: invalid lng ${item.lng}`); + } + + if (item.lat < -90 || item.lat > 90) { + errors.push(`Item ${index}: invalid lat ${item.lat}`); + } + }); + + if (errors.length > 0) { + console.error('Data validation errors:', errors); + return false; + } + + return true; +} + +// 使用 +if (validateData(data)) { + layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); +} +``` + +## 最佳实践 + +### 1. 统一数据结构 + +```javascript +// 推荐:统一的字段命名 +const data = [ + { lng: 120.19, lat: 30.26, name: '点1', value: 100 }, + { lng: 121.47, lat: 31.23, name: '点2', value: 200 }, +]; + +// 避免:不一致的结构 +const badData = [ + { longitude: 120.19, latitude: 30.26, title: '点1' }, + { x: 121.47, y: 31.23, name: '点2' }, +]; +``` + +### 2. 错误处理 + +```javascript +fetch('/api/data') + .then((res) => res.json()) + .then((data) => { + if (validateData(data)) { + layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); + } + }) + .catch((error) => { + console.error('Error:', error); + // 显示错误提示 + }); +``` + +### 3. 数据更新 + +```javascript +// 动态更新数据 +function updateLayerData(newData) { + layer.setData(newData); +} + +// 定时更新 +setInterval(async () => { + const newData = await fetch('/api/data/latest').then((r) => r.json()); + updateLayerData(newData); +}, 5000); +``` + +## 相关技能 + +- [GeoJSON 数据源](./source-geojson.md) +- [CSV 数据源](./source-csv.md) +- [数据解析配置](./source-parser.md) +- [点图层](../layers/point.md) +- [线图层](../layers/line.md) + +## 参考资源 + +- [JSON 规范](https://www.json.org/) +- [MDN - JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) +- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) diff --git a/skills/l7/references/data/source-mvt.md b/skills/l7/references/data/source-mvt.md new file mode 100644 index 0000000..27dccdb --- /dev/null +++ b/skills/l7/references/data/source-mvt.md @@ -0,0 +1,574 @@ +--- +skill_id: source-mvt +skill_name: MVT 数据源 +category: data +difficulty: intermediate +tags: [mvt, vector-tile, pbf, tile-source] +dependencies: [] +version: 2.x +--- + +# MVT 数据源 + +## 技能描述 + +掌握 L7 中 MVT (Mapbox Vector Tile) 数据源的配置和使用。MVT 是一种高效的矢量瓦片格式,使用 Protocol Buffer 编码,广泛应用于大规模地理数据的可视化。 + +## 何时使用 + +- ✅ 需要加载矢量瓦片地图服务 +- ✅ 渲染海量矢量数据(百万级以上) +- ✅ 需要按需加载地理数据 +- ✅ 使用第三方瓦片服务(Mapbox、阿里云等) +- ✅ 自建 MVT 瓦片服务 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好 MVT 瓦片服务 URL + +## 核心概念 + +### MVT 格式 + +MVT (Mapbox Vector Tile) 是一种矢量瓦片规范: + +- **编码格式**:使用 Protocol Buffer(.pbf 文件) +- **瓦片结构**:遵循 TMS 或 XYZ 瓦片命名规则 +- **图层组织**:一个瓦片可包含多个图层(layers) +- **坐标系统**:使用 Web Mercator 投影(EPSG:3857) + +### 瓦片 URL 模板 + +MVT 数据源使用 URL 模板,包含以下占位符: + +- `{z}` - 缩放级别(zoom level) +- `{x}` - 列号(column) +- `{y}` - 行号(row) + +``` +https://example.com/tiles/{z}/{x}/{y}.pbf +``` + +## 基础用法 + +### 单服务器配置 + +```javascript +import { Source } from '@antv/l7'; + +const source = new Source('https://tiles.example.com/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', // 固定为 'mvt' + tileSize: 256, // 瓦片大小(像素) + maxZoom: 14, // 最大缩放级别 + minZoom: 0, // 最小缩放级别 + extent: [ + // 数据范围 [west, south, east, north] + -180, -85.051129, 179, 85.051129, + ], + }, +}); + +// 使用数据源 +layer.source(source); +``` + +### 多服务器配置 + +使用大括号语法实现负载均衡: + +```javascript +// 数字范围 {1-4} +const source1 = new Source('https://tile{1-4}.example.com/tiles/{z}/{x}/{y}.pbf', { + parser: { type: 'mvt' }, +}); + +// 字母范围 {a-d} +const source2 = new Source('https://tile{a-d}.example.com/tiles/{z}/{x}/{y}.pbf', { + parser: { type: 'mvt' }, +}); + +// 会自动分发到多个域名请求: +// tile1.example.com, tile2.example.com, tile3.example.com, tile4.example.com +``` + +## 配置参数 + +### parser 参数 + +| 参数 | 类型 | 默认值 | 说明 | +| ------------- | ---------------------------------- | -------------------------------------------- | ---------------------- | +| type | `string` | - | 固定为 `'mvt'` | +| tileSize | `number` | `256` | 瓦片大小(像素) | +| minZoom | `number` | `0` | 最小缩放级别 | +| maxZoom | `number` | `Infinity` | 最大缩放级别 | +| zoomOffset | `number` | `0` | 缩放级别偏移 | +| extent | `[number, number, number, number]` | `[-Infinity, -Infinity, Infinity, Infinity]` | 数据边界范围 | +| getCustomData | `function` | - | 自定义瓦片数据获取方法 | + +### tileSize + +瓦片的像素大小,常见值: + +- `256` - 标准瓦片大小(默认) +- `512` - 高清瓦片 +- `128` - 低精度瓦片 + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + tileSize: 512, // 使用 512 像素瓦片 + }, +}); +``` + +### minZoom / maxZoom + +控制瓦片请求的缩放级别范围: + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + minZoom: 3, // 地图缩放级别 < 3 时不请求 + maxZoom: 14, // 地图缩放级别 > 14 时使用 14 级数据 + }, +}); +``` + +**使用场景**: + +- `minZoom`:避免在全球视图下请求过多瓦片 +- `maxZoom`:复用较低层级数据,减少请求量 + +### zoomOffset + +瓦片层级偏移量: + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + zoomOffset: 1, // 请求比当前 zoom 高一级的瓦片 + }, +}); + +// 地图 zoom=10 时,请求 z=11 的瓦片 +``` + +**使用场景**: + +- 提高数据精度(正偏移) +- 减少瓦片数量(负偏移) + +### extent + +限制瓦片请求的地理范围: + +```javascript +// 只请求中国范围的瓦片 +const source = new Source(url, { + parser: { + type: 'mvt', + extent: [73.66, 3.86, 135.05, 53.55], // [west, south, east, north] + }, +}); +``` + +**使用场景**: + +- 区域性应用(只显示特定国家/地区) +- 减少不必要的瓦片请求 +- 提升性能 + +## 高级用法 + +### 1. 自定义瓦片请求 + +使用 `getCustomData` 实现自定义请求逻辑: + +```javascript +const source = new Source('https://api.example.com/tiles', { + parser: { + type: 'mvt', + getCustomData: (tile, callback) => { + const { x, y, z } = tile; + + // 自定义 URL 和参数 + const url = `https://api.example.com/tiles?z=${z}&x=${x}&y=${y}&token=YOUR_TOKEN`; + + fetch(url, { + headers: { + Authorization: 'Bearer YOUR_TOKEN', + }, + }) + .then((res) => res.arrayBuffer()) + .then((data) => { + callback(null, data); // 成功回调 + }) + .catch((err) => { + callback(err, null); // 失败回调 + }); + }, + }, +}); +``` + +**使用场景**: + +- 需要鉴权的瓦片服务 +- 动态参数(时间、过滤条件等) +- 数据加密/解密 +- 自定义缓存策略 + +### 2. 数据源复用 + +多个图层共享同一个 MVT 数据源: + +```javascript +const vectorSource = new Source( + 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/rs_l7/{z}/{x}/{y}.pbf', + { + parser: { + type: 'mvt', + maxZoom: 9, + }, + }, +); + +// 多个图层复用数据源 +const polygonLayer = new PolygonLayer({ + sourceLayer: 'regions', +}).source(vectorSource); + +const lineLayer = new LineLayer({ + sourceLayer: 'boundaries', +}).source(vectorSource); + +// 只会请求一份瓦片数据,高效! +scene.addLayer(polygonLayer); +scene.addLayer(lineLayer); +``` + +### 3. 多数据源组合 + +```javascript +// 全球底图数据 +const globalSource = new Source('https://global.tiles.com/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + maxZoom: 10, + }, +}); + +// 中国详细数据 +const chinaSource = new Source('https://china.tiles.com/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + extent: [73.66, 3.86, 135.05, 53.55], + minZoom: 10, + }, +}); + +// 根据缩放级别切换数据源 +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + if (zoom < 10) { + layer.source(globalSource); + } else { + layer.source(chinaSource); + } +}); +``` + +## 实际应用场景 + +### 1. 使用公开瓦片服务 + +#### 阿里云 Ganos + +```javascript +const source = new Source('https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/rs_l7/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 9, + extent: [-180, -85.051129, 179, 85.051129], + }, +}); +``` + +#### Mapbox + +```javascript +const source = new Source( + 'https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.mvt?access_token=YOUR_TOKEN', + { + parser: { + type: 'mvt', + maxZoom: 14, + }, + }, +); +``` + +### 2. 自建瓦片服务 + +#### MBTiles 文件服务 + +```javascript +// 使用 tileserver-gl 等工具提供服务 +const source = new Source('http://localhost:8080/data/china/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 14, + }, +}); +``` + +#### PostGIS + Tegola + +```javascript +const source = new Source('http://tegola.io/maps/osm/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + tileSize: 512, + }, +}); +``` + +### 3. 带鉴权的私有服务 + +```javascript +const source = new Source('https://api.example.com/secure-tiles', { + parser: { + type: 'mvt', + getCustomData: (tile, callback) => { + const { x, y, z } = tile; + + // 获取 token + const token = getAuthToken(); + + fetch(`https://api.example.com/secure-tiles/${z}/${x}/${y}.pbf`, { + headers: { + Authorization: `Bearer ${token}`, + 'X-API-Key': 'YOUR_API_KEY', + }, + }) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + return res.arrayBuffer(); + }) + .then((data) => callback(null, data)) + .catch((err) => { + console.error('瓦片加载失败:', err); + callback(err, null); + }); + }, + }, +}); +``` + +### 4. 动态参数(时间序列) + +```javascript +let currentTime = '2024-01-01'; + +const source = new Source('https://api.example.com/tiles', { + parser: { + type: 'mvt', + getCustomData: (tile, callback) => { + const { x, y, z } = tile; + const url = `https://api.example.com/tiles/${z}/${x}/${y}.pbf?time=${currentTime}`; + + fetch(url) + .then((res) => res.arrayBuffer()) + .then((data) => callback(null, data)) + .catch((err) => callback(err, null)); + }, + }, +}); + +// 更新时间参数 +function updateTime(newTime) { + currentTime = newTime; + layer.source(source); // 重新加载 + scene.render(); +} +``` + +## 性能优化 + +### 1. 合理设置缩放范围 + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + minZoom: 5, // 避免全球视图请求过多瓦片 + maxZoom: 14, // 限制最大精度,复用低层级数据 + }, +}); +``` + +### 2. 限制数据范围 + +```javascript +// 只请求业务相关区域的瓦片 +const source = new Source(url, { + parser: { + type: 'mvt', + extent: [100, 20, 130, 50], // 东亚地区 + }, +}); +``` + +### 3. 使用多服务器 + +```javascript +// 利用浏览器并发请求能力 +const source = new Source('https://tile{1-4}.example.com/{z}/{x}/{y}.pbf', { + parser: { type: 'mvt' }, +}); +``` + +### 4. 适当使用 zoomOffset + +```javascript +// 使用低一级的瓦片,减少请求量 +const source = new Source(url, { + parser: { + type: 'mvt', + zoomOffset: -1, // 地图 zoom=10 时使用 z=9 的瓦片 + }, +}); +``` + +## 常见问题 + +### Q: 如何查看瓦片包含的图层? + +A: 使用浏览器开发者工具或在线工具: + +```javascript +// 1. 查看网络请求的 PBF 文件 + +// 2. 使用 TileDebugLayer +import { TileDebugLayer } from '@antv/l7'; +const debugLayer = new TileDebugLayer(); +scene.addLayer(debugLayer); + +// 3. 使用在线工具 +// https://protomaps.github.io/PMTiles/ +// https://mapbox.github.io/vector-tile-spec/ +``` + +### Q: MVT 和 GeoJSON 有什么区别? + +A: + +| 对比项 | MVT | GeoJSON | +| ---------- | ------------------------ | --------------- | +| 编码格式 | Protocol Buffer (二进制) | JSON (文本) | +| 文件大小 | 小(通常 < 50KB) | 大(可能数 MB) | +| 加载方式 | 按需加载(瓦片) | 一次性加载全部 | +| 适用数据量 | 百万级以上 | 万级以下 | +| 数据修改 | 需重新生成瓦片 | 可直接修改 | + +### Q: 如何调试瓦片加载问题? + +A: + +1. **检查网络请求**: + - 打开浏览器开发者工具 Network 面板 + - 查看瓦片请求的 URL 是否正确 + - 检查 HTTP 状态码 + +2. **使用调试图层**: + +```javascript +import { TileDebugLayer } from '@antv/l7'; +const debugLayer = new TileDebugLayer(); +scene.addLayer(debugLayer); +``` + +3. **查看控制台错误**: + +```javascript +scene.on('error', (e) => { + console.error('场景错误:', e); +}); +``` + +### Q: 瓦片 404 怎么办? + +A: 检查以下几点: + +1. URL 模板格式是否正确(`{z}/{x}/{y}`) +2. 是否超出 maxZoom 范围 +3. extent 是否包含当前视野 +4. 服务器是否支持 CORS +5. 是否需要鉴权 + +### Q: 如何实现瓦片缓存? + +A: 浏览器会自动缓存瓦片(HTTP 缓存),也可以使用 Service Worker: + +```javascript +// 使用 getCustomData 实现自定义缓存 +const cache = new Map(); + +const source = new Source(url, { + parser: { + type: 'mvt', + getCustomData: (tile, callback) => { + const key = `${tile.z}/${tile.x}/${tile.y}`; + + // 从缓存读取 + if (cache.has(key)) { + callback(null, cache.get(key)); + return; + } + + // 请求数据 + fetch(url) + .then((res) => res.arrayBuffer()) + .then((data) => { + cache.set(key, data); // 存入缓存 + callback(null, data); + }) + .catch((err) => callback(err, null)); + }, + }, +}); +``` + +## 注意事项 + +⚠️ **坐标系统**:MVT 使用 Web Mercator 投影(EPSG:3857),确保数据匹配 + +⚠️ **文件格式**:瓦片必须是 Protocol Buffer 格式(.pbf 或 .mvt) + +⚠️ **CORS 配置**:跨域请求需要服务器配置 CORS 头 + +⚠️ **缩放范围**:合理设置 minZoom 和 maxZoom 避免性能问题 + +⚠️ **数据源复用**:多图层使用同一数据时,务必复用 Source 对象 + +⚠️ **URL 占位符**:确保使用正确的占位符格式(`{z}/{x}/{y}`,不是 `$z/$x/$y`) + +## 相关技能 + +- [矢量瓦片图层](../layers/tile-vector.md) +- [GeoJSON 数据源](./source-geojson.md) +- [场景初始化](../core/scene.md) +- [性能优化](../performance/optimization.md) + +## 在线示例 + +查看更多示例:[L7 矢量瓦片示例](https://l7.antv.antgroup.com/examples/tile/vector) diff --git a/skills/l7/references/data/source-parser.md b/skills/l7/references/data/source-parser.md new file mode 100644 index 0000000..104e04f --- /dev/null +++ b/skills/l7/references/data/source-parser.md @@ -0,0 +1,671 @@ +--- +skill_id: source-parser +skill_name: 数据解析配置 +category: data +difficulty: intermediate +tags: [parser, data, transform, config, 解析器, 数据转换] +dependencies: [source-geojson, source-csv, source-json] +version: 2.x +--- + +# 数据解析配置 + +## 技能描述 + +配置数据解析器(Parser)和数据转换器(Transform),将原始数据转换为 L7 可以使用的格式。 + +## 何时使用 + +- ✅ 数据格式需要特殊处理 +- ✅ 需要数据转换和清洗 +- ✅ 字段映射和重命名 +- ✅ 数据过滤和聚合 +- ✅ 自定义数据处理逻辑 + +## Parser 类型 + +L7 支持多种数据解析器: + +| Parser 类型 | 说明 | 适用场景 | +| ----------- | ------------- | -------------------- | +| `geojson` | GeoJSON 格式 | 标准地理数据 | +| `json` | JSON 对象数组 | 业务数据、API 数据 | +| `csv` | CSV 文本 | 表格数据、Excel 导出 | +| `image` | 图片 | 图片图层 | +| `raster` | 栅格数据 | 遥感影像 | +| `mvt` | 矢量瓦片 | 大规模矢量数据 | + +## 代码示例 + +### GeoJSON Parser + +```javascript +layer.source(geojsonData, { + parser: { + type: 'geojson', + }, +}); +``` + +### JSON Parser - 基础配置 + +```javascript +const data = [{ lng: 120.19, lat: 30.26, name: '杭州', value: 100 }]; + +layer.source(data, { + parser: { + type: 'json', + x: 'lng', // 经度字段 + y: 'lat', // 纬度字段 + }, +}); +``` + +### JSON Parser - 嵌套字段 + +```javascript +const data = [ + { + location: { + coordinates: { + lng: 120.19, + lat: 30.26, + }, + }, + info: { name: '杭州' }, + }, +]; + +layer.source(data, { + parser: { + type: 'json', + x: 'location.coordinates.lng', // 支持点号访问嵌套字段 + y: 'location.coordinates.lat', + }, +}); +``` + +### JSON Parser - 坐标数组 + +```javascript +const data = [ + { + coordinates: [120.19, 30.26], + name: '杭州', + }, +]; + +layer.source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', // 直接使用坐标数组 + }, +}); +``` + +### JSON Parser - 路径数据 + +```javascript +const pathData = [ + { + name: '路线1', + path: [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28], + ], + }, +]; + +layer.source(pathData, { + parser: { + type: 'json', + coordinates: 'path', // 路径坐标数组 + }, +}); +``` + +### JSON Parser - OD 数据 + +```javascript +const odData = [ + { + from_lng: 120.19, + from_lat: 30.26, + to_lng: 121.47, + to_lat: 31.23, + count: 100, + }, +]; + +layer.source(odData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', // 终点经度 + y1: 'to_lat', // 终点纬度 + }, +}); +``` + +### CSV Parser + +```javascript +const csvData = `lng,lat,name,value +120.19,30.26,杭州,100 +121.47,31.23,上海,200`; + +layer.source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + delimiter: ',', // 分隔符,默认为逗号 + }, +}); +``` + +### Image Parser + +```javascript +layer.source('https://example.com/image.png', { + parser: { + type: 'image', + extent: [minLng, minLat, maxLng, maxLat], // 图片地理范围 + }, +}); +``` + +### Raster Parser + +```javascript +layer.source(rasterImageUrl, { + parser: { + type: 'rasterImage', + extent: [minLng, minLat, maxLng, maxLat], + }, +}); +``` + +### MVT Parser(矢量瓦片) + +```javascript +layer.source('https://tiles.example.com/{z}/{x}/{y}.mvt', { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 14, + extent: [-180, -85.051129, 179, 85.051129], + }, +}); +``` + +## Transforms 数据转换 + +### Map 转换 - 字段映射 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 字段重命名 + item.value = item.val; + delete item.val; + + // 数据类型转换 + item.value = Number(item.value); + + // 计算新字段 + item.category = item.value > 100 ? 'high' : 'low'; + + return item; + }, + }, + ], +}); +``` + +### Filter 转换 - 数据过滤 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'filter', + callback: (item) => { + // 过滤条件 + return item.value > 100 && item.type === 'city'; + }, + }, + ], +}); +``` + +### 多个转换组合 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + // 1. 先过滤 + { + type: 'filter', + callback: (item) => item.value > 0, + }, + // 2. 再转换 + { + type: 'map', + callback: (item) => { + item.value = Math.sqrt(item.value); + item.normalized = item.value / 100; + return item; + }, + }, + ], +}); +``` + +## 常见场景 + +### 场景 1: API 数据适配 + +```javascript +// API 返回的数据结构 +const apiData = { + code: 200, + data: { + list: [ + { + id: 1, + location: { longitude: 120.19, latitude: 30.26 }, + metrics: { score: 85, rating: 4.5 }, + }, + ], + }, +}; + +// 提取并转换数据 +const data = apiData.data.list; + +layer.source(data, { + parser: { + type: 'json', + x: 'location.longitude', + y: 'location.latitude', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + return { + id: item.id, + lng: item.location.longitude, + lat: item.location.latitude, + score: item.metrics.score, + rating: item.metrics.rating, + }; + }, + }, + ], +}); +``` + +### 场景 2: 数据聚合 + +```javascript +// 对数据进行聚合 +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item, index, arr) => { + // 计算该点周围的平均值 + const nearby = arr.filter( + (d) => Math.abs(d.lng - item.lng) < 0.1 && Math.abs(d.lat - item.lat) < 0.1, + ); + + item.nearbyCount = nearby.length; + item.averageValue = nearby.reduce((sum, d) => sum + d.value, 0) / nearby.length; + + return item; + }, + }, + ], +}); +``` + +### 场景 3: 数据标准化 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item, index, arr) => { + // 计算最大最小值 + const values = arr.map((d) => d.value); + const min = Math.min(...values); + const max = Math.max(...values); + + // 归一化 + item.normalized = (item.value - min) / (max - min); + + return item; + }, + }, + ], +}); +``` + +### 场景 4: 时间数据处理 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + // 解析时间字符串 + item.timestamp = new Date(item.dateStr).getTime(); + + // 提取时间组件 + const date = new Date(item.dateStr); + item.year = date.getFullYear(); + item.month = date.getMonth() + 1; + item.day = date.getDate(); + item.hour = date.getHours(); + + return item; + }, + }, + { + type: 'filter', + callback: (item) => { + // 只显示最近7天的数据 + const now = Date.now(); + const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000; + return item.timestamp >= sevenDaysAgo; + }, + }, + ], +}); +``` + +### 场景 5: 坐标系转换 + +```javascript +// 假设需要从 GCJ-02 转 WGS84 +function gcj02ToWgs84(lng, lat) { + // 坐标转换算法 + // ... + return { lng: newLng, lat: newLat }; +} + +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item) => { + const { lng, lat } = gcj02ToWgs84(item.lng, item.lat); + item.lng = lng; + item.lat = lat; + return item; + }, + }, + ], +}); +``` + +## 性能优化 + +### 1. 避免在 transform 中进行重复计算 + +```javascript +// ❌ 不推荐:每条数据都计算全局统计 +transforms: [ + { + type: 'map', + callback: (item, index, arr) => { + const max = Math.max(...arr.map((d) => d.value)); // 重复计算 + item.normalized = item.value / max; + return item; + }, + }, +]; + +// ✅ 推荐:预先计算 +const max = Math.max(...data.map((d) => d.value)); + +transforms: [ + { + type: 'map', + callback: (item) => { + item.normalized = item.value / max; + return item; + }, + }, +]; +``` + +### 2. 合并转换逻辑 + +```javascript +// ❌ 不推荐:多次遍历 +transforms: [ + { type: 'filter', callback: (item) => item.value > 0 }, + { + type: 'map', + callback: (item) => { + item.doubled = item.value * 2; + return item; + }, + }, + { + type: 'map', + callback: (item) => { + item.category = item.doubled > 100 ? 'high' : 'low'; + return item; + }, + }, +]; + +// ✅ 推荐:一次遍历 +transforms: [ + { + type: 'map', + callback: (item) => { + if (item.value <= 0) return null; // 过滤 + + item.doubled = item.value * 2; + item.category = item.doubled > 100 ? 'high' : 'low'; + return item; + }, + }, + { + type: 'filter', + callback: (item) => item !== null, + }, +]; +``` + +### 3. 数据量大时使用 Web Worker + +```javascript +// worker.js +self.onmessage = function (e) { + const data = e.data; + + const processed = data.map((item) => { + // 复杂的数据处理 + return processItem(item); + }); + + self.postMessage(processed); +}; + +// 主线程 +const worker = new Worker('worker.js'); + +worker.postMessage(rawData); + +worker.onmessage = function (e) { + const processedData = e.data; + + layer.source(processedData, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); +}; +``` + +## 调试技巧 + +### 1. 打印解析后的数据 + +```javascript +layer.source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'map', + callback: (item, index) => { + // 打印前几条数据 + if (index < 3) { + console.log('Item', index, item); + } + return item; + }, + }, + ], +}); + +// 或者在图层添加后查看 +layer.on('add', () => { + const source = layer.getSource(); + console.log('Source data:', source.data.dataArray.slice(0, 5)); +}); +``` + +### 2. 验证数据格式 + +```javascript +transforms: [ + { + type: 'map', + callback: (item, index) => { + // 数据验证 + if (isNaN(item.lng) || isNaN(item.lat)) { + console.error(`Invalid coordinates at index ${index}:`, item); + } + + if (item.lng < -180 || item.lng > 180) { + console.warn(`Invalid longitude at index ${index}: ${item.lng}`); + } + + return item; + }, + }, +]; +``` + +## 常见问题 + +### 1. 数据解析失败 + +**检查清单**: + +- ✅ parser.type 是否正确 +- ✅ 字段名是否正确(区分大小写) +- ✅ 坐标格式是否正确(经度在前) +- ✅ 数据是否为空 + +### 2. Transform 不生效 + +**原因**: Transform callback 必须返回数据 + +```javascript +// ❌ 错误:没有返回值 +transforms: [ + { + type: 'map', + callback: (item) => { + item.value = item.value * 2; + // 忘记 return + }, + }, +]; + +// ✅ 正确 +transforms: [ + { + type: 'map', + callback: (item) => { + item.value = item.value * 2; + return item; // 必须返回 + }, + }, +]; +``` + +### 3. 性能问题 + +对于大数据量,避免复杂的 transform 操作,尽量在服务端处理。 + +## 相关技能 + +- [GeoJSON 数据源](./source-geojson.md) +- [CSV 数据源](./source-csv.md) +- [JSON 数据源](./source-json.md) +- [性能优化](../performance/optimization.md) + +## 最佳实践 + +1. **预处理优先**: 尽量在服务端或数据加载前完成数据处理 +2. **简化 transform**: Transform 在渲染时执行,保持逻辑简单 +3. **类型转换**: 确保数字字段是 number 类型而非 string +4. **错误处理**: 添加数据验证,避免坏数据导致渲染失败 +5. **性能监控**: 对大数据量使用 console.time 监控处理时间 diff --git a/skills/l7/references/interaction/components.md b/skills/l7/references/interaction/components.md new file mode 100644 index 0000000..6a39c56 --- /dev/null +++ b/skills/l7/references/interaction/components.md @@ -0,0 +1,212 @@ +# L7 Components Reference + +本文档整合了 L7 的所有交互组件使用指南。 + +## Popup - 信息弹窗 + +详见 [popup.md](popup.md) - 完整的 Popup 组件使用指南,包括基础用法、高级配置、样式定制等。 + +### 快速示例 + +```javascript +import { Popup } from '@antv/l7'; + +const popup = new Popup({ + closeButton: true, +}) + .setLnglat([120.19, 30.26]) + .setHTML('

标题

内容

'); + +scene.addPopup(popup); +``` + +### 事件绑定 + +```javascript +layer.on('click', (e) => { + const popup = new Popup().setLnglat(e.lnglat).setHTML(`
${e.feature.properties.name}
`); + scene.addPopup(popup); +}); +``` + +## Marker - 标注点 + +自定义 HTML 标注,支持拖拽。 + +### 基础用法 + +```javascript +import { Marker } from '@antv/l7'; + +const marker = new Marker().setLnglat([120.19, 30.26]); + +scene.addMarker(marker); +``` + +### 自定义样式 + +```javascript +const el = document.createElement('div'); +el.className = 'custom-marker'; +el.innerHTML = ''; + +const marker = new Marker({ + element: el, +}).setLnglat([120.19, 30.26]); + +scene.addMarker(marker); +``` + +### 拖拽标注 + +```javascript +const marker = new Marker({ + draggable: true, +}).setLnglat([120.19, 30.26]); + +marker.on('dragend', () => { + const lnglat = marker.getLnglat(); + console.log('新位置:', lnglat); +}); + +scene.addMarker(marker); +``` + +## Controls - 地图控件 + +L7 提供多种内置控件。 + +### Zoom Control - 缩放控件 + +```javascript +import { Zoom } from '@antv/l7'; + +const zoom = new Zoom({ + position: 'topright', +}); + +scene.addControl(zoom); +``` + +### Scale Control - 比例尺 + +```javascript +import { Scale } from '@antv/l7'; + +const scale = new Scale({ + position: 'bottomleft', +}); + +scene.addControl(scale); +``` + +### Layer Control - 图层控制 + +```javascript +import { LayerControl } from '@antv/l7'; + +const layerControl = new LayerControl({ + layers: [ + { layer: pointLayer, name: '点图层' }, + { layer: lineLayer, name: '线图层' }, + ], +}); + +scene.addControl(layerControl); +``` + +## Legend - 图例 + +自定义图例组件。 + +### 基础图例 + +```javascript +import { Legend } from '@antv/l7'; + +const legend = new Legend({ + position: 'bottomright', + items: [ + { color: '#5B8FF9', value: '类型A' }, + { color: '#5AD8A6', value: '类型B' }, + { color: '#FF6B3B', value: '类型C' }, + ], +}); + +scene.addControl(legend); +``` + +### 渐变图例 + +```javascript +const legend = new Legend({ + position: 'bottomright', + type: 'gradient', + items: [ + { color: '#FFF5B8', value: '0' }, + { color: '#FFAB5C', value: '50' }, + { color: '#FF6B3B', value: '100' }, + ], +}); + +scene.addControl(legend); +``` + +## 组件组合使用 + +实际应用中通常组合使用多个组件: + +```javascript +import { Scene, PointLayer, Popup, Marker, Zoom, Scale } from '@antv/l7'; + +// 添加控件 +scene.addControl(new Zoom({ position: 'topright' })); +scene.addControl(new Scale({ position: 'bottomleft' })); + +// 添加标注 +const marker = new Marker().setLnglat([120.19, 30.26]); +scene.addMarker(marker); + +// 点击显示 Popup +layer.on('click', (e) => { + const popup = new Popup().setLnglat(e.lnglat).setHTML(`

${e.feature.properties.name}

`); + scene.addPopup(popup); +}); +``` + +## 常见问题 + +### 1. Popup 位置偏移 + +使用 `offset` 调整位置: + +```javascript +const popup = new Popup({ + offset: [0, -10], // x, y 偏移量 +}); +``` + +### 2. Marker 自定义图标 + +```javascript +const el = document.createElement('div'); +el.style.backgroundImage = 'url(icon.png)'; +el.style.width = '32px'; +el.style.height = '32px'; + +const marker = new Marker({ element: el }); +``` + +### 3. 控件位置 + +支持的位置: + +- `'topleft'` - 左上角 +- `'topright'` - 右上角 +- `'bottomleft'` - 左下角 +- `'bottomright'` - 右下角 + +## 相关文档 + +- [events.md](events.md) - 事件处理详细指南 +- [popup.md](popup.md) - Popup 完整文档 diff --git a/skills/l7/references/interaction/controls.md b/skills/l7/references/interaction/controls.md new file mode 100644 index 0000000..3fcf6b1 --- /dev/null +++ b/skills/l7/references/interaction/controls.md @@ -0,0 +1,674 @@ +--- +skill_id: controls +skill_name: 地图控件 +category: interaction +difficulty: intermediate +tags: [control, zoom, scale, fullscreen, layer-switch] +dependencies: [scene-initialization] +version: 2.x +--- + +# 地图控件 + +## 技能描述 + +掌握 L7 地图控件的使用,通过各种控件实现地图缩放、比例尺显示、全屏、图层切换等交互功能。控件是悬浮在地图四周,对地图和图层进行信息呈现或交互的组件。 + +## 何时使用 + +- ✅ 需要地图缩放控制按钮 +- ✅ 需要显示地图比例尺 +- ✅ 需要全屏显示地图 +- ✅ 需要切换不同图层的显示 +- ✅ 需要显示鼠标位置的经纬度 +- ✅ 需要添加自定义 Logo + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) + +## 核心概念 + +### 控件位置 + +L7 支持将控件插入到地图的 **8 个位置** 或自定义 DOM: + +- `topleft` - 左上角 +- `topright` - 右上角 +- `bottomleft` - 左下角 +- `bottomright` - 右下角 +- `topcenter` - 顶部中心 +- `bottomcenter` - 底部中心 +- `leftcenter` - 左侧中心 +- `rightcenter` - 右侧中心 + +![控件位置](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*BfG1TI231ysAAAAAAAAAAAAAARQnAQ) + +### 控件排列 + +同一位置的多个控件支持横向和纵向排列,通过 `layout` 配置: + +```javascript +const control = new Zoom({ + position: 'topleft', + layout: 'horizontal', // 'horizontal' | 'vertical' +}); +``` + +## 基础用法 + +### 1. 添加控件 + +```javascript +import { Scene, Zoom } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120, 30], + zoom: 10, + }), +}); + +scene.on('loaded', () => { + // 实例化控件 + const zoom = new Zoom({ + position: 'topleft', + }); + + // 添加控件 + scene.addControl(zoom); +}); +``` + +### 2. 移除控件 + +```javascript +// 移除指定控件 +scene.removeControl(zoom); + +// 移除所有控件 +scene.removeAllControl(); +``` + +### 3. 更新控件配置 + +```javascript +const zoom = new Zoom({ + position: 'topleft', +}); + +scene.addControl(zoom); + +// 更新配置 +zoom.setOptions({ + position: 'topright', + className: 'custom-class', +}); +``` + +## 内置控件 + +### Zoom - 缩放控件 + +用于地图放大和缩小操作。 + +![Zoom](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*CJx3Tby-XlEAAAAAAAAAAAAAARQnAQ) + +```javascript +import { Zoom } from '@antv/l7'; + +const zoom = new Zoom({ + position: 'topleft', + zoomInText: '+', // 放大按钮内容 + zoomInTitle: '放大', // 放大按钮 title + zoomOutText: '-', // 缩小按钮内容 + zoomOutTitle: '缩小', // 缩小按钮 title + showZoom: true, // 显示当前缩放级别数值 +}); + +scene.addControl(zoom); + +// 方法 +zoom.zoomIn(); // 放大 +zoom.zoomOut(); // 缩小 +``` + +**配置项**: + +| 参数 | 类型 | 说明 | +| ------------ | ------------------- | -------------------------------- | +| zoomInText | `Element \| string` | 放大按钮的展示内容 | +| zoomInTitle | `string` | 放大按钮的 title 属性 | +| zoomOutText | `Element \| string` | 缩小按钮的展示内容 | +| zoomOutTitle | `string` | 缩小按钮的 title 属性 | +| showZoom | `boolean` | 是否展示当前缩放级别(向下取整) | + +### Scale - 比例尺控件 + +显示地图比例尺,支持公制和英制。 + +![Scale](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*r3iSQI4SekYAAAAAAAAAAAAAARQnAQ) + +```javascript +import { Scale } from '@antv/l7'; + +const scale = new Scale({ + position: 'bottomleft', + lockWidth: false, // 是否固定容器宽度 + maxWidth: 100, // 容器最大宽度(像素) + metric: true, // 显示公制(千米) + imperial: false, // 显示英制(英里) + updateWhenIdle: false, // 是否只在拖拽/缩放结束后更新 +}); + +scene.addControl(scale); +``` + +**配置项**: + +| 参数 | 类型 | 默认值 | 说明 | +| -------------- | --------- | ------- | ---------------------- | +| lockWidth | `boolean` | `true` | 是否固定容器宽度 | +| maxWidth | `number` | `100` | 组件容器最大宽度 | +| metric | `boolean` | `true` | 展示千米格式的比例尺 | +| imperial | `boolean` | `false` | 展示英里格式的比例尺 | +| updateWhenIdle | `boolean` | `false` | 是否只在交互结束后更新 | + +### Fullscreen - 全屏控件 + +控制地图区域的全屏显示。 + +![Fullscreen](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*CcOXRqK5ARgAAAAAAAAAAAAAARQnAQ) + +```javascript +import { Fullscreen } from '@antv/l7'; + +const fullscreen = new Fullscreen({ + position: 'topright', + btnText: '全屏', // 按钮文本 + btnTitle: '全屏', // 按钮 title + exitBtnText: '退出', // 退出按钮文本 + exitTitle: '退出全屏', // 退出按钮 title +}); + +scene.addControl(fullscreen); + +// 方法 +fullscreen.toggleFullscreen(); // 切换全屏状态 + +// 事件 +fullscreen.on('fullscreenChange', (isFullscreen) => { + console.log('全屏状态:', isFullscreen); +}); +``` + +**配置项**: + +| 参数 | 类型 | 说明 | +| ----------- | --------------------------- | ----------------------- | +| btnIcon | `HTMLElement \| SVGElement` | 全屏按钮图标 | +| btnText | `string` | 全屏按钮文本 | +| btnTitle | `string` | 全屏按钮 title 属性 | +| exitBtnIcon | `HTMLElement \| SVGElement` | 退出全屏按钮图标 | +| exitBtnText | `string` | 退出全屏按钮文本 | +| exitTitle | `string` | 退出全屏按钮 title 属性 | + +### Logo - 标志控件 + +在地图上展示 Logo 图片,支持超链接跳转。 + +![Logo](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*CbdSRLizMLIAAAAAAAAAAAAAARQnAQ) + +```javascript +import { Logo } from '@antv/l7'; + +// 隐藏默认 Logo +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + /* ... */ + }), + logoVisible: false, // 关闭默认 L7 Logo +}); + +// 添加自定义 Logo +const logo = new Logo({ + position: 'bottomleft', + img: 'https://example.com/logo.png', // Logo 图片 URL + href: 'https://example.com', // 点击跳转的链接 +}); + +scene.addControl(logo); +``` + +**配置项**: + +| 参数 | 类型 | 说明 | +| ---- | -------- | ------------------------ | +| img | `string` | Logo 图片 URL | +| href | `string` | 点击跳转的超链接(可选) | + +### LayerSwitch - 图层切换控件 + +控制图层的显示和隐藏,支持单选和多选模式。 + +![LayerSwitch](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*SiQWT5RnMDYAAAAAAAAAAAAAARQnAQ) + +```javascript +import { LayerSwitch } from '@antv/l7'; + +// 创建图层时指定名称 +const layer1 = new PointLayer({ + name: 'POI图层', +}) + .source(data1) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +const layer2 = new LineLayer({ + name: '道路图层', +}) + .source(data2) + .shape('line') + .size(2) + .color('#5AD8A6'); + +scene.addLayer(layer1); +scene.addLayer(layer2); + +// 添加图层切换控件 +const layerSwitch = new LayerSwitch({ + position: 'topright', + layers: [layer1, layer2], // 可传入图层实例或图层 ID + multiple: true, // 是否多选模式(false 为单选) +}); + +scene.addControl(layerSwitch); +``` + +**高级配置**: + +```javascript +const layerSwitch = new LayerSwitch({ + position: 'topright', + layers: [ + { + layer: layer1, + name: '自定义图层名称', // 覆盖图层默认名称 + img: 'https://example.com/icon.png', // 图层缩略图 + }, + { + layer: layer2, + name: '道路网络', + }, + ], + multiple: true, +}); + +scene.addControl(layerSwitch); +``` + +**配置项**: + +| 参数 | 类型 | 说明 | +| -------- | -------------------------------------------- | ---------------------------------------- | +| layers | `Array` | 需要控制的图层数组 | +| multiple | `boolean` | 是否多选模式(单选时默认显示第一个图层) | + +### MouseLocation - 鼠标位置控件 + +实时显示鼠标在地图上的经纬度坐标。 + +![MouseLocation](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*i4F5QZ4K650AAAAAAAAAAAAAARQnAQ) + +```javascript +import { MouseLocation } from '@antv/l7'; + +const mouseLocation = new MouseLocation({ + position: 'bottomright', + transform: (position) => { + // 可以对坐标进行转换处理 + const [lng, lat] = position; + return [lng.toFixed(6), lat.toFixed(6)]; + }, +}); + +scene.addControl(mouseLocation); + +// 事件 +mouseLocation.on('locationChange', (position) => { + console.log('当前位置:', position); +}); +``` + +**配置项**: + +| 参数 | 类型 | 说明 | +| --------- | -------------------------------------------------- | ------------------ | +| transform | `(position: [number, number]) => [number, number]` | 转换坐标的回调函数 | + +## 通用配置 + +所有控件都支持以下通用配置: + +| 参数 | 类型 | 默认值 | 说明 | +| --------- | ---------------------------- | ------------ | --------------- | +| position | `string` | `'topright'` | 控件位置 | +| name | `string` | - | 控件名称 | +| className | `string` | - | 自定义 CSS 类名 | +| layout | `'horizontal' \| 'vertical'` | - | 排列方向 | + +## 通用方法 + +所有控件都支持以下方法: + +```javascript +// 显示控件 +control.show(); + +// 隐藏控件 +control.hide(); + +// 更新配置 +control.setOptions({ position: 'topleft' }); + +// 移除控件 +scene.removeControl(control); +``` + +## 实际应用场景 + +### 1. 完整地图控件组合 + +```javascript +import { Scene, Zoom, Scale, Fullscreen, Logo } from '@antv/l7'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + /* ... */ + }), + logoVisible: false, +}); + +scene.on('loaded', () => { + // 左上角:缩放控件 + const zoom = new Zoom({ + position: 'topleft', + showZoom: true, + }); + + // 右上角:全屏控件 + const fullscreen = new Fullscreen({ + position: 'topright', + }); + + // 左下角:自定义 Logo + const logo = new Logo({ + position: 'bottomleft', + img: 'https://example.com/logo.png', + href: 'https://example.com', + }); + + // 右下角:比例尺 + const scale = new Scale({ + position: 'bottomright', + metric: true, + }); + + scene.addControl(zoom); + scene.addControl(fullscreen); + scene.addControl(logo); + scene.addControl(scale); +}); +``` + +### 2. 图层组管理 + +```javascript +// 创建不同类型的图层 +const baseLayer = new PolygonLayer({ + name: '行政区划', +}) + .source(adminData) + .color('#f0f0f0'); + +const heatLayer = new HeatmapLayer({ + name: '人口热力图', +}) + .source(populationData) + .size('value', [0, 1]); + +const poiLayer = new PointLayer({ + name: 'POI点', +}) + .source(poiData) + .shape('circle') + .size(8); + +scene.addLayer(baseLayer); +scene.addLayer(heatLayer); +scene.addLayer(poiLayer); + +// 添加图层切换控件 +const layerSwitch = new LayerSwitch({ + position: 'topright', + layers: [ + { + layer: baseLayer, + name: '底图', + img: 'base.png', + }, + { + layer: heatLayer, + name: '热力', + img: 'heat.png', + }, + { + layer: poiLayer, + name: '标注', + img: 'poi.png', + }, + ], + multiple: true, +}); + +scene.addControl(layerSwitch); +``` + +### 3. 自定义样式控件 + +```javascript +const zoom = new Zoom({ + position: 'topleft', + className: 'custom-zoom', + zoomInText: '➕', + zoomOutText: '➖', +}); + +scene.addControl(zoom); + +// CSS 样式 +/* +.custom-zoom { + background: rgba(0, 0, 0, 0.8); + border-radius: 8px; + padding: 8px; +} + +.custom-zoom button { + color: white; + font-size: 20px; +} +*/ +``` + +### 4. 响应式控件位置 + +```javascript +const zoom = new Zoom({ + position: window.innerWidth < 768 ? 'bottomleft' : 'topleft', +}); + +scene.addControl(zoom); + +// 监听窗口大小变化 +window.addEventListener('resize', () => { + const newPosition = window.innerWidth < 768 ? 'bottomleft' : 'topleft'; + zoom.setOptions({ position: newPosition }); +}); +``` + +### 5. 控件事件监听 + +```javascript +const layerSwitch = new LayerSwitch({ + layers: [layer1, layer2], + multiple: false, +}); + +// 监听图层切换 +layerSwitch.on('selectChange', (selectedLayers) => { + console.log('当前选中的图层:', selectedLayers); + + // 根据选中图层更新其他UI + updateLegend(selectedLayers[0]); +}); + +const fullscreen = new Fullscreen(); + +fullscreen.on('fullscreenChange', (isFullscreen) => { + if (isFullscreen) { + console.log('进入全屏'); + // 隐藏其他UI元素 + } else { + console.log('退出全屏'); + // 恢复UI元素 + } +}); + +scene.addControl(layerSwitch); +scene.addControl(fullscreen); +``` + +## 常见问题 + +### Q: 如何自定义控件位置? + +A: 除了使用预设的 8 个位置,还可以通过 CSS 自定义: + +```javascript +const zoom = new Zoom({ + position: 'topleft', + className: 'custom-position', +}); + +scene.addControl(zoom); + +// CSS +/* +.custom-position { + position: absolute; + top: 100px !important; + left: 20px !important; +} +*/ +``` + +### Q: 如何移除默认 Logo? + +A: 在 Scene 初始化时配置: + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + /* ... */ + }), + logoVisible: false, // 关闭默认 Logo +}); +``` + +### Q: LayerSwitch 如何指定默认显示的图层? + +A: 单选模式下默认显示第一个图层,多选模式下根据图层初始状态: + +```javascript +// 单选模式:默认显示 layer1 +const layerSwitch = new LayerSwitch({ + layers: [layer1, layer2, layer3], + multiple: false, +}); + +// 多选模式:控制图层初始可见性 +layer1.show(); +layer2.hide(); +layer3.show(); + +const layerSwitch = new LayerSwitch({ + layers: [layer1, layer2, layer3], + multiple: true, +}); +``` + +### Q: 如何获取控件实例? + +A: 保存实例引用或通过 Scene 获取: + +```javascript +// 方式1:保存引用 +const zoom = new Zoom(); +scene.addControl(zoom); + +// 方式2:通过名称获取 +const zoom = new Zoom({ name: 'zoom-control' }); +scene.addControl(zoom); + +const control = scene.getControlByName('zoom-control'); +``` + +### Q: 控件如何实现国际化? + +A: 通过配置项传入不同语言文本: + +```javascript +const lang = 'zh-CN'; // 或 'en-US' + +const zoom = new Zoom({ + zoomInTitle: lang === 'zh-CN' ? '放大' : 'Zoom In', + zoomOutTitle: lang === 'zh-CN' ? '缩小' : 'Zoom Out', +}); + +const fullscreen = new Fullscreen({ + btnText: lang === 'zh-CN' ? '全屏' : 'Fullscreen', + exitBtnText: lang === 'zh-CN' ? '退出' : 'Exit', +}); +``` + +## 注意事项 + +⚠️ **控件顺序**:同一位置的控件按添加顺序排列 + +⚠️ **图层名称**:LayerSwitch 依赖图层的 `name` 属性,务必在创建图层时指定 + +⚠️ **移动端适配**:移动端建议将控件放在不遮挡重要内容的位置 + +⚠️ **控件样式**:可通过 `className` 自定义样式,但需注意不要影响控件功能 + +⚠️ **性能考虑**:MouseLocation 控件会高频更新,注意 `transform` 回调的性能 + +⚠️ **全屏 API**:Fullscreen 控件依赖浏览器的 Fullscreen API,部分浏览器可能不支持 + +## 相关技能 + +- [场景初始化](../core/scene.md) +- [场景方法](../core/scene-methods.md) +- [图层管理](../layers/base-layer.md) +- [事件处理](./events.md) + +## 在线示例 + +查看更多示例:[L7 控件示例](https://l7.antv.antgroup.com/examples/component/control) diff --git a/skills/l7/references/interaction/events.md b/skills/l7/references/interaction/events.md new file mode 100644 index 0000000..7ec909f --- /dev/null +++ b/skills/l7/references/interaction/events.md @@ -0,0 +1,485 @@ +--- +skill_id: event-handling +skill_name: 事件处理 +category: interaction +difficulty: beginner +tags: [event, click, mousemove, interaction] +dependencies: [scene-initialization] +version: 2.x +--- + +# 事件处理 + +## 技能描述 + +为图层添加交互事件监听,响应用户操作(点击、鼠标移动、悬停等)。 + +## 何时使用 + +- ✅ 点击要素查看详情 +- ✅ 鼠标悬停高亮显示 +- ✅ 右键显示菜单 +- ✅ 双击缩放定位 +- ✅ 实现自定义交互逻辑 + +## 支持的事件类型 + +| 事件 | 说明 | 常用场景 | +| ------------- | -------- | ------------------ | +| `click` | 单击 | 查看详情、选中要素 | +| `dblclick` | 双击 | 缩放定位 | +| `mousemove` | 鼠标移动 | Tooltip、高亮 | +| `mouseenter` | 鼠标进入 | 高亮、改变样式 | +| `mouseleave` | 鼠标离开 | 取消高亮 | +| `mousedown` | 鼠标按下 | 拖拽开始 | +| `mouseup` | 鼠标抬起 | 拖拽结束 | +| `contextmenu` | 右键菜单 | 自定义菜单 | + +## 代码示例 + +### 基础用法 - 点击事件 + +```javascript +layer.on('click', (e) => { + console.log('点击位置:', e.lngLat); + console.log('要素数据:', e.feature); + console.log('要素属性:', e.feature.properties); +}); +``` + +### 点击显示详情 + +```javascript +import { Popup } from '@antv/l7'; + +layer.on('click', (e) => { + const { name, value, category } = e.feature.properties; + + const popup = new Popup({ + offsets: [0, 10], + }).setLnglat(e.lngLat).setHTML(` +
+

${name}

+

类别: ${category}

+

数值: ${value}

+
+ `); + + scene.addPopup(popup); +}); +``` + +### 鼠标悬停高亮 + +```javascript +let hoveredFeatureId = null; + +// 鼠标进入 +layer.on('mouseenter', (e) => { + // 重置之前的高亮 + if (hoveredFeatureId !== null) { + layer.setActive(hoveredFeatureId, false); + } + + // 高亮当前要素 + hoveredFeatureId = e.featureId; + layer.setActive(hoveredFeatureId, true); + + // 改变鼠标样式 + scene.setMapStatus({ dragEnable: false }); + document.body.style.cursor = 'pointer'; +}); + +// 鼠标离开 +layer.on('mouseleave', () => { + // 取消高亮 + if (hoveredFeatureId !== null) { + layer.setActive(hoveredFeatureId, false); + hoveredFeatureId = null; + } + + // 恢复鼠标样式 + scene.setMapStatus({ dragEnable: true }); + document.body.style.cursor = 'default'; +}); + +// 配置高亮样式 +layer.style({ + selectColor: '#FF0000', +}); +``` + +### 鼠标移动显示 Tooltip + +```javascript +let popup = null; + +layer.on('mousemove', (e) => { + const { name, value } = e.feature.properties; + + // 移除旧的 Tooltip + if (popup) { + popup.remove(); + } + + // 创建新的 Tooltip + popup = new Popup({ + closeButton: false, + closeOnClick: false, + anchor: 'bottom', + offsets: [0, -10], + }).setLnglat(e.lngLat).setHTML(` +
+ ${name}: ${value} +
+ `); + + scene.addPopup(popup); +}); + +layer.on('mouseleave', () => { + if (popup) { + popup.remove(); + popup = null; + } +}); +``` + +### 右键菜单 + +```javascript +layer.on('contextmenu', (e) => { + e.originalEvent.preventDefault(); // 阻止默认右键菜单 + + const { name, id } = e.feature.properties; + + // 显示自定义菜单 + showContextMenu(e.x, e.y, [ + { + label: '查看详情', + onClick: () => showDetail(id), + }, + { + label: '编辑', + onClick: () => editFeature(id), + }, + { + label: '删除', + onClick: () => deleteFeature(id), + }, + ]); +}); +``` + +### 双击定位 + +```javascript +layer.on('dblclick', (e) => { + const { lng, lat } = e.lngLat; + + // 缩放并定位到点击位置 + scene.setZoomAndCenter(12, [lng, lat], { + duration: 1000, // 动画时长 1 秒 + }); +}); +``` + +### 点击选中/取消选中 + +```javascript +let selectedFeatureId = null; + +layer.on('click', (e) => { + // 如果点击的是已选中的要素,取消选中 + if (selectedFeatureId === e.featureId) { + layer.setActive(selectedFeatureId, false); + selectedFeatureId = null; + return; + } + + // 取消之前的选中 + if (selectedFeatureId !== null) { + layer.setActive(selectedFeatureId, false); + } + + // 选中当前要素 + selectedFeatureId = e.featureId; + layer.setActive(selectedFeatureId, true); + + console.log('选中:', e.feature.properties.name); +}); +``` + +### 多选功能 + +```javascript +const selectedFeatures = new Set(); + +layer.on('click', (e) => { + const featureId = e.featureId; + + if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { + // 按住 Ctrl/Cmd 多选 + if (selectedFeatures.has(featureId)) { + selectedFeatures.delete(featureId); + layer.setActive(featureId, false); + } else { + selectedFeatures.add(featureId); + layer.setActive(featureId, true); + } + } else { + // 单选,清除其他选中 + selectedFeatures.forEach((id) => { + layer.setActive(id, false); + }); + selectedFeatures.clear(); + + selectedFeatures.add(featureId); + layer.setActive(featureId, true); + } + + console.log('已选中:', selectedFeatures.size, '个要素'); +}); +``` + +### 拖拽事件 + +```javascript +let isDragging = false; +let dragStartPos = null; + +layer.on('mousedown', (e) => { + isDragging = true; + dragStartPos = e.lngLat; + console.log('开始拖拽'); +}); + +layer.on('mousemove', (e) => { + if (isDragging) { + console.log('拖拽中:', e.lngLat); + } +}); + +layer.on('mouseup', (e) => { + if (isDragging) { + isDragging = false; + console.log('结束拖拽'); + console.log('拖拽距离:', calculateDistance(dragStartPos, e.lngLat)); + } +}); +``` + +### 节流优化 - 高频事件 + +```javascript +import { throttle } from 'lodash'; + +// 使用节流优化 mousemove 事件 +const handleMouseMove = throttle((e) => { + console.log('鼠标移动:', e.lngLat); + // 更新 UI +}, 100); // 每 100ms 最多执行一次 + +layer.on('mousemove', handleMouseMove); +``` + +## 事件对象属性 + +### 事件对象 (e) + +| 属性 | 类型 | 说明 | +| --------------- | ---------------- | ------------ | +| `lngLat` | {lng, lat} | 地理坐标 | +| `x` | number | 屏幕 X 坐标 | +| `y` | number | 屏幕 Y 坐标 | +| `feature` | Object | 要素对象 | +| `featureId` | string \| number | 要素 ID | +| `originalEvent` | Event | 原生事件对象 | + +### feature 对象 + +```javascript +layer.on('click', (e) => { + console.log(e.feature); + // { + // type: 'Feature', + // properties: { name: '杭州', value: 100 }, + // geometry: { type: 'Point', coordinates: [120, 30] } + // } +}); +``` + +## 移除事件监听 + +```javascript +// 添加事件监听 +const handleClick = (e) => { + console.log('点击'); +}; + +layer.on('click', handleClick); + +// 移除特定事件监听 +layer.off('click', handleClick); + +// 移除所有事件监听 +layer.off('click'); +``` + +## 场景事件 + +除了图层事件,Scene 也支持事件监听: + +```javascript +// 场景加载完成 +scene.on('loaded', () => { + console.log('场景加载完成'); +}); + +// 地图移动 +scene.on('mapmove', () => { + console.log('地图移动'); +}); + +// 地图缩放 +scene.on('zoom', (e) => { + console.log('缩放级别:', e.zoom); +}); + +// 地图点击(未点击到图层) +scene.on('click', (e) => { + console.log('点击地图:', e.lngLat); +}); +``` + +## 常见问题 + +### 1. 事件不触发 + +**检查清单**: + +- ✅ 图层是否已添加到场景 +- ✅ 图层是否可见 +- ✅ 事件名称是否正确 +- ✅ 是否有其他图层遮挡 + +```javascript +// 调试代码 +console.log('图层是否可见:', layer.isVisible()); +console.log('图层层级:', layer.zIndex); +``` + +### 2. 事件触发多次 + +**原因**: 可能添加了多个相同的监听器 + +**解决方案**: + +```javascript +// 移除旧的监听器 +layer.off('click'); + +// 添加新的监听器 +layer.on('click', handler); +``` + +### 3. 性能问题 - mousemove 卡顿 + +**原因**: mousemove 事件频率太高 + +**解决方案**: 使用节流 + +```javascript +import { throttle } from 'lodash'; + +const handler = throttle((e) => { + // 处理逻辑 +}, 100); + +layer.on('mousemove', handler); +``` + +### 4. 阻止事件冒泡 + +```javascript +layer.on('click', (e) => { + e.originalEvent.stopPropagation(); // 阻止冒泡 + + // 你的逻辑 +}); +``` + +## 高级用法 + +### 事件委托 - 统一处理 + +```javascript +class LayerEventManager { + constructor(layers) { + this.layers = layers; + this.selectedFeatures = new Map(); + } + + bindClickEvents() { + this.layers.forEach((layer) => { + layer.on('click', (e) => { + this.handleClick(layer, e); + }); + }); + } + + handleClick(layer, e) { + console.log(`图层 ${layer.id} 被点击`); + console.log('要素:', e.feature.properties); + + // 统一的选中逻辑 + this.selectFeature(layer, e.featureId); + } + + selectFeature(layer, featureId) { + // 清除其他图层的选中 + this.clearAllSelections(); + + // 选中当前要素 + layer.setActive(featureId, true); + this.selectedFeatures.set(layer.id, featureId); + } + + clearAllSelections() { + this.selectedFeatures.forEach((featureId, layerId) => { + const layer = this.layers.find((l) => l.id === layerId); + if (layer) { + layer.setActive(featureId, false); + } + }); + this.selectedFeatures.clear(); + } +} + +// 使用 +const manager = new LayerEventManager([layer1, layer2, layer3]); +manager.bindClickEvents(); +``` + +### 条件事件触发 + +```javascript +layer.on('click', (e) => { + // 只处理特定类型的要素 + if (e.feature.properties.type === 'important') { + handleImportantFeature(e); + } else { + handleNormalFeature(e); + } +}); +``` + +## 相关技能 + +- [场景初始化](../core/scene.md) +- [弹窗组件](./popup.md) +- [标注组件](./components.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 交互](https://l7.antv.antgroup.com/examples/interaction/select) diff --git a/skills/l7/references/interaction/popup.md b/skills/l7/references/interaction/popup.md new file mode 100644 index 0000000..95acb33 --- /dev/null +++ b/skills/l7/references/interaction/popup.md @@ -0,0 +1,454 @@ +--- +skill_id: popup +skill_name: 弹窗组件 +category: components +difficulty: beginner +tags: [popup, tooltip, info, interaction] +dependencies: [scene-initialization] +version: 2.x +--- + +# 弹窗组件 + +## 技能描述 + +在地图上显示信息弹窗,用于展示详细信息、提示文本等。 + +## 何时使用 + +- ✅ 点击要素显示详情 +- ✅ 鼠标悬停显示 Tooltip +- ✅ 显示固定位置的说明信息 +- ✅ 展示表格、图表等复杂内容 +- ✅ 自定义交互提示 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) + +## 代码示例 + +### 基础用法 - 简单弹窗 + +```javascript +import { Popup } from '@antv/l7'; + +const popup = new Popup({ + offsets: [0, 10], // 偏移量 [x, y] + closeButton: true, // 显示关闭按钮 +}) + .setLnglat([120.19, 30.26]) + .setHTML('
这是一个弹窗
'); + +scene.addPopup(popup); +``` + +### 点击图层显示弹窗 + +```javascript +import { PointLayer } from '@antv/l7'; +import { Popup } from '@antv/l7'; + +const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(pointLayer); + +// 点击显示弹窗 +pointLayer.on('click', (e) => { + const { name, value, category } = e.feature.properties; + + const popup = new Popup({ + offsets: [0, 10], + }).setLnglat(e.lngLat).setHTML(` +
+

${name}

+

类别: ${category}

+

数值: ${value}

+
+ `); + + scene.addPopup(popup); +}); +``` + +### 鼠标悬停显示 Tooltip + +```javascript +let popup = null; + +layer.on('mousemove', (e) => { + const { name, value } = e.feature.properties; + + // 移除旧弹窗 + if (popup) { + popup.remove(); + } + + // 创建新弹窗 + popup = new Popup({ + closeButton: false, + closeOnClick: false, + }) + .setLnglat(e.lngLat) + .setHTML(`
${name}: ${value}
`); + + scene.addPopup(popup); +}); + +layer.on('mouseleave', () => { + if (popup) { + popup.remove(); + popup = null; + } +}); +``` + +### 自定义样式的弹窗 + +```javascript +const popup = new Popup({ + offsets: [0, 10], + closeButton: true, + className: 'custom-popup', // 自定义 CSS 类名 +}).setLnglat([120.19, 30.26]).setHTML(` + + `); + +scene.addPopup(popup); +``` + +对应 CSS: + +```css +.custom-popup { + max-width: 300px; +} + +.custom-popup .popup-content { + padding: 15px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.custom-popup .popup-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; + color: #333; +} + +.custom-popup .popup-body { + font-size: 14px; + color: #666; +} + +.custom-popup .popup-body p { + margin: 5px 0; +} +``` + +### 使用 DOM 元素创建弹窗 + +```javascript +const el = document.createElement('div'); +el.className = 'my-popup'; +el.innerHTML = ` +

自定义内容

+ +`; + +const popup = new Popup({ + offsets: [0, 10], +}) + .setLnglat([120.19, 30.26]) + .setDOMContent(el); + +scene.addPopup(popup); +``` + +### 显示图表的弹窗 + +```javascript +import { Popup } from '@antv/l7'; +import { Chart } from '@antv/g2'; + +layer.on('click', (e) => { + const { name, data } = e.feature.properties; + + // 创建容器 + const container = document.createElement('div'); + container.style.width = '400px'; + container.style.height = '300px'; + + const popup = new Popup({ + offsets: [0, 10], + closeButton: true, + }) + .setLnglat(e.lngLat) + .setDOMContent(container); + + scene.addPopup(popup); + + // 在弹窗中渲染图表 + const chart = new Chart({ + container: container, + autoFit: true, + }); + + chart.data(data); + chart.interval().position('month*value'); + chart.render(); +}); +``` + +### 只允许显示一个弹窗 + +```javascript +let currentPopup = null; + +layer.on('click', (e) => { + // 关闭之前的弹窗 + if (currentPopup) { + currentPopup.remove(); + } + + // 创建新弹窗 + currentPopup = new Popup({ + offsets: [0, 10], + }) + .setLnglat(e.lngLat) + .setHTML(`
${e.feature.properties.name}
`); + + scene.addPopup(currentPopup); +}); +``` + +### 固定位置的说明弹窗 + +```javascript +// 在地图右上角显示固定说明 +const infoPopup = new Popup({ + anchor: 'top-left', + closeButton: false, + closeOnClick: false, +}).setLnglat([120.25, 30.3]).setHTML(` +
+

数据说明

+

数据来源: XXX

+

更新时间: 2024-01-01

+
+ `); + +scene.addPopup(infoPopup); +``` + +## 配置参数 + +### 构造函数参数 + +| 参数 | 类型 | 默认值 | 说明 | +| ----------------- | ---------------- | -------- | ---------------- | +| `closeButton` | boolean | true | 是否显示关闭按钮 | +| `closeOnClick` | boolean | true | 点击地图是否关闭 | +| `closeOnEsc` | boolean | true | 按 ESC 是否关闭 | +| `maxWidth` | string | '240px' | 最大宽度 | +| `offsets` | [number, number] | [0, 0] | 偏移量 [x, y] | +| `anchor` | string | 'bottom' | 锚点位置 | +| `className` | string | '' | 自定义 CSS 类名 | +| `stopPropagation` | boolean | true | 是否阻止事件传播 | + +### 锚点位置 (anchor) + +- `center`: 中心 +- `top`: 上方 +- `bottom`: 下方 +- `left`: 左侧 +- `right`: 右侧 +- `top-left`: 左上 +- `top-right`: 右上 +- `bottom-left`: 左下 +- `bottom-right`: 右下 + +```javascript +const popup = new Popup({ + anchor: 'top', // 弹窗出现在点的上方 +}); +``` + +## 实例方法 + +| 方法 | 说明 | 示例 | +| ------------------- | -------------- | ---------------------------------- | +| `setLnglat(lnglat)` | 设置位置 | `popup.setLnglat([120, 30])` | +| `setHTML(html)` | 设置 HTML 内容 | `popup.setHTML('
内容
')` | +| `setText(text)` | 设置纯文本内容 | `popup.setText('文本内容')` | +| `setDOMContent(el)` | 设置 DOM 元素 | `popup.setDOMContent(element)` | +| `remove()` | 移除弹窗 | `popup.remove()` | +| `isOpen()` | 是否打开 | `popup.isOpen()` | +| `addTo(scene)` | 添加到场景 | `popup.addTo(scene)` | + +## 常见问题 + +### 1. 弹窗位置不对 + +**原因**: 偏移量设置不合适 + +**解决方案**: + +```javascript +const popup = new Popup({ + offsets: [0, -20], // 向上偏移 20 像素 + anchor: 'bottom', // 设置锚点位置 +}); +``` + +### 2. 弹窗内容被截断 + +**原因**: 宽度限制 + +**解决方案**: + +```javascript +const popup = new Popup({ + maxWidth: '400px' // 增加最大宽度 +}); + +// 或使用 CSS +.l7-popup { + max-width: 400px !important; +} +``` + +### 3. 弹窗事件冲突 + +**问题**: 点击弹窗内的按钮触发了地图事件 + +**解决方案**: + +```javascript +const popup = new Popup({ + stopPropagation: true, // 阻止事件传播 +}); +``` + +### 4. 多个弹窗管理 + +**问题**: 同时显示多个弹窗,需要统一管理 + +**解决方案**: + +```javascript +class PopupManager { + constructor() { + this.popups = []; + } + + add(popup) { + this.popups.push(popup); + } + + removeAll() { + this.popups.forEach((popup) => popup.remove()); + this.popups = []; + } + + removeLast() { + const popup = this.popups.pop(); + if (popup) { + popup.remove(); + } + } +} + +const manager = new PopupManager(); + +layer.on('click', (e) => { + const popup = new Popup().setLnglat(e.lngLat).setHTML(`
${e.feature.properties.name}
`); + + scene.addPopup(popup); + manager.add(popup); +}); + +// 清除所有弹窗 +manager.removeAll(); +``` + +## 高级用法 + +### 响应式弹窗 + +```javascript +function createResponsivePopup(data, lnglat) { + const isMobile = window.innerWidth < 768; + + const popup = new Popup({ + maxWidth: isMobile ? '200px' : '400px', + offsets: isMobile ? [0, 5] : [0, 10], + }).setLnglat(lnglat).setHTML(` +
+

${data.name}

+

${data.value}

+
+ `); + + return popup; +} +``` + +### 带加载状态的弹窗 + +```javascript +layer.on('click', async (e) => { + // 显示加载状态 + const popup = new Popup({ + offsets: [0, 10], + }) + .setLnglat(e.lngLat) + .setHTML('
加载中...
'); + + scene.addPopup(popup); + + // 异步加载数据 + try { + const data = await fetchDetailData(e.feature.properties.id); + + // 更新内容 + popup.setHTML(` +
+

${data.name}

+

${data.description}

+
+ `); + } catch (error) { + popup.setHTML('
加载失败
'); + } +}); +``` + +## 相关技能 + +- [场景初始化](../core/scene.md) +- [事件处理](./events.md) +- [标注组件](./components.md) +- [点图层](../layers/point.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - Popup](https://l7.antv.antgroup.com/examples/component/popup) diff --git a/skills/l7/references/layers/base-layer.md b/skills/l7/references/layers/base-layer.md new file mode 100644 index 0000000..531d9a4 --- /dev/null +++ b/skills/l7/references/layers/base-layer.md @@ -0,0 +1,698 @@ +--- +skill_id: layer-common-api +skill_name: 图层通用方法和事件 +category: layers +difficulty: beginner +tags: + [ + layer-api, + layer-methods, + layer-events, + layer-common, + show, + hide, + visible, + setIndex, + fitBounds, + zoom, + click, + mousemove, + mouseout, + hover, + contextmenu, + source, + scale, + filter, + event, + active, + select, + 通用方法, + 图层控制, + 鼠标事件, + 数据方法, + ] +type: reference +dependencies: [scene-initialization] +applies_to: [point, line, polygon, heatmap, image, raster, tile-vector] +related_skills: [point, line, polygon, heatmap, events] +version: 2.x +--- + +# 图层通用方法和事件 + +## 技能描述 + +掌握 L7 所有图层通用的方法和事件。本文档是参考手册,详细说明了 PointLayer、LineLayer、PolygonLayer 等所有图层类型共享的核心 API,包括显示控制、数据管理、事件监听等能力。 + +> 💡 **使用提示**:这是通用 API 参考文档。使用具体图层时,请查看对应的图层文档(如 [点图层](./point.md)、[线图层](./line.md)),它们会介绍图层特有功能并引用本文档的通用能力。 + +## 何时使用 + +- ✅ 需要控制图层的显示和隐藏 +- ✅ 需要调整图层的绘制顺序 +- ✅ 需要监听图层的鼠标事件 +- ✅ 需要动态更新图层数据或样式 +- ✅ 需要获取图层的状态和属性 +- ✅ 需要聚合数据或使用数据转换 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 了解基本的图层类型([点图层](./point.md)、[线图层](./line.md)等) + +## 核心概念 + +### 图层语法 + +L7 图层遵循图形语法,提供链式调用API: + +```javascript +const layer = new PointLayer(options) + .source(data, config) // 设置数据源 + .scale(field, scaleConfig) // 设置数据映射 + .filter(callback) // 数据过滤 + .shape(field, values) // 设置形状 + .color(field, colors) // 设置颜色 + .size(field, sizes) // 设置大小 + .texture(field, textures) // 设置纹理 + .animate(options) // 设置动画 + .active(options) // 设置高亮 + .select(options) // 设置选中 + .style(options); // 设置样式 + +scene.addLayer(layer); +``` + +## 图层控制方法 + +### show(): void + +显示图层。 + +```javascript +layer.show(); +``` + +### hide(): void + +隐藏图层。 + +```javascript +layer.hide(); +``` + +**示例:切换图层显示** + +```javascript +let isVisible = true; + +function toggleLayer() { + if (isVisible) { + layer.hide(); + } else { + layer.show(); + } + isVisible = !isVisible; +} + +// 按钮点击事件 +document.getElementById('toggle-btn').addEventListener('click', toggleLayer); +``` + +### isVisible(): boolean + +检查图层是否可见。 + +```javascript +if (layer.isVisible()) { + console.log('图层可见'); +} else { + console.log('图层隐藏'); +} +``` + +### setIndex(zIndex: number): void + +设置图层绘制顺序,数值越大越在上层。 + +```javascript +// 设置图层在最上层 +layer.setIndex(999); + +// 设置图层在底层 +layer.setIndex(1); +``` + +**示例:图层分层管理** + +```javascript +// 底层:区域底色 +polygonLayer.setIndex(1); + +// 中层:道路 +lineLayer.setIndex(2); + +// 顶层:POI 标注 +pointLayer.setIndex(3); +``` + +### fitBounds(fitBoundsOptions?: IFitBoundsOptions): void + +缩放地图至图层数据范围。 + +```javascript +// 基础用法 +layer.fitBounds(); + +// 带参数 +layer.fitBounds({ + padding: 50, // 边距(像素) +}); +``` + +**示例:加载数据后自动适配范围** + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9'); + +scene.addLayer(layer); + +scene.on('loaded', () => { + // 自动缩放到数据范围 + layer.fitBounds(); +}); +``` + +### setMinZoom(zoom: number): void + +设置图层最小缩放等级(小于此等级时不显示)。 + +```javascript +// 地图缩放级别小于 10 时不显示该图层 +layer.setMinZoom(10); +``` + +### setMaxZoom(zoom: number): void + +设置图层最大缩放等级(大于此等级时不显示)。 + +```javascript +// 地图缩放级别大于 18 时不显示该图层 +layer.setMaxZoom(18); +``` + +**示例:根据缩放级别切换图层** + +```javascript +// 小级别显示聚合数据 +clusterLayer.setMaxZoom(12); + +// 大级别显示详细数据 +detailLayer.setMinZoom(12); + +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + console.log(`当前缩放级别: ${zoom}`); +}); +``` + +## 数据方法 + +### source(data, config): Layer + +设置图层数据源和解析配置。 + +```javascript +layer.source(data, { + parser: { + type: 'json', // 数据类型: json | geojson | csv + x: 'lng', // 经度字段 + y: 'lat', // 纬度字段 + }, + transforms: [ + // 数据转换(可选) + { + type: 'map', + callback: (item) => { + item.value = item.value * 100; + return item; + }, + }, + ], +}); +``` + +**支持的数据格式**: + +- GeoJSON - [详见文档](../data/source-geojson.md) +- JSON - [详见文档](../data/source-json.md) +- CSV - [详见文档](../data/source-csv.md) + +### scale(field, scaleOptions): Layer + +设置数据字段的映射规则。 + +```javascript +layer.scale('value', { + type: 'linear', // scale 类型 + domain: [0, 100], // 数据值域 +}); +``` + +**Scale 类型**: + +| 类型 | 适用数据 | 说明 | +| --------- | -------- | ---------------- | +| linear | 连续数值 | 线性映射 | +| log | 连续数值 | 对数映射 | +| pow | 连续数值 | 幂次映射 | +| quantize | 连续数值 | 等间距分类 | +| quantile | 连续数值 | 分位数分类 | +| threshold | 连续数值 | 自定义阈值分类 | +| diverging | 连续数值 | 发散分类(双色) | +| cat | 分类数据 | 类别映射 | +| identity | 任意 | 值即映射结果 | + +**示例**: + +```javascript +// 线性映射 +layer.scale('population', { + type: 'linear', + domain: [0, 10000000], +}); + +// 分类映射 +layer.scale('category', { + type: 'cat', + domain: ['A', 'B', 'C'], +}); + +// 阈值分类 +layer.scale('aqi', { + type: 'threshold', + domain: [50, 100, 150, 200, 300], // 5个阈值,需要6个颜色 +}); +``` + +详细说明参见 [视觉映射](../visual/mapping.md)。 + +### filter(callback): Layer + +数据过滤,返回 true 的数据会被显示。 + +```javascript +// 只显示值大于 100 的数据 +layer.filter((feature) => { + return feature.value > 100; +}); + +// 根据类型过滤 +layer.filter((feature) => { + return ['A', 'B'].includes(feature.type); +}); +``` + +### getScale(scaleName: string): IScale + +获取指定字段的 scale 实例。 + +```javascript +const valueScale = layer.getScale('value'); +console.log('值域:', valueScale.domain); +console.log('映射范围:', valueScale.range); +``` + +## 数据聚合方法 + +### cluster 聚合配置 + +使用聚合功能时,可通过以下方法获取聚合数据: + +#### getClusters(zoom: number): IFeatureCollection + +获取指定缩放等级的聚合数据。 + +```javascript +const source = layer.getSource(); +const clusters = source.getClusters(10); // 获取 zoom=10 的聚合数据 +console.log('聚合节点数量:', clusters.features.length); +``` + +#### getClustersLeaves(id: string): IFeatureCollection + +获取聚合节点包含的原始数据。 + +```javascript +const source = layer.getSource(); + +layer.on('click', (e) => { + if (e.feature.cluster) { + // 获取聚合节点的原始数据 + const leaves = source.getClustersLeaves(e.feature.cluster_id); + console.log('该聚合包含数据:', leaves); + } +}); +``` + +## 鼠标事件 + +所有图层支持的鼠标事件: + +### 基础鼠标事件 + +```javascript +// 点击事件 +layer.on('click', (e) => { + console.log('点击位置:', e.lngLat); + console.log('点击要素:', e.feature); +}); + +// 双击事件 +layer.on('dblclick', (e) => { + console.log('双击要素:', e.feature); +}); + +// 鼠标移动 +layer.on('mousemove', (e) => { + // 高频事件,注意性能 +}); + +// 鼠标移出 +layer.on('mouseout', (e) => { + console.log('鼠标移出'); +}); + +// 鼠标按下 +layer.on('mousedown', (e) => { + console.log('鼠标按下'); +}); + +// 鼠标抬起 +layer.on('mouseup', (e) => { + console.log('鼠标抬起'); +}); + +// 右键菜单 +layer.on('contextmenu', (e) => { + e.preventDefault(); // 阻止默认菜单 + console.log('右键点击:', e.lngLat); +}); +``` + +### 未拾取事件(Unpick Events) + +当鼠标操作未选中图层要素时触发: + +```javascript +// 点击图层外 +layer.on('unclick', (e) => { + console.log('点击了图层外的区域'); +}); + +// 图层外移动 +layer.on('unmousemove', (e) => { + // 鼠标在图层外移动 +}); + +// 图层外鼠标抬起 +layer.on('unmouseup', (e) => {}); + +// 图层外鼠标按下 +layer.on('unmousedown', (e) => {}); + +// 图层外右键 +layer.on('uncontextmenu', (e) => {}); + +// 所有未拾取事件 +layer.on('unpick', (e) => { + console.log('所有图层外的操作'); +}); +``` + +### 移动端事件 + +```javascript +// 触摸开始 +layer.on('touchstart', (e) => { + console.log('触摸开始'); +}); + +// 触摸结束 +layer.on('touchend', (e) => { + console.log('触摸结束'); +}); +``` + +### 事件参数 + +所有鼠标事件回调参数包含: + +```typescript +interface ILayerMouseEvent { + x: number; // 鼠标在地图位置 x 坐标 + y: number; // 鼠标在地图位置 y 坐标 + type: string; // 事件类型 + lngLat: ILngLat; // 经纬度 { lng, lat } + feature: any; // 选中的要素数据 + featureId: number | null; // 要素 ID +} +``` + +## 实际应用场景 + +### 1. 图层高亮和弹窗 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9') + .active(true); // 开启高亮 + +// 鼠标移入显示信息 +layer.on('mousemove', (e) => { + const { feature } = e; + const popup = new Popup({ + offsets: [0, 20], + }).setLnglat(e.lngLat).setHTML(` +
+

${feature.name}

+

值: ${feature.value}

+
+ `); + scene.addPopup(popup); +}); + +// 鼠标移出关闭弹窗 +layer.on('mouseout', () => { + scene.removeAllPopup(); +}); +``` + +### 2. 点击查看详情 + +```javascript +layer.on('click', (e) => { + const { feature } = e; + + // 显示详情面板 + showDetailPanel({ + name: feature.name, + address: feature.address, + phone: feature.phone, + }); + + // 高亮选中的点 + layer.select(true); +}); +``` + +### 3. 根据缩放级别切换图层 + +```javascript +// 创建两个图层 +const clusterLayer = new PointLayer({ name: 'cluster' }) + .source(clusterData) + .shape('circle') + .size('count', [20, 50]) + .color('count', ['#ffffcc', '#800026']) + .setMaxZoom(12); // 小于 12 级显示 + +const detailLayer = new PointLayer({ name: 'detail' }) + .source(detailData) + .shape('circle') + .size(8) + .color('#5B8FF9') + .setMinZoom(12) // 大于 12 级显示 + .hide(); // 初始隐藏 + +scene.addLayer(clusterLayer); +scene.addLayer(detailLayer); + +// 监听缩放变化 +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + if (zoom >= 12) { + clusterLayer.hide(); + detailLayer.show(); + } else { + clusterLayer.show(); + detailLayer.hide(); + } +}); +``` + +### 4. 数据过滤和更新 + +```javascript +const layer = new PointLayer().source(data).shape('circle').size(10).color('category', colorMap); + +// 动态过滤数据 +function filterByCategory(categories) { + layer.filter((feature) => { + return categories.includes(feature.category); + }); + scene.render(); // 触发重绘 +} + +// 过滤按钮 +document.getElementById('filter-A').addEventListener('click', () => { + filterByCategory(['A', 'B']); +}); +``` + +### 5. 聚合数据展示 + +```javascript +const layer = new PointLayer() + .source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + cluster: true, + clusterOption: { + radius: 40, + minZoom: 0, + maxZoom: 16, + }, + }) + .shape('circle') + .size('point_count', [20, 60]) + .color('point_count', ['#ffffcc', '#800026']); + +// 点击聚合节点查看包含的数据 +layer.on('click', (e) => { + const source = layer.getSource(); + + if (e.feature.cluster) { + // 获取聚合节点的原始数据 + const leaves = source.getClustersLeaves(e.feature.cluster_id); + console.log('包含的数据:', leaves); + + // 显示列表 + showDataList(leaves); + } +}); +``` + +## 常见问题 + +### Q: 如何获取图层对象? + +A: 通过 Scene 的方法获取: + +```javascript +// 通过 ID +const layer = scene.getLayer('layer-id'); + +// 通过名称 +const layer = scene.getLayerByName('my-layer'); + +// 获取所有图层 +const layers = scene.getLayers(); +``` + +### Q: 图层顺序如何控制? + +A: 使用 `setIndex()` 方法,数值越大越在上层: + +```javascript +bottomLayer.setIndex(1); +middleLayer.setIndex(2); +topLayer.setIndex(3); +``` + +### Q: 如何监听图层加载完成? + +A: 图层添加到 Scene 后会自动加载,监听 Scene 的 `loaded` 事件: + +```javascript +scene.on('loaded', () => { + console.log('所有图层加载完成'); +}); +``` + +### Q: 鼠标事件中如何获取原始数据? + +A: 通过事件参数的 `feature` 属性: + +```javascript +layer.on('click', (e) => { + console.log('原始数据:', e.feature.properties); + console.log('坐标:', e.feature.geometry.coordinates); +}); +``` + +### Q: 如何移除事件监听? + +A: 使用 `off()` 方法: + +```javascript +const handleClick = (e) => { + console.log('点击:', e); +}; + +// 绑定事件 +layer.on('click', handleClick); + +// 移除事件 +layer.off('click', handleClick); +``` + +### Q: filter 过滤后如何重置? + +A: 传入返回 true 的函数即可: + +```javascript +// 重置过滤(显示所有数据) +layer.filter(() => true); +scene.render(); +``` + +## 注意事项 + +⚠️ **事件顺序**:先添加图层到 Scene,再绑定事件 + +⚠️ **性能优化**:`mousemove` 是高频事件,避免在回调中执行复杂计算 + +⚠️ **图层销毁**:使用 `scene.removeLayer(layer)` 会自动销毁图层并移除事件 + +⚠️ **坐标系统**:确保数据坐标系统与地图匹配 + +⚠️ **Scale 配置**:scale 要在 color/size 之前调用 + +## 相关技能 + +- [点图层](./point.md) +- [线图层](./line.md) +- [面图层](./polygon.md) +- [视觉映射](../visual/mapping.md) +- [事件处理](../interaction/events.md) + +## 在线示例 + +查看更多示例:[L7 官方示例](https://l7.antv.antgroup.com/examples) diff --git a/skills/l7/references/layers/heatmap.md b/skills/l7/references/layers/heatmap.md new file mode 100644 index 0000000..cffcc32 --- /dev/null +++ b/skills/l7/references/layers/heatmap.md @@ -0,0 +1,465 @@ +--- +skill_id: heatmap-layer +skill_name: 热力图层 +category: layers +difficulty: beginner +tags: [heatmap, layer, density, heat-visualization, intensity] +dependencies: [scene-initialization, source-geojson] +version: 2.x +--- + +# 热力图层 + +## 技能描述 + +使用颜色梯度展示地理空间数据的密度或强度分布,适合表现数据的聚集程度和热度区域。 + +## 何时使用 + +- ✅ 显示数据密度分布(人口密度、POI 密度) +- ✅ 展示热点区域(犯罪热点、事故高发区) +- ✅ 可视化强度分布(温度分布、污染程度) +- ✅ 分析空间聚集模式(用户活跃区域、订单集中区) +- ✅ 网格热力图(蜂窝热力图、方格热力图) + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好点位数据(包含经纬度和权重值) + +## 输入参数 + +### 数据格式 + +```typescript +interface HeatmapData { + lng: number; // 经度 + lat: number; // 纬度 + value?: number; // 权重值(可选,默认为 1) + [key: string]: any; +} +``` + +### 图层配置 + +| 方法 | 参数 | 说明 | +| ---------------------- | -------------------------------- | --------------------------------------- | +| `source(data, config)` | data: 数据数组 | 设置数据源 | +| `shape(type)` | type: 形状类型 | heatmap \| heatmap3D \| hexagon \| grid | +| `size(field, range)` | field: 聚合字段, range: 映射范围 | 设置热力大小 | +| `color(colors)` | colors: 颜色数组 | 设置颜色渐变 | +| `style(config)` | config: 样式对象 | 设置样式参数 | + +## 输出 + +返回 `HeatmapLayer` 实例 + +## 通用方法 + +热力图层继承了所有图层的通用能力: + +### 显示控制 + +```javascript +// 显示/隐藏图层 +heatmapLayer.show(); +heatmapLayer.hide(); + +// 设置图层顺序(热力图通常在上层) +heatmapLayer.setIndex(10); +``` + +### 事件监听 + +```javascript +// 点击热力区域 +heatmapLayer.on('click', (e) => { + console.log('点击位置:', e.lngLat); + console.log('热力值:', e.feature); +}); +``` + +### 缩放范围 + +```javascript +// 只在特定缩放级别显示热力图 +heatmapLayer.setMinZoom(10); // 小于 10 级不显示 +heatmapLayer.setMaxZoom(18); // 大于 18 级不显示 +``` + +> 📖 **完整文档**:查看 [图层通用方法和事件](./layer-common-api.md) 了解所有通用 API。 + +--- + +## 代码示例 + +### 基础用法 - 经典热力图 + +```javascript +import { HeatmapLayer } from '@antv/l7'; + +const data = [ + { lng: 120.19, lat: 30.26, value: 100 }, + { lng: 120.2, lat: 30.27, value: 200 }, + { lng: 120.21, lat: 30.28, value: 150 }, + { lng: 120.19, lat: 30.29, value: 300 }, +]; + +scene.on('loaded', () => { + const heatmapLayer = new HeatmapLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('heatmap') + .size('value', [0, 1]) + .style({ + intensity: 3, // 热力强度 + radius: 20, // 热力半径 + opacity: 1.0, // 透明度 + rampColors: { + colors: ['#2E8AE6', '#69D1AB', '#DAF291', '#FFE234', '#FF7C6A', '#FF4818'], + positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], + }, + }); + + scene.addLayer(heatmapLayer); +}); +``` + +### 3D 热力图 + +```javascript +const heatmap3DLayer = new HeatmapLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'hexagon', + size: 1000, // 六边形网格大小 + field: 'value', + method: 'sum', // 聚合方式:sum | max | min | mean + }, + ], + }) + .shape('heatmap3D') + .size('sum', [0, 600]) // 3D 高度映射 + .color('sum', [ + '#0B1678', + '#1E3EAD', + '#2E8AE6', + '#69D1AB', + '#DAF291', + '#FFE234', + '#FF7C6A', + '#FF4818', + ]) + .style({ + coverage: 0.9, // 覆盖度 + angle: 0, // 旋转角度 + opacity: 1.0, + }); + +scene.addLayer(heatmap3DLayer); +``` + +### 蜂窝热力图 + +```javascript +const hexagonLayer = new HeatmapLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'hexagon', + size: 500, // 六边形大小(米) + field: 'value', + method: 'sum', + }, + ], + }) + .shape('hexagon') + .color('sum', ['#ffffcc', '#c7e9b4', '#7fcdbb', '#41b6c4', '#2c7fb8', '#253494']) + .style({ + coverage: 0.8, + angle: 0, + }); + +scene.addLayer(hexagonLayer); +``` + +### 方格热力图 + +```javascript +const gridLayer = new HeatmapLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'grid', + size: 1000, // 方格大小(米) + field: 'value', + method: 'sum', + }, + ], + }) + .shape('square') + .color('sum', ['#feedde', '#fdd0a2', '#fdae6b', '#fd8d3c', '#e6550d', '#a63603']) + .style({ + coverage: 1, // 完全覆盖 + }); + +scene.addLayer(gridLayer); +``` + +### 自定义配置 - 高级用法 + +```javascript +const advancedHeatmap = new HeatmapLayer({ + name: 'custom-heatmap', + blend: 'additive', // 混合模式 +}) + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('heatmap') + .size('value', (value) => { + // 自定义大小映射函数 + return Math.sqrt(value) / 100; + }) + .style({ + intensity: 2, + radius: 25, + opacity: 0.8, + rampColors: { + colors: [ + 'rgba(33,102,172,0)', + 'rgb(103,169,207)', + 'rgb(209,229,240)', + 'rgb(253,219,199)', + 'rgb(239,138,98)', + 'rgb(178,24,43)', + ], + positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], + }, + }); + +scene.addLayer(advancedHeatmap); +``` + +## 样式配置详解 + +### 经典热力图样式 + +```javascript +{ + intensity: 3, // 热力强度,值越大颜色越浓,范围 1-10 + radius: 20, // 热力半径(像素),影响扩散范围 + opacity: 1.0, // 整体透明度,0-1 + rampColors: { + colors: [...], // 颜色数组,从低到高 + positions: [...] // 颜色位置,0-1,需与 colors 对应 + } +} +``` + +### 3D/网格热力图样式 + +```javascript +{ + coverage: 0.9, // 覆盖度,0-1,控制网格间距 + angle: 0, // 旋转角度,0-360 + opacity: 1.0 // 透明度 +} +``` + +## 常见场景 + +### 1. 人口密度分析 + +```javascript +const populationHeatmap = new HeatmapLayer() + .source(populationData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('heatmap') + .size('population', [0, 1]) + .style({ + intensity: 3, + radius: 30, + rampColors: { + colors: [ + '#FFFFCC', + '#FFEDA0', + '#FED976', + '#FEB24C', + '#FD8D3C', + '#FC4E2A', + '#E31A1C', + '#BD0026', + '#800026', + ], + positions: [0, 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.85, 1.0], + }, + }); +``` + +### 2. 实时订单热力 + +```javascript +const orderHeatmap = new HeatmapLayer() + .source(orderData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('heatmap3D') + .size('order_count', [0, 500]) + .color('order_count', ['#440154', '#3b528b', '#21908c', '#5dc863', '#fde725']) + .style({ + coverage: 0.8, + opacity: 0.8, + }); + +// 定时更新数据 +setInterval(() => { + fetch('/api/realtime-orders') + .then((res) => res.json()) + .then((newData) => { + orderHeatmap.setData(newData); + }); +}, 5000); +``` + +### 3. 交通事故热点 + +```javascript +const accidentHeatmap = new HeatmapLayer() + .source(accidentData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + transforms: [ + { + type: 'hexagon', + size: 500, + field: 'severity', + method: 'max', + }, + ], + }) + .shape('hexagon') + .color('max', [ + '#f7fbff', + '#deebf7', + '#c6dbef', + '#9ecae1', + '#6baed6', + '#4292c6', + '#2171b5', + '#084594', + ]); +``` + +## 性能优化 + +### 1. 数据量优化 + +```javascript +// 大数据量时使用网格聚合 +const layer = new HeatmapLayer() + .source(bigData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + transforms: [ + { + type: 'grid', + size: 2000, // 增大网格减少计算量 + field: 'value', + method: 'sum', + }, + ], + }) + .shape('square'); +``` + +### 2. 降低渲染精度 + +```javascript +layer.style({ + radius: 15, // 减小半径 + intensity: 2, // 降低强度 + opacity: 0.8, +}); +``` + +### 3. 使用抽样 + +```javascript +// 数据抽样,适合超大数据集 +const sampledData = data.filter((_, index) => index % 10 === 0); + +layer.source(sampledData, { + /* config */ +}); +``` + +## 注意事项 + +⚠️ **数据量**:经典热力图建议数据量在 10 万以内,超过建议使用网格热力图 + +⚠️ **颜色位置**:`rampColors.positions` 必须是递增的 0-1 数组,长度需与 colors 对应 + +⚠️ **半径设置**:`radius` 过大会导致性能下降,建议 10-50 像素 + +⚠️ **权重字段**:如果数据没有权重字段,系统会为每个点赋予默认权重 1 + +⚠️ **坐标系统**:确保经纬度数据正确,经度范围 -180~180,纬度范围 -90~90 + +## 常见问题 + +### Q: 热力图颜色不明显? + +A: 增加 `intensity` 参数或增大 `radius` 值 + +### Q: 3D 热力图高度不明显? + +A: 调整 `size()` 方法的映射范围,如 `[0, 1000]` + +### Q: 网格热力图出现空洞? + +A: 增大 `coverage` 参数至 1.0,或减小网格 `size` + +### Q: 数据更新后热力图不刷新? + +A: 使用 `layer.setData(newData)` 或 `scene.render()` + +### Q: 热力图边界被裁切? + +A: 调整地图的 `padding` 或使用 `fitBounds()` 方法 + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [点图层](./point.md) +- [面图层](./polygon.md) +- [数据映射](../visual/mapping.md) +- [数据聚合](../data/source-parser.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 热力图](https://l7.antv.antgroup.com/examples/heatmap/heatmap) diff --git a/skills/l7/references/layers/image.md b/skills/l7/references/layers/image.md new file mode 100644 index 0000000..28aae28 --- /dev/null +++ b/skills/l7/references/layers/image.md @@ -0,0 +1,577 @@ +--- +skill_id: image-layer +skill_name: 图片图层 +category: layers +difficulty: beginner +tags: [image, layer, raster, overlay, photo] +dependencies: [scene-initialization] +version: 2.x +--- + +# 图片图层 + +## 技能描述 + +在地图上叠加显示图片,支持卫星图、航拍图、扫描地图、历史地图等图片的精确定位和显示。 + +## 何时使用 + +- ✅ 显示卫星遥感图片(卫星影像、航拍照片) +- ✅ 叠加历史地图(古地图对比、历史影像) +- ✅ 显示扫描文档(建筑平面图、工程图纸) +- ✅ 展示分析结果图(热力分析图、风险区域图) +- ✅ 自定义底图(特殊区域地图、室内地图) + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好图片 URL 或 Base64 数据 +- 确定图片的地理边界坐标(四个角点) + +## 输入参数 + +### 数据格式 + +```typescript +interface ImageData { + extent: [number, number, number, number]; // [minLng, minLat, maxLng, maxLat] + // 或使用四个角点 + coordinates: [ + [number, number], // 左上角 [lng, lat] + [number, number], // 右上角 [lng, lat] + [number, number], // 右下角 [lng, lat] + [number, number], // 左下角 [lng, lat] + ]; +} +``` + +### 图层配置 + +| 方法 | 参数 | 说明 | +| --------------------- | --------------------------- | -------------- | +| `source(url, config)` | url: 图片地址, config: 配置 | 设置图片数据源 | +| `shape(type)` | type: 形状类型 | image(默认) | +| `style(config)` | config: 样式对象 | 设置样式 | + +## 输出 + +返回 `ImageLayer` 实例 + +## 通用方法 + +图片图层继承了所有图层的通用能力: + +### 显示控制 + +```javascript +// 显示/隐藏图层 +imageLayer.show(); +imageLayer.hide(); + +// 检查可见性 +if (imageLayer.isVisible()) { + console.log('图片可见'); +} + +// 设置图层顺序 +imageLayer.setIndex(5); +``` + +### 事件监听 + +```javascript +// 点击图片区域 +imageLayer.on('click', (e) => { + console.log('点击位置:', e.lngLat); +}); +``` + +### 缩放和范围 + +```javascript +// 缩放到图片范围 +imageLayer.fitBounds(); + +// 设置显示的缩放范围 +imageLayer.setMinZoom(8); +imageLayer.setMaxZoom(16); +``` + +> 📖 **完整文档**:查看 [图层通用方法和事件](./layer-common-api.md) 了解所有通用 API。 + +--- + +## 代码示例 + +### 基础用法 - extent 方式 + +```javascript +import { ImageLayer } from '@antv/l7'; + +scene.on('loaded', () => { + const imageLayer = new ImageLayer() + .source('https://gw.alipayobjects.com/zos/rmsportal/FnHFeFklTzKDdUESRNDv.jpg', { + parser: { + type: 'image', + extent: [121.168, 30.2828, 121.384, 30.4219], // [西, 南, 东, 北] + }, + }) + .style({ + opacity: 1.0, + }); + + scene.addLayer(imageLayer); +}); +``` + +### 四角点定位方式 + +```javascript +const imageLayer = new ImageLayer().source('https://example.com/aerial-photo.jpg', { + parser: { + type: 'image', + coordinates: [ + [121.168, 30.4219], // 左上角 [经度, 纬度] + [121.384, 30.4219], // 右上角 + [121.384, 30.2828], // 右下角 + [121.168, 30.2828], // 左下角 + ], + }, +}); + +scene.addLayer(imageLayer); +``` + +### Base64 图片 + +```javascript +const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANS...'; + +const imageLayer = new ImageLayer() + .source(base64Image, { + parser: { + type: 'image', + extent: [120.0, 30.0, 121.0, 31.0], + }, + }) + .style({ + opacity: 0.8, + }); + +scene.addLayer(imageLayer); +``` + +### 多张图片叠加 + +```javascript +const images = [ + { + url: 'https://example.com/layer1.png', + extent: [121.0, 30.0, 122.0, 31.0], + opacity: 0.5, + }, + { + url: 'https://example.com/layer2.png', + extent: [121.0, 30.0, 122.0, 31.0], + opacity: 0.7, + }, +]; + +images.forEach((img, index) => { + const layer = new ImageLayer({ name: `image-${index}` }) + .source(img.url, { + parser: { + type: 'image', + extent: img.extent, + }, + }) + .style({ + opacity: img.opacity, + }); + + scene.addLayer(layer); +}); +``` + +### 动态更新图片 + +```javascript +const imageLayer = new ImageLayer().source('https://example.com/image1.jpg', { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, +}); + +scene.addLayer(imageLayer); + +// 切换图片 +function updateImage(newUrl) { + imageLayer.setData(newUrl, { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, + }); +} + +// 使用示例 +updateImage('https://example.com/image2.jpg'); +``` + +### 带交互的图片图层 + +```javascript +const imageLayer = new ImageLayer() + .source('https://example.com/floor-plan.png', { + parser: { + type: 'image', + extent: [121.4737, 31.2304, 121.4837, 31.2404], + }, + }) + .style({ + opacity: 0.8, + }); + +// 点击事件 +imageLayer.on('click', (e) => { + console.log('点击位置:', e.lngLat); + + // 显示信息弹窗 + const popup = new L7.Popup({ + offsets: [0, 0], + closeButton: false, + }) + .setLnglat(e.lngLat) + .setHTML(`
坐标: ${e.lngLat.lng.toFixed(4)}, ${e.lngLat.lat.toFixed(4)}
`) + .addTo(scene); +}); + +// 鼠标悬停改变透明度 +imageLayer.on('mousemove', () => { + imageLayer.style({ opacity: 1.0 }); + scene.render(); +}); + +imageLayer.on('mouseout', () => { + imageLayer.style({ opacity: 0.8 }); + scene.render(); +}); + +scene.addLayer(imageLayer); +``` + +## 样式配置详解 + +### 基础样式 + +```javascript +{ + opacity: 1.0, // 透明度,0-1,默认 1.0 + clampLow: true, // 是否裁剪低于最小值的部分 + clampHigh: true, // 是否裁剪高于最大值的部分 + domain: [0, 1], // 数据值域范围 + rampColors: { // 颜色映射(用于灰度图) + colors: [...], + positions: [...] + } +} +``` + +## 常见场景 + +### 1. 卫星影像叠加 + +```javascript +// 叠加高清卫星图 +const satelliteLayer = new ImageLayer() + .source('https://api.example.com/satellite/tile.jpg', { + parser: { + type: 'image', + extent: [116.3, 39.9, 116.5, 40.1], // 北京区域 + }, + }) + .style({ + opacity: 0.8, + }); + +scene.addLayer(satelliteLayer); + +// 添加滑动条控制透明度 +const slider = document.getElementById('opacity-slider'); +slider.addEventListener('input', (e) => { + const opacity = parseFloat(e.target.value); + satelliteLayer.style({ opacity }); + scene.render(); +}); +``` + +### 2. 历史地图对比 + +```javascript +let showHistorical = false; + +// 当前地图(默认显示) +const currentMap = new ImageLayer({ name: 'current' }) + .source('https://example.com/current-map.jpg', { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, + }) + .style({ opacity: 1.0 }); + +// 历史地图(初始隐藏) +const historicalMap = new ImageLayer({ name: 'historical' }) + .source('https://example.com/historical-map.jpg', { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, + }) + .style({ opacity: 0.0 }); + +scene.addLayer(currentMap); +scene.addLayer(historicalMap); + +// 切换按钮 +document.getElementById('toggle-btn').addEventListener('click', () => { + showHistorical = !showHistorical; + + currentMap.style({ opacity: showHistorical ? 0 : 1 }); + historicalMap.style({ opacity: showHistorical ? 1 : 0 }); + scene.render(); +}); +``` + +### 3. 建筑平面图 + +```javascript +// 室内平面图 +const floorPlan = new ImageLayer() + .source('https://example.com/floor-1.png', { + parser: { + type: 'image', + coordinates: [ + [121.4737, 31.2404], // 左上 + [121.4837, 31.2404], // 右上 + [121.4837, 31.2304], // 右下 + [121.4737, 31.2304], // 左下 + ], + }, + }) + .style({ + opacity: 0.9, + }); + +scene.addLayer(floorPlan); + +// 楼层切换 +const floors = { + '1F': 'https://example.com/floor-1.png', + '2F': 'https://example.com/floor-2.png', + '3F': 'https://example.com/floor-3.png', +}; + +function switchFloor(floorName) { + floorPlan.setData(floors[floorName], { + parser: { + type: 'image', + coordinates: [ + [121.4737, 31.2404], + [121.4837, 31.2404], + [121.4837, 31.2304], + [121.4737, 31.2304], + ], + }, + }); +} +``` + +### 4. 雷达气象图 + +```javascript +// 实时更新的雷达图 +class WeatherRadar { + constructor(scene) { + this.scene = scene; + this.layer = null; + this.updateInterval = null; + } + + start() { + this.layer = new ImageLayer() + .source('', { + parser: { + type: 'image', + extent: [115.0, 28.0, 125.0, 38.0], // 覆盖区域 + }, + }) + .style({ + opacity: 0.7, + }); + + this.scene.addLayer(this.layer); + this.update(); + + // 每5分钟更新一次 + this.updateInterval = setInterval( + () => { + this.update(); + }, + 5 * 60 * 1000, + ); + } + + async update() { + try { + const response = await fetch('/api/weather/radar/latest'); + const data = await response.json(); + + this.layer.setData(data.imageUrl, { + parser: { + type: 'image', + extent: [115.0, 28.0, 125.0, 38.0], + }, + }); + } catch (error) { + console.error('更新雷达图失败:', error); + } + } + + stop() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + if (this.layer) { + this.scene.removeLayer(this.layer); + } + } +} + +// 使用 +const radar = new WeatherRadar(scene); +radar.start(); +``` + +## 性能优化 + +### 1. 图片尺寸优化 + +```javascript +// 使用合适分辨率的图片 +// 根据显示区域大小选择图片 +const mapWidth = scene.getSize().width; +const mapHeight = scene.getSize().height; + +// 推荐图片分辨率不超过显示区域的 2 倍 +const recommendedWidth = mapWidth * 2; +const recommendedHeight = mapHeight * 2; + +// 使用 CDN 图片服务调整尺寸 +const imageUrl = `https://cdn.example.com/image.jpg?w=${recommendedWidth}&h=${recommendedHeight}`; +``` + +### 2. 懒加载 + +```javascript +// 只在需要时加载图片图层 +let imageLayer = null; + +function showImageLayer() { + if (!imageLayer) { + imageLayer = new ImageLayer().source('https://example.com/large-image.jpg', { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, + }); + + scene.addLayer(imageLayer); + } else { + imageLayer.show(); + } +} + +function hideImageLayer() { + if (imageLayer) { + imageLayer.hide(); + } +} +``` + +### 3. 使用 WebP 格式 + +```javascript +// WebP 格式可减少 25-35% 的文件大小 +const imageLayer = new ImageLayer().source('https://example.com/image.webp', { + parser: { + type: 'image', + extent: [121.0, 30.0, 122.0, 31.0], + }, +}); +``` + +## 注意事项 + +⚠️ **坐标顺序**:extent 格式为 `[minLng, minLat, maxLng, maxLat]`(西南东北) + +⚠️ **图片大小**:建议单张图片不超过 5MB,过大会影响加载速度 + +⚠️ **跨域问题**:确保图片服务器配置了正确的 CORS 头 + +⚠️ **坐标精度**:确保图片边界坐标准确,否则会出现偏移或变形 + +⚠️ **透明度**:PNG 格式支持透明度,JPG 不支持 + +⚠️ **更新性能**:频繁更新图片会影响性能,建议控制更新频率 + +## 常见问题 + +### Q: 图片无法显示? + +A: 检查:1) 图片 URL 是否可访问;2) CORS 配置;3) extent 坐标是否正确;4) 图片格式是否支持 + +### Q: 图片位置偏移? + +A: 检查 extent 或 coordinates 的坐标是否准确,注意经纬度顺序 + +### Q: 图片模糊? + +A: 使用更高分辨率的图片,或使用原始尺寸而非缩放后的图片 + +### Q: 图片加载慢? + +A: 1) 压缩图片大小;2) 使用 CDN;3) 使用 WebP 格式;4) 预加载图片 + +### Q: 如何实现图片淡入效果? + +A: 创建图层时设置 opacity: 0,然后逐渐增加到 1 + +```javascript +const layer = new ImageLayer().style({ opacity: 0 }); +scene.addLayer(layer); + +let opacity = 0; +const fadeIn = setInterval(() => { + opacity += 0.05; + if (opacity >= 1) { + opacity = 1; + clearInterval(fadeIn); + } + layer.style({ opacity }); + scene.render(); +}, 50); +``` + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [场景初始化](../core/scene.md) +- [栅格图层](raster.md) +- [瓦片图层](raster.md) +- [事件处理](../interaction/events.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 图片图层](https://l7.antv.antgroup.com/examples/raster/image) diff --git a/skills/l7/references/layers/line.md b/skills/l7/references/layers/line.md new file mode 100644 index 0000000..ee7660a --- /dev/null +++ b/skills/l7/references/layers/line.md @@ -0,0 +1,582 @@ +--- +skill_id: line-layer +skill_name: 线图层 +category: layers +difficulty: beginner +tags: [line, path, arc, route, trajectory] +dependencies: [scene-initialization] +version: 2.x +--- + +# 线图层 + +## 技能描述 + +在地图上绘制线状地理要素,支持路径线、弧线、3D 弧线等多种形式。 + +## 何时使用 + +- ✅ 显示道路、河流等线性要素 +- ✅ 显示轨迹路径(车辆、人员移动) +- ✅ 显示 OD 流向(人口迁徙、物流) +- ✅ 显示航线、航路 +- ✅ 显示行政区划边界 +- ✅ 显示等值线 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好线段数据 + +## 线类型 + +| 类型 | 说明 | 适用场景 | +| ------------- | -------- | ---------------- | +| `line` | 基础直线 | 道路、河流、边界 | +| `arc` | 2D 弧线 | 短距离流向 | +| `arc3d` | 3D 弧线 | 长距离流向、航线 | +| `greatcircle` | 大圆航线 | 跨越半球的航线 | +| `wall` | 墙/幕墙 | 3D 围栏效果 | + +## 通用方法 + +线图层继承了所有图层的通用能力,以下是最常用的方法: + +### 显示控制 + +```javascript +// 显示/隐藏图层 +lineLayer.show(); +lineLayer.hide(); + +// 设置图层绘制顺序 +lineLayer.setIndex(5); + +// 适配到数据范围 +lineLayer.fitBounds(); +``` + +### 事件监听 + +```javascript +// 点击线段 +lineLayer.on('click', (e) => { + console.log('线段数据:', e.feature); +}); + +// 鼠标悬停高亮 +lineLayer.on('mousemove', (e) => { + lineLayer.setActive(e.feature.id); +}); +``` + +### 数据过滤 + +```javascript +// 只显示特定类型的线 +lineLayer.filter((feature) => { + return ['高速公路', '国道'].includes(feature.type); +}); +``` + +> 📖 **完整文档**:查看 [图层通用方法和事件](./layer-common-api.md) 了解所有通用 API。 + +--- + +## 代码示例 + +### 基础用法 - 路径线 + +```javascript +import { LineLayer } from '@antv/l7'; + +const data = [ + { + coordinates: [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28], + ], + name: '路线1', + type: 'A', + }, +]; + +scene.on('loaded', () => { + const lineLayer = new LineLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .shape('line') + .size(3) + .color('#5B8FF9') + .style({ + opacity: 0.8, + }); + + scene.addLayer(lineLayer); +}); +``` + +### 多条线段 + +```javascript +const lines = [ + { + coordinates: [ + [120.19, 30.26], + [120.2, 30.27], + ], + name: '线路1', + value: 100, + }, + { + coordinates: [ + [120.21, 30.28], + [120.22, 30.29], + ], + name: '线路2', + value: 200, + }, +]; + +const lineLayer = new LineLayer() + .source(lines, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .shape('line') + .size('value', [2, 10]) // 根据数值映射宽度 + .color('name', ['#5B8FF9', '#5AD8A6']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(lineLayer); +``` + +### 虚线样式 + +```javascript +const lineLayer = new LineLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .shape('line') + .size(2) + .color('#5B8FF9') + .style({ + lineType: 'dash', // 虚线类型: solid | dash + dashArray: [5, 5], // 虚线间隔 + opacity: 0.8, + }); + +scene.addLayer(lineLayer); +``` + +### 2D 弧线 - OD 流向 + +```javascript +const odData = [ + { + from_lng: 120.19, + from_lat: 30.26, + to_lng: 121.47, + to_lat: 31.23, + value: 100, + }, +]; + +const arcLayer = new LineLayer() + .source(odData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc') + .size('value', [1, 5]) + .color('value', ['#5B8FF9', '#5AD8A6', '#FF6B3B']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(arcLayer); +``` + +### 3D 弧线 - 城市迁徙 + +```javascript +const migrationLayer = new LineLayer() + .source(migrationData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc3d') + .size('count', [1, 5]) + .color('count', ['#5B8FF9', '#5AD8A6', '#FF6B3B', '#CF1D49']) + .style({ + opacity: 0.8, + sourceColor: '#5B8FF9', // 起点颜色 + targetColor: '#CF1D49', // 终点颜色 + }); + +scene.addLayer(migrationLayer); +``` + +### 大圆航线 + +```javascript +const flightLayer = new LineLayer() + .source(flightData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('greatcircle') // 大圆航线,地球表面最短路径 + .size(2) + .color('#6495ED') + .style({ + opacity: 0.6, + }); + +scene.addLayer(flightLayer); +``` + +### 带动画的轨迹 + +```javascript +const trajectoryLayer = new LineLayer() + .source(pathData, { + parser: { + type: 'json', + coordinates: 'path', + }, + }) + .shape('line') + .size(3) + .color('#6495ED') + .animate({ + enable: true, + interval: 0.2, // 动画间隔 + duration: 5, // 动画持续时间 + trailLength: 0.2, // 轨迹长度比例 + }) + .style({ + opacity: 0.8, + }); + +scene.addLayer(trajectoryLayer); +``` + +### 渐变色线条 + +```javascript +const gradientLineLayer = new LineLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .shape('line') + .size(4) + .color('#5B8FF9') + .style({ + opacity: 0.8, + lineGradient: true, // 开启渐变 + sourceColor: '#5B8FF9', // 起始颜色 + targetColor: '#CF1D49', // 结束颜色 + }); + +scene.addLayer(gradientLineLayer); +``` + +### 3D 墙效果 + +```javascript +const wallLayer = new LineLayer() + .source(data, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .shape('wall') + .size('height', [10, 100]) // 墙的高度 + .color('#5B8FF9') + .style({ + opacity: 0.6, + }); + +scene.addLayer(wallLayer); +``` + +## 数据格式要求 + +### 路径线数据格式 + +```json +[ + { + "coordinates": [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28] + ], + "name": "路线1", + "type": "A", + "value": 100 + } +] +``` + +### OD 数据格式 + +```json +[ + { + "from_lng": 120.19, + "from_lat": 30.26, + "to_lng": 121.47, + "to_lat": 31.23, + "value": 100, + "category": "A" + } +] +``` + +### GeoJSON 线数据 + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "线路1", + "type": "A" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.28] + ] + } + } + ] +} +``` + +使用 GeoJSON: + +```javascript +lineLayer.source(geojsonData, { + parser: { + type: 'geojson', + }, +}); +``` + +## 样式配置详解 + +### 基础样式 + +```javascript +layer.style({ + opacity: 0.8, // 透明度 + lineType: 'solid', // 线类型: solid | dash + dashArray: [5, 5], // 虚线配置 [实线长度, 间隔长度] +}); +``` + +### 渐变样式 + +```javascript +layer.style({ + lineGradient: true, // 开启渐变 + sourceColor: '#5B8FF9', // 起点颜色 + targetColor: '#CF1D49', // 终点颜色 +}); +``` + +### 动画配置 + +```javascript +layer.animate({ + enable: true, // 开启动画 + interval: 0.2, // 动画间隔,数值越小速度越快 + duration: 5, // 动画持续时间(秒) + trailLength: 0.2, // 轨迹长度比例 0-1 +}); +``` + +## 常见问题 + +### 1. 线不显示 + +**检查清单**: + +- ✅ 坐标数据是否正确(至少 2 个点) +- ✅ 线的宽度是否太小 +- ✅ 颜色是否与背景相同 +- ✅ 坐标是否在地图视野内 + +```javascript +// 调试代码 +console.log('数据:', data); +layer.size(10); // 加粗线条 +layer.color('#FF0000'); // 使用明显颜色 +``` + +### 2. 弧线方向错误 + +OD 数据的起点和终点要明确: + +```javascript +// 正确的配置 +.source(data, { + parser: { + type: 'json', + x: 'from_lng', // 起点经度 + y: 'from_lat', // 起点纬度 + x1: 'to_lng', // 终点经度 + y1: 'to_lat' // 终点纬度 + } +}) +``` + +### 3. 动画不流畅 + +**优化方案**: + +```javascript +// 1. 调整动画参数 +layer.animate({ + enable: true, + interval: 0.1, // 减小间隔 + duration: 3, // 缩短持续时间 + trailLength: 0.1, // 缩短轨迹长度 +}); + +// 2. 减少数据量 +const simplifiedData = data.filter((d, i) => i % 5 === 0); +``` + +### 4. 3D 弧线看不到 + +需要设置地图倾斜角度: + +```javascript +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + pitch: 45, // 设置倾斜角度 + style: 'dark', + center: [120, 30], + zoom: 5, + }), +}); +``` + +## 高级用法 + +### 多图层组合 - 线 + 端点 + +```javascript +// 1. 绘制线 +const lineLayer = new LineLayer() + .source(odData, { + parser: { + type: 'json', + x: 'from_lng', + y: 'from_lat', + x1: 'to_lng', + y1: 'to_lat', + }, + }) + .shape('arc') + .size(2) + .color('#6495ED'); + +// 2. 绘制起点 +const startPoints = odData.map((d) => ({ + lng: d.from_lng, + lat: d.from_lat, +})); + +const startLayer = new PointLayer() + .source(startPoints, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('circle') + .size(5) + .color('#00FF00'); + +// 3. 绘制终点 +const endPoints = odData.map((d) => ({ + lng: d.to_lng, + lat: d.to_lat, +})); + +const endLayer = new PointLayer() + .source(endPoints, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('circle') + .size(5) + .color('#FF0000'); + +scene.addLayer(lineLayer); +scene.addLayer(startLayer); +scene.addLayer(endLayer); +``` + +### 动态更新线数据 + +```javascript +// 更新数据 +const newData = [...]; +layer.setData(newData); + +// 更新样式 +layer.size(5); +layer.color('#FF0000'); +scene.render(); +``` + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [场景初始化](../core/scene.md) +- [点图层](./point.md) +- [轨迹动画](../animation/layer-animation.md) +- [颜色映射](../visual/mapping.md) +- [事件交互](../interaction/events.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 线图层](https://l7.antv.antgroup.com/examples/line/path) diff --git a/skills/l7/references/layers/point.md b/skills/l7/references/layers/point.md new file mode 100644 index 0000000..fa4b532 --- /dev/null +++ b/skills/l7/references/layers/point.md @@ -0,0 +1,530 @@ +--- +skill_id: point-layer +skill_name: 点图层 +category: layers +difficulty: beginner +tags: [point, layer, scatter, bubble, visualization] +dependencies: [scene-initialization, source-geojson] +version: 2.x +--- + +# 点图层 + +## 技能描述 + +在地图上绘制点状地理要素,支持气泡图、散点图、符号地图等多种形式。 + +## 何时使用 + +- ✅ 显示 POI 位置(餐厅、商店、景点等) +- ✅ 显示事件发生点(地震、案件、事故) +- ✅ 显示采样点位置(气象站、监测点) +- ✅ 散点图可视化(人口分布、经济指标) +- ✅ 3D 柱状图(城市数据对比) + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好点位数据(包含经纬度) + +## 输入参数 + +### 数据格式 + +```typescript +interface PointData { + lng: number; // 经度 + lat: number; // 纬度 + [key: string]: any; // 其他属性 +} +``` + +### 图层配置 + +| 方法 | 参数 | 说明 | +| ---------------------- | --------------------- | ------------------------------------------------------- | +| `source(data, config)` | data: 数据数组 | 设置数据源 | +| `shape(type)` | type: 形状类型 | circle \| square \| hexagon \| triangle \| text \| icon | +| `size(value)` | value: 数字或字段映射 | 设置点大小 | +| `color(value)` | value: 颜色或字段映射 | 设置点颜色 | +| `style(config)` | config: 样式对象 | 设置样式 | + +## 输出 + +返回 `PointLayer` 实例 + +## 通用方法 + +点图层继承了所有图层的通用能力,以下是最常用的方法: + +### 显示控制 + +```javascript +// 显示/隐藏图层 +pointLayer.show(); +pointLayer.hide(); + +// 检查可见性 +if (pointLayer.isVisible()) { + console.log('图层可见'); +} + +// 设置图层绘制顺序(数值越大越在上层) +pointLayer.setIndex(10); +``` + +### 事件监听 + +```javascript +// 点击事件 +pointLayer.on('click', (e) => { + console.log('点击的点:', e.feature); + console.log('经纬度:', e.lngLat); +}); + +// 鼠标悬停 +pointLayer.on('mousemove', (e) => { + // 显示 tooltip +}); + +// 鼠标移出 +pointLayer.on('mouseout', () => { + // 隐藏 tooltip +}); +``` + +### 数据更新 + +```javascript +// 数据过滤 +pointLayer.filter((feature) => { + return feature.value > 100; +}); + +// 适配到数据范围 +pointLayer.fitBounds(); + +// 设置缩放范围 +pointLayer.setMinZoom(10); // zoom < 10 时不显示 +pointLayer.setMaxZoom(18); // zoom > 18 时不显示 +``` + +> 📖 **完整文档**:查看 [图层通用方法和事件](./layer-common-api.md) 了解所有通用 API,包括 source、scale、所有事件类型、聚合方法等。 + +--- + +## 代码示例 + +### 基础用法 - 简单散点图 + +```javascript +import { PointLayer } from '@antv/l7'; + +const data = [ + { lng: 120.19, lat: 30.26, name: '点位1', value: 100 }, + { lng: 120.2, lat: 30.27, name: '点位2', value: 200 }, + { lng: 120.21, lat: 30.28, name: '点位3', value: 300 }, +]; + +scene.on('loaded', () => { + const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size(10) + .color('#5B8FF9') + .style({ + opacity: 0.8, + }); + + scene.addLayer(pointLayer); +}); +``` + +### 数据驱动 - 颜色和大小映射 + +```javascript +const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size('value', [5, 20]) // 根据 value 字段映射大小 + .color('category', { + // 根据 category 字段映射颜色 + A: '#5B8FF9', + B: '#5AD8A6', + C: '#5D7092', + }) + .style({ + opacity: 0.8, + strokeWidth: 2, + stroke: '#fff', + }); + +scene.addLayer(pointLayer); +``` + +### 气泡图 - 面积映射 + +```javascript +const bubbleLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .size('population', (value) => Math.sqrt(value)) // 使用回调函数 + .color('gdp', ['#FFF5B8', '#FFAB5C', '#FF6B3B', '#CC2B12']) + .style({ + opacity: 0.6, + strokeWidth: 1, + stroke: '#fff', + }); + +scene.addLayer(bubbleLayer); +``` + +### 不同形状的点 + +```javascript +// 圆形 +const circleLayer = new PointLayer().source(data).shape('circle').size(10).color('#5B8FF9'); + +// 方形 +const squareLayer = new PointLayer().source(data).shape('square').size(10).color('#5AD8A6'); + +// 三角形 +const triangleLayer = new PointLayer().source(data).shape('triangle').size(10).color('#5D7092'); + +// 六边形 +const hexagonLayer = new PointLayer().source(data).shape('hexagon').size(10).color('#FF6B3B'); +``` + +### 文本标注 + +```javascript +const textLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('name', 'text') // 使用 name 字段作为文本 + .size(14) + .color('#000') + .style({ + textAnchor: 'center', // 文本对齐方式 + textOffset: [0, 20], // 文本偏移 + fontWeight: 'bold', + stroke: '#fff', + strokeWidth: 2, + }); + +scene.addLayer(textLayer); +``` + +### 图标点 + +```javascript +const iconLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('icon', 'image') + .size(20) + .style({ + icon: 'https://example.com/icon.png', + }); + +scene.addLayer(iconLayer); +``` + +### 3D 柱状图 + +```javascript +const columnLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('cylinder') // 3D 柱状 + .size('value', [10, 100]) // 映射高度 + .color('type', ['#5B8FF9', '#5AD8A6', '#5D7092']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(columnLayer); +``` + +### 组合使用 - 点 + 文本 + +```javascript +// 底层点 +const pointLayer = new PointLayer() + .source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('circle') + .size(15) + .color('#5B8FF9') + .style({ + opacity: 0.8, + strokeWidth: 2, + stroke: '#fff', + }); + +// 顶层文本 +const textLayer = new PointLayer() + .source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .shape('name', 'text') + .size(12) + .color('#fff') + .style({ + textAnchor: 'center', + textOffset: [0, 0], + }); + +scene.addLayer(pointLayer); +scene.addLayer(textLayer); +``` + +## 样式配置详解 + +### 基础样式 + +```javascript +layer.style({ + opacity: 0.8, // 透明度 0-1 + strokeWidth: 2, // 描边宽度 + stroke: '#fff', // 描边颜色 + strokeOpacity: 1, // 描边透明度 +}); +``` + +### 文本样式 + +```javascript +layer.style({ + textAnchor: 'center', // 对齐: center | left | right | top | bottom + textOffset: [0, 20], // 偏移 [x, y] + spacing: 2, // 字符间距 + padding: [5, 5], // 内边距 + fontFamily: 'sans-serif', + fontSize: 12, + fontWeight: 'normal', // normal | bold + textAllowOverlap: true, // 是否允许文本重叠 + stroke: '#fff', // 文本描边 + strokeWidth: 2, + strokeOpacity: 1, +}); +``` + +## 数据格式要求 + +### GeoJSON 格式 + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "点位1", + "value": 100, + "category": "A" + }, + "geometry": { + "type": "Point", + "coordinates": [120.19, 30.26] + } + } + ] +} +``` + +使用 GeoJSON: + +```javascript +layer.source(geojsonData, { + parser: { + type: 'geojson', + }, +}); +``` + +### JSON 格式 + +```json +[ + { + "lng": 120.19, + "lat": 30.26, + "name": "点位1", + "value": 100, + "category": "A" + } +] +``` + +### CSV 格式 + +```csv +lng,lat,name,value,category +120.19,30.26,点位1,100,A +120.20,30.27,点位2,200,B +120.21,30.28,点位3,300,C +``` + +使用 CSV: + +```javascript +layer.source(csvData, { + parser: { + type: 'csv', + x: 'lng', + y: 'lat', + }, +}); +``` + +## 常见问题 + +### 1. 点不显示 + +**原因分析**: + +- 坐标不在地图视野范围内 +- 点太小看不见 +- 颜色与背景相同 +- 数据格式错误 + +**解决方案**: + +```javascript +// 1. 检查数据坐标 +console.log(data); + +// 2. 增大点的大小 +layer.size(20); + +// 3. 使用明显的颜色 +layer.color('#FF0000'); + +// 4. 检查地图中心和缩放级别 +scene.setCenter([120.19, 30.26]); +scene.setZoom(12); + +// 5. 使用 fitBounds 自动调整视野 +const bounds = [ + [minLng, minLat], + [maxLng, maxLat], +]; +scene.fitBounds(bounds); +``` + +### 2. 大数据量性能问题 + +**优化方案**: + +```javascript +// 1. 数据抽稀 +layer.source(data.filter((d, i) => i % 10 === 0)); + +// 2. 根据缩放级别显示 +layer.setMinZoom(10); // 只在 zoom >= 10 时显示 + +// 3. 使用聚合 +// 参考: ../performance/optimization.md +``` + +### 3. 点的大小不一致 + +**问题**: 不同缩放级别下点的大小变化 + +**解决方案**: + +```javascript +// 使用单位配置 +layer.size(10).style({ + unit: 'meter', // 使用地理单位,点会随缩放变化 +}); + +// 或使用像素单位(默认) +layer.size(10).style({ + unit: 'pixel', // 使用像素单位,点大小固定 +}); +``` + +## 高级用法 + +### 动态更新数据 + +```javascript +// 更新数据 +const newData = [...]; +layer.setData(newData); + +// 只更新样式 +layer.color('#FF0000'); +layer.size(20); +scene.render(); +``` + +### 图层控制 + +```javascript +// 显示/隐藏 +layer.show(); +layer.hide(); + +// 销毁图层 +layer.destroy(); +scene.removeLayer(layer); + +// 设置层级 +layer.setIndex(10); + +// 设置显示范围 +layer.setMinZoom(5); +layer.setMaxZoom(15); +``` + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [场景初始化](../core/scene.md) +- [GeoJSON 数据处理](../data/source-geojson.md) +- [颜色映射](../visual/mapping.md) +- [大小映射](../visual/mapping.md) +- [事件交互](../interaction/events.md) +- [添加弹窗](../interaction/popup.md) + +## 在线示例 + +查看更多在线示例: [L7 官方示例 - 点图层](https://l7.antv.antgroup.com/examples/point/scatter) diff --git a/skills/l7/references/layers/polygon.md b/skills/l7/references/layers/polygon.md new file mode 100644 index 0000000..83db372 --- /dev/null +++ b/skills/l7/references/layers/polygon.md @@ -0,0 +1,524 @@ +--- +skill_id: polygon-layer +skill_name: 面图层 +category: layers +difficulty: beginner +tags: [polygon, fill, extrude, 3d, choropleth] +dependencies: [scene-initialization] +version: 2.x +--- + +# 面图层 + +## 技能描述 + +在地图上绘制面状地理要素,支持填充图、3D 挤出、等值图等多种形式。 + +## 何时使用 + +- ✅ 显示行政区划(省、市、区) +- ✅ 显示建筑轮廓(2D/3D) +- ✅ 显示土地利用分类 +- ✅ 显示等值区域(人口密度、经济指标) +- ✅ 显示湖泊、公园等区域 +- ✅ 制作热力分区图 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好面数据(通常是 GeoJSON 格式) + +## 面类型 + +| 类型 | 说明 | 适用场景 | +| --------- | -------- | ------------------ | +| `fill` | 2D 填充 | 行政区划、土地分类 | +| `extrude` | 3D 挤出 | 建筑、人口柱状图 | +| `water` | 水面效果 | 湖泊、海洋 | +| `ocean` | 海洋效果 | 全球海洋 | + +## 通用方法 + +面图层继承了所有图层的通用能力,以下是最常用的方法: + +### 显示控制 + +```javascript +// 显示/隐藏图层 +polygonLayer.show(); +polygonLayer.hide(); + +// 设置图层顺序(面图层通常在底层) +polygonLayer.setIndex(1); + +// 缩放到图层范围 +polygonLayer.fitBounds(); +``` + +### 事件监听 + +```javascript +// 点击区域 +polygonLayer.on('click', (e) => { + console.log('区域名称:', e.feature.properties.name); + console.log('区域数据:', e.feature); +}); + +// 鼠标悬停高亮 +polygonLayer.on('mousemove', (e) => { + // 高亮当前区域 +}); + +polygonLayer.on('mouseout', () => { + // 取消高亮 +}); +``` + +### 数据过滤 + +```javascript +// 只显示特定省份 +polygonLayer.filter((feature) => { + return ['浙江省', '江苏省', '上海市'].includes(feature.name); +}); +``` + +> 📖 **完整文档**:查看 [图层通用方法和事件](./layer-common-api.md) 了解所有通用 API。 + +--- + +## 代码示例 + +### 基础用法 - 区域填充 + +```javascript +import { PolygonLayer } from '@antv/l7'; + +scene.on('loaded', () => { + fetch('https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json') + .then((res) => res.json()) + .then((data) => { + const polygonLayer = new PolygonLayer().source(data).shape('fill').color('#5B8FF9').style({ + opacity: 0.6, + }); + + scene.addLayer(polygonLayer); + }); +}); +``` + +### 数据驱动 - 等值图 + +```javascript +const provinceLayer = new PolygonLayer() + .source(provinceData) + .shape('fill') + .color('gdp', ['#FFF5B8', '#FFAB5C', '#FF6B3B', '#CC2B12']) // GDP 分级着色 + .style({ + opacity: 0.8, + }); + +scene.addLayer(provinceLayer); +``` + +### 自定义颜色映射 + +```javascript +const landUseLayer = new PolygonLayer() + .source(landUseData) + .shape('fill') + .color('type', { + 住宅: '#5B8FF9', + 商业: '#5AD8A6', + 工业: '#5D7092', + 绿地: '#61DDAA', + 水域: '#65789B', + }) + .style({ + opacity: 0.7, + }); + +scene.addLayer(landUseLayer); +``` + +### 带描边的填充图 + +```javascript +// 填充层 +const fillLayer = new PolygonLayer() + .source(data) + .shape('fill') + .color('value', ['#FFF5B8', '#FFAB5C', '#FF6B3B']) + .style({ + opacity: 0.8, + }); + +// 描边层 +const lineLayer = new LineLayer().source(data).shape('line').size(1).color('#fff').style({ + opacity: 0.6, +}); + +scene.addLayer(fillLayer); +scene.addLayer(lineLayer); +``` + +### 3D 建筑 - 挤出效果 + +```javascript +const buildingLayer = new PolygonLayer() + .source(buildingData) + .shape('extrude') + .size('height') // 根据 height 字段设置高度 + .color('type', { + 住宅: '#5B8FF9', + 商业: '#5AD8A6', + 办公: '#5D7092', + }) + .style({ + opacity: 0.8, + }); + +scene.addLayer(buildingLayer); + +// 确保场景有倾斜角度才能看到 3D 效果 +scene.setPitch(45); +``` + +### 3D 人口柱状图 + +```javascript +const populationLayer = new PolygonLayer() + .source(districtData) + .shape('extrude') + .size('population', [0, 50000]) // 人口映射到高度 + .color('population', ['#FFF5B8', '#FFAB5C', '#FF6B3B', '#CC2B12']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(populationLayer); +``` + +### 水面效果 + +```javascript +const lakeLayer = new PolygonLayer() + .source(lakeData) + .shape('water') + .color('#6495ED') + .style({ + opacity: 0.8, + speed: 0.5, // 水波速度 + }) + .animate(true); + +scene.addLayer(lakeLayer); +``` + +### 海洋效果 + +```javascript +const oceanLayer = new PolygonLayer() + .source(oceanData) + .shape('ocean') + .color('#284AC9') + .style({ + opacity: 0.8, + }) + .animate(true); + +scene.addLayer(oceanLayer); +``` + +## 数据格式要求 + +### GeoJSON 格式(推荐) + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "浙江省", + "adcode": "330000", + "gdp": 82553, + "population": 6540 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.26], + [120.19, 30.26] + ] + ] + } + } + ] +} +``` + +使用 GeoJSON: + +```javascript +layer.source(geojsonData, { + parser: { + type: 'geojson', + }, +}); +``` + +### MultiPolygon(多面) + +```json +{ + "type": "Feature", + "properties": { + "name": "浙江省(含岛屿)" + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [120.19, 30.26], + [120.2, 30.27], + [120.21, 30.26], + [120.19, 30.26] + ] + ], + [ + [ + [122.1, 30.0], + [122.11, 30.01], + [122.12, 30.0], + [122.1, 30.0] + ] + ] + ] + } +} +``` + +### 3D 建筑数据格式 + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "大厦A", + "height": 150, + "floors": 30, + "type": "商业" + }, + "geometry": { + "type": "Polygon", + "coordinates": [...] + } + } + ] +} +``` + +## 样式配置详解 + +### 2D 填充样式 + +```javascript +layer.style({ + opacity: 0.8, // 透明度 + stroke: '#fff', // 描边颜色(需要配合 lineLayer) + strokeWidth: 1, // 描边宽度 +}); +``` + +### 3D 挤出样式 + +```javascript +layer.style({ + opacity: 0.8, + extrudeBase: 0, // 挤出基准高度 + pickLight: true, // 是否接受光照 +}); +``` + +### 水面样式 + +```javascript +layer.style({ + opacity: 0.8, + speed: 0.5, // 水波动画速度 +}); +``` + +## 常见问题 + +### 1. 面不显示 + +**检查清单**: + +- ✅ GeoJSON 数据格式是否正确 +- ✅ 坐标顺序是否正确(经度在前,纬度在后) +- ✅ 多边形是否闭合(首尾坐标相同) +- ✅ 颜色是否与背景相同 +- ✅ opacity 是否为 0 + +```javascript +// 调试代码 +console.log('数据:', data); +layer.color('#FF0000'); // 使用明显颜色 +layer.style({ opacity: 1 }); +``` + +### 2. 3D 效果看不到 + +需要设置地图倾斜: + +```javascript +// 创建场景时设置 +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + pitch: 45, // 倾斜角度 + center: [120, 30], + zoom: 12, + }), +}); + +// 或运行时设置 +scene.setPitch(45); +``` + +### 3. 数据加载慢 + +**优化方案**: + +```javascript +// 1. 简化几何形状 +layer.source(data, { + parser: { + type: 'geojson', + }, + transforms: [ + { + type: 'simplify', + tolerance: 0.01, // 简化容差 + }, + ], +}); + +// 2. 根据缩放级别显示 +layer.setMinZoom(5); +layer.setMaxZoom(15); +``` + +### 4. 描边效果不明显 + +需要单独创建线图层: + +```javascript +// 填充层 +const fillLayer = new PolygonLayer() + .source(data) + .shape('fill') + .color('#5B8FF9') + .style({ opacity: 0.6 }); + +// 线图层 +const lineLayer = new LineLayer() + .source(data) + .shape('line') + .size(2) + .color('#fff') + .style({ opacity: 1 }); + +scene.addLayer(fillLayer); +scene.addLayer(lineLayer); +``` + +## 高级用法 + +### 分级设色图(Choropleth) + +```javascript +// 配置比例尺 +layer + .source(data) + .shape('fill') + .color('value', ['#FFF5B8', '#FFAB5C', '#FF6B3B', '#CC2B12']) + .scale('value', { + type: 'quantize', // 分段类型 + domain: [0, 1000], + }) + .style({ + opacity: 0.8, + }); + +// 添加图例 +import { Legend } from '@antv/l7'; + +const legend = new Legend({ + position: 'bottomright', + items: [ + { value: '0-250', color: '#FFF5B8' }, + { value: '250-500', color: '#FFAB5C' }, + { value: '500-750', color: '#FF6B3B' }, + { value: '750-1000', color: '#CC2B12' }, + ], +}); + +scene.addControl(legend); +``` + +### 动态更新数据 + +```javascript +// 更新数据 +const newData = {...}; +layer.setData(newData); + +// 更新样式 +layer.color('newField', ['#5B8FF9', '#5AD8A6']); +scene.render(); +``` + +### 高亮选中区域 + +```javascript +layer.on('click', (e) => { + // 重置之前的高亮 + if (layer.selectedFeatureId) { + layer.setActive(layer.selectedFeatureId, false); + } + + // 高亮当前选中 + layer.setActive(e.featureId, true); + layer.selectedFeatureId = e.featureId; +}); + +// 配置高亮样式 +layer.style({ + selectColor: '#FF0000', +}); +``` + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [场景初始化](../core/scene.md) +- [线图层(描边)](./line.md) +- [颜色映射](../visual/mapping.md) +- [事件交互](../interaction/events.md) +- [添加弹窗](../interaction/popup.md) +- [添加图例](../interaction/components.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 面图层](https://l7.antv.antgroup.com/examples/polygon/fill) diff --git a/skills/l7/references/layers/raster.md b/skills/l7/references/layers/raster.md new file mode 100644 index 0000000..cd77965 --- /dev/null +++ b/skills/l7/references/layers/raster.md @@ -0,0 +1,605 @@ +--- +skill_id: raster-layer +skill_name: 栅格瓦片图层 +category: layers +difficulty: beginner +tags: [raster, layer, tile, xyz, tms, imagery] +dependencies: [scene-initialization] +version: 2.x +--- + +# 栅格瓦片图层 + +## 技能描述 + +加载和显示第三方图片瓦片服务,支持 XYZ、TMS 等标准瓦片协议,可用于叠加卫星影像、地形图、专题图等栅格数据。 + +## 何时使用 + +- ✅ 叠加第三方卫星影像(天地图、OpenStreetMap) +- ✅ 显示专题地图图层(气象云图、热力分析) +- ✅ 加载自定义瓦片服务(地形渲染、历史地图) +- ✅ 多源数据叠加(多个瓦片图层组合) +- ✅ 替换或补充默认底图 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 准备好瓦片服务 URL(XYZ 或 TMS 格式) +- 了解瓦片服务的坐标系和缩放级别范围 + +## 输入参数 + +### 瓦片 URL 格式 + +```typescript +// XYZ 格式 (最常用) +'https://tile.server.com/{z}/{x}/{y}.png'; + +// TMS 格式 (Y轴反转) +'https://tile.server.com/{z}/{x}/{-y}.png'; + +// 多子域 +'https://{s}.tile.server.com/{z}/{x}/{y}.png'; + +// 带参数 +'https://tile.server.com/{z}/{x}/{y}.png?token=YOUR_TOKEN'; +``` + +### 图层配置 + +| 方法 | 参数 | 说明 | +| --------------------- | -------------------------- | -------------- | +| `source(url, config)` | url: 瓦片URL, config: 配置 | 设置瓦片数据源 | +| `style(config)` | config: 样式对象 | 设置样式参数 | + +### 配置参数 + +```typescript +{ + parser: { + type: 'rasterTile', + tileSize: 256, // 瓦片大小,默认 256 + minZoom: 0, // 最小缩放级别 + maxZoom: 18, // 最大缩放级别 + zoomOffset: 0, // 缩放偏移量 + updateStrategy: 'overlap' // 更新策略: overlap | replace + } +} +``` + +## 输出 + +返回 `RasterLayer` 实例 + +## 代码示例 + +### 基础用法 - OpenStreetMap 瓦片 + +```javascript +import { RasterLayer } from '@antv/l7'; + +scene.on('loaded', () => { + const osmLayer = new RasterLayer() + .source('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 0, + maxZoom: 18, + }, + }) + .style({ + opacity: 1.0, + }); + + scene.addLayer(osmLayer); +}); +``` + +### 天地图影像服务 + +```javascript +const TOKEN = 'YOUR_TIANDITU_TOKEN'; + +const tdtImageLayer = new RasterLayer({ + name: 'tianditu-imagery', + zIndex: 1, +}).source(`https://t{s}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${TOKEN}`, { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 0, + maxZoom: 18, + subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], + }, +}); + +// 叠加天地图注记 +const tdtLabelLayer = new RasterLayer({ + name: 'tianditu-labels', + zIndex: 2, +}).source(`https://t{s}.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=${TOKEN}`, { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 0, + maxZoom: 18, + subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], + }, +}); + +scene.addLayer(tdtImageLayer); +scene.addLayer(tdtLabelLayer); +``` + +### Mapbox 卫星图层 + +```javascript +const MAPBOX_TOKEN = 'YOUR_MAPBOX_TOKEN'; + +const satelliteLayer = new RasterLayer() + .source( + `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=${MAPBOX_TOKEN}`, + { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 0, + maxZoom: 22, + }, + }, + ) + .style({ + opacity: 0.8, + }); + +scene.addLayer(satelliteLayer); +``` + +### TMS 格式瓦片 + +```javascript +// TMS 格式的 Y 轴是反转的 +const tmsLayer = new RasterLayer().source('https://tile.server.com/{z}/{x}/{-y}.png', { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 0, + maxZoom: 16, + }, +}); + +scene.addLayer(tmsLayer); +``` + +### 自定义瓦片服务 + +```javascript +const customTileLayer = new RasterLayer({ + name: 'custom-tiles', +}) + .source('https://your-tile-server.com/tiles/{z}/{x}/{y}.png', { + parser: { + type: 'rasterTile', + tileSize: 512, // 512x512 瓦片 + minZoom: 0, + maxZoom: 14, + zoomOffset: 0, + updateStrategy: 'overlap', + }, + }) + .style({ + opacity: 1.0, + }); + +scene.addLayer(customTileLayer); +``` + +### 多子域配置 + +```javascript +// 提高并发加载速度 +const tileLayer = new RasterLayer().source('https://{s}.example.com/{z}/{x}/{y}.png', { + parser: { + type: 'rasterTile', + tileSize: 256, + subdomains: ['a', 'b', 'c', 'd'], // 4个子域轮流请求 + }, +}); +``` + +### 带认证的瓦片服务 + +```javascript +const securedTileLayer = new RasterLayer().source( + 'https://secure-tiles.example.com/{z}/{x}/{y}.png?apikey=YOUR_API_KEY', + { + parser: { + type: 'rasterTile', + tileSize: 256, + maxZoom: 18, + }, + }, +); + +scene.addLayer(securedTileLayer); +``` + +### 图层控制 - 显示/隐藏 + +```javascript +const rasterLayer = new RasterLayer().source(tileUrl, config); +scene.addLayer(rasterLayer); + +// 隐藏图层 +rasterLayer.hide(); + +// 显示图层 +rasterLayer.show(); + +// 切换可见性 +function toggleLayer() { + const isVisible = rasterLayer.isVisible(); + if (isVisible) { + rasterLayer.hide(); + } else { + rasterLayer.show(); + } +} +``` + +### 透明度动态调整 + +```javascript +const rasterLayer = new RasterLayer().source(tileUrl, config); +scene.addLayer(rasterLayer); + +// 创建透明度滑块 +const slider = document.getElementById('opacity-slider'); +slider.addEventListener('input', (e) => { + const opacity = parseFloat(e.target.value); + rasterLayer.style({ opacity }); + scene.render(); +}); +``` + +## 样式配置详解 + +```javascript +{ + opacity: 1.0, // 透明度,0-1,默认 1.0 + // 暂不支持更多样式配置 +} +``` + +## 常见场景 + +### 1. 叠加气象云图 + +```javascript +class WeatherTileLayer { + constructor(scene) { + this.scene = scene; + this.layer = null; + this.updateInterval = null; + } + + start() { + // 创建气象瓦片图层 + this.layer = new RasterLayer({ + name: 'weather-radar', + }) + .source('https://weather.example.com/radar/{z}/{x}/{y}.png?time=' + Date.now(), { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 1, + maxZoom: 10, + }, + }) + .style({ + opacity: 0.6, + }); + + this.scene.addLayer(this.layer); + + // 每10分钟更新一次 + this.updateInterval = setInterval( + () => { + this.update(); + }, + 10 * 60 * 1000, + ); + } + + update() { + const timestamp = Date.now(); + this.layer.setData(`https://weather.example.com/radar/{z}/{x}/{y}.png?time=${timestamp}`, { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 1, + maxZoom: 10, + }, + }); + } + + stop() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + if (this.layer) { + this.scene.removeLayer(this.layer); + } + } +} + +// 使用 +const weatherLayer = new WeatherTileLayer(scene); +weatherLayer.start(); +``` + +### 2. 多图层切换 + +```javascript +const layers = { + satellite: { + name: '卫星影像', + url: 'https://tile.server.com/satellite/{z}/{x}/{y}.jpg', + layer: null, + }, + street: { + name: '街道地图', + url: 'https://tile.server.com/street/{z}/{x}/{y}.png', + layer: null, + }, + terrain: { + name: '地形图', + url: 'https://tile.server.com/terrain/{z}/{x}/{y}.png', + layer: null, + }, +}; + +// 初始化所有图层 +Object.keys(layers).forEach((key) => { + const config = layers[key]; + config.layer = new RasterLayer({ name: key }) + .source(config.url, { + parser: { + type: 'rasterTile', + tileSize: 256, + }, + }) + .style({ + opacity: key === 'satellite' ? 1.0 : 0, // 默认显示卫星图 + }); + + scene.addLayer(config.layer); +}); + +// 切换图层 +function switchLayer(layerKey) { + Object.keys(layers).forEach((key) => { + const opacity = key === layerKey ? 1.0 : 0; + layers[key].layer.style({ opacity }); + }); + scene.render(); +} + +// UI 控制 +document.querySelectorAll('.layer-switch').forEach((btn) => { + btn.addEventListener('click', (e) => { + const layerKey = e.target.dataset.layer; + switchLayer(layerKey); + }); +}); +``` + +### 3. 历史影像对比 + +```javascript +// 对比两个不同时期的影像 +const layer2020 = new RasterLayer({ name: 'image-2020' }) + .source('https://tiles.example.com/2020/{z}/{x}/{y}.png', { + parser: { type: 'rasterTile', tileSize: 256 }, + }) + .style({ opacity: 1.0 }); + +const layer2025 = new RasterLayer({ name: 'image-2025' }) + .source('https://tiles.example.com/2025/{z}/{x}/{y}.png', { + parser: { type: 'rasterTile', tileSize: 256 }, + }) + .style({ opacity: 0.0 }); + +scene.addLayer(layer2020); +scene.addLayer(layer2025); + +// 卷帘对比控制 +const compareSlider = document.getElementById('compare-slider'); +compareSlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); // 0-1 + layer2020.style({ opacity: 1 - value }); + layer2025.style({ opacity: value }); + scene.render(); +}); +``` + +### 4. 叠加专题图层 + +```javascript +// 基础底图 +const baseLayer = new RasterLayer({ name: 'base', zIndex: 1 }).source( + 'https://tiles.example.com/base/{z}/{x}/{y}.png', + { + parser: { type: 'rasterTile', tileSize: 256 }, + }, +); + +// 专题图层 - 人口密度 +const populationLayer = new RasterLayer({ name: 'population', zIndex: 2 }) + .source('https://tiles.example.com/population/{z}/{x}/{y}.png', { + parser: { type: 'rasterTile', tileSize: 256 }, + }) + .style({ opacity: 0.6 }); + +// 专题图层 - 交通路网 +const trafficLayer = new RasterLayer({ name: 'traffic', zIndex: 3 }) + .source('https://tiles.example.com/traffic/{z}/{x}/{y}.png', { + parser: { type: 'rasterTile', tileSize: 256 }, + }) + .style({ opacity: 0.5 }); + +scene.addLayer(baseLayer); +scene.addLayer(populationLayer); +scene.addLayer(trafficLayer); + +// 图层控制面板 +function toggleThematicLayer(layerName, visible) { + const layers = { population: populationLayer, traffic: trafficLayer }; + if (layers[layerName]) { + visible ? layers[layerName].show() : layers[layerName].hide(); + scene.render(); + } +} +``` + +## 性能优化 + +### 1. 设置合理的缩放级别 + +```javascript +const tileLayer = new RasterLayer().source(tileUrl, { + parser: { + type: 'rasterTile', + tileSize: 256, + minZoom: 3, // 避免加载过大范围的瓦片 + maxZoom: 16, // 避免加载过精细的瓦片 + }, +}); +``` + +### 2. 使用多子域提高并发 + +```javascript +const tileLayer = new RasterLayer().source('https://{s}.tile.server.com/{z}/{x}/{y}.png', { + parser: { + type: 'rasterTile', + tileSize: 256, + subdomains: ['a', 'b', 'c', 'd'], // 多个子域并发请求 + }, +}); +``` + +### 3. 图层按需加载 + +```javascript +let rasterLayer = null; + +function showRasterLayer() { + if (!rasterLayer) { + rasterLayer = new RasterLayer().source(tileUrl, config); + scene.addLayer(rasterLayer); + } else { + rasterLayer.show(); + } +} + +function hideRasterLayer() { + if (rasterLayer) { + rasterLayer.hide(); // 隐藏而不是销毁,保留缓存 + } +} +``` + +### 4. 使用较大瓦片尺寸 + +```javascript +// 512x512 瓦片可减少请求数量 +const tileLayer = new RasterLayer().source(tileUrl, { + parser: { + type: 'rasterTile', + tileSize: 512, // 使用 512 而不是 256 + maxZoom: 16, + }, +}); +``` + +## 注意事项 + +⚠️ **瓦片 URL 格式**:确保使用正确的占位符 `{z}`、`{x}`、`{y}`,TMS 格式使用 `{-y}` + +⚠️ **跨域问题**:确保瓦片服务器配置了 CORS 头,或使用代理 + +⚠️ **缩放级别**:不同瓦片服务支持的缩放级别不同,设置合适的 minZoom/maxZoom + +⚠️ **瓦片尺寸**:大多数服务使用 256x256,部分使用 512x512,需与服务匹配 + +⚠️ **坐标系**:确保瓦片服务使用 Web Mercator (EPSG:3857) 坐标系 + +⚠️ **API Key**:使用第三方服务时注意 API Key 的有效性和配额限制 + +⚠️ **性能**:过多图层会影响性能,建议不超过 3-5 个栅格图层同时显示 + +## 常见问题 + +### Q: 瓦片无法显示? + +A: 检查:1) URL 格式是否正确;2) CORS 配置;3) API Key 是否有效;4) 网络连接;5) 缩放级别范围 + +### Q: 瓦片加载很慢? + +A: 1) 使用多子域配置;2) 使用 CDN;3) 设置合理的缩放级别范围;4) 考虑使用更大的瓦片尺寸 + +### Q: 瓦片位置偏移? + +A: 检查:1) 坐标系是否匹配(Web Mercator);2) 是否需要使用 TMS 格式({-y});3) zoomOffset 设置 + +### Q: 如何判断使用 XYZ 还是 TMS? + +A: 如果瓦片 Y 轴从上到下递增用 XYZ(`{y}`),从下到上递增用 TMS(`{-y}`) + +### Q: 瓦片有缝隙? + +A: 1) 检查 tileSize 设置是否与服务匹配;2) 可能是网络加载延迟,等待加载完成 + +### Q: 如何实现瓦片预加载? + +A: L7 会自动缓存已加载的瓦片,可通过调整地图视角提前触发瓦片加载 + +## 瓦片服务示例 + +### 国内常用服务 + +```javascript +// 天地图 (需要申请 token) +'https://t{s}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=YOUR_TOKEN'; + +// 高德地图 (供参考,建议使用官方 SDK) +'https://wprd0{s}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&style=7'; + +// OpenStreetMap +'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; +``` + +### 国际常用服务 + +```javascript +// Mapbox +'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=YOUR_TOKEN'; + +// CartoDB +'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; + +// Stamen +'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'; +``` + +## 相关技能 + +- [场景初始化](../core/scene.md) +- [图片图层](./image.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 栅格图层](https://l7.antv.antgroup.com/examples/raster/raster) diff --git a/skills/l7/references/layers/tile-vector.md b/skills/l7/references/layers/tile-vector.md new file mode 100644 index 0000000..6941a3a --- /dev/null +++ b/skills/l7/references/layers/tile-vector.md @@ -0,0 +1,608 @@ +--- +skill_id: tile-vector +skill_name: 矢量瓦片图层 +category: layers +difficulty: advanced +tags: [vector-tile, mvt, big-data, tile-layer] +dependencies: [layer-common-api, source-mvt] +version: 2.x +--- + +# 矢量瓦片图层 + +## 技能描述 + +掌握 L7 矢量瓦片图层的使用,通过瓦片化加载实现海量地理数据的高效渲染。矢量瓦片将大范围的矢量数据切分成小块,按需加载和渲染,显著降低数据传输量和渲染压力。 + +## 何时使用 + +- ✅ 需要渲染超大数据量的矢量数据(百万级以上) +- ✅ 需要支持不同缩放级别的详细程度(LOD) +- ✅ 需要减少初始加载时间,按需加载数据 +- ✅ 需要在低带宽环境下流畅显示地图 +- ✅ 使用 Mapbox Vector Tile (MVT) 标准的瓦片服务 + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 了解[MVT 数据源](../data/source-mvt.md) +- 准备好矢量瓦片服务(MVT 格式) + +## 核心概念 + +### 什么是矢量瓦片 + +矢量瓦片是将地理矢量数据按照瓦片金字塔结构切分的数据格式: + +- **瓦片金字塔**:不同缩放级别(zoom)对应不同精度的数据 +- **按需加载**:只加载当前视野范围内的瓦片 +- **矢量格式**:保留矢量数据,可在客户端动态样式化 +- **标准格式**:通常使用 MVT (Mapbox Vector Tile) 或 PBF 格式 + +### 矢量瓦片 vs 栅格瓦片 + +| 对比项 | 矢量瓦片 | 栅格瓦片 | +| ---------- | ------------------ | ---------------- | +| 数据格式 | 矢量数据(点线面) | 图片(PNG/JPG) | +| 文件大小 | 小(通常 < 50KB) | 较大(10-100KB) | +| 样式灵活性 | 可动态改变样式 | 样式固定 | +| 高清屏支持 | 完美支持 | 需要 @2x 图 | +| 数据映射 | 支持数据驱动样式 | 不支持 | +| 交互性 | 可点击、查询 | 不可交互 | + +## 支持的图层类型 + +L7 矢量瓦片支持以下图层类型: + +| 图层类型 | 描述 | 使用场景 | +| -------------- | ------------ | ------------------ | +| PointLayer | 矢量点瓦片 | POI、定位点、标注 | +| LineLayer | 矢量线瓦片 | 道路、边界、管线 | +| PolygonLayer | 矢量面瓦片 | 行政区、建筑、地块 | +| MaskLayer | 矢量掩模瓦片 | 配合栅格图层使用 | +| TileDebugLayer | 瓦片调试图层 | 查看瓦片加载状态 | + +## 基础用法 + +### 1. 点图层 + +```javascript +import { Scene, PointLayer, Source } from '@antv/l7'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120, 30], + zoom: 10, + }), +}); + +// 创建 MVT 数据源 +const vectorSource = new Source( + 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/rs_l7/{z}/{x}/{y}.pbf', + { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 14, + extent: [-180, -85.051129, 179, 85.051129], + }, + }, +); + +// 创建点图层 +const pointLayer = new PointLayer({ + featureId: 'id', // 要素唯一标识字段 + sourceLayer: 'points', // MVT 数据源中的图层名称 +}) + .source(vectorSource) + .shape('circle') + .size(8) + .color('category', ['#5B8FF9', '#5AD8A6', '#5D7092']); + +scene.on('loaded', () => { + scene.addLayer(pointLayer); +}); +``` + +### 2. 线图层 + +```javascript +import { LineLayer } from '@antv/l7'; + +const lineLayer = new LineLayer({ + featureId: 'id', + sourceLayer: 'roads', // 道路图层 +}) + .source(vectorSource) + .shape('line') + .size('type', (type) => { + // 根据道路类型设置宽度 + const widthMap = { + highway: 5, + main: 3, + secondary: 2, + default: 1, + }; + return widthMap[type] || widthMap.default; + }) + .color('type', { + highway: '#FF6B6B', + main: '#4ECDC4', + secondary: '#95E1D3', + default: '#CCCCCC', + }); + +scene.addLayer(lineLayer); +``` + +### 3. 面图层 + +```javascript +import { PolygonLayer } from '@antv/l7'; + +const polygonLayer = new PolygonLayer({ + featureId: 'adcode', + sourceLayer: 'regions', // 行政区图层 +}) + .source(vectorSource) + .shape('fill') + .color('population', ['#FFF5EB', '#FEE6CE', '#FDD0A2', '#FDAE6B', '#FD8D3C', '#E6550D']) + .style({ + opacity: 0.8, + }); + +scene.addLayer(polygonLayer); +``` + +## 配置选项 + +### 图层构造参数 + +```typescript +interface IVectorTileLayerOptions { + featureId?: string; // 要素唯一标识字段(用于高亮和选中) + sourceLayer: string; // MVT 数据源中的图层名称 + // ... 其他基础图层参数 +} +``` + +### Source 配置 + +详见 [MVT 数据源](../data/source-mvt.md) + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', // 固定为 'mvt' + tileSize: 256, // 瓦片大小,默认 256 + minZoom: 0, // 最小缩放级别 + maxZoom: 14, // 最大缩放级别 + zoomOffset: 0, // 层级偏移 + extent: [ + // 数据边界 + -180, -85.051129, 179, 85.051129, + ], + }, +}); +``` + +## 高级用法 + +### 1. 数据驱动样式 + +矢量瓦片支持完整的数据映射能力: + +```javascript +const layer = new PolygonLayer({ + sourceLayer: 'buildings', +}) + .source(vectorSource) + .shape('fill') + .color('height', ['#ffffcc', '#c7e9b4', '#7fcdbb', '#41b6c4', '#2c7fb8', '#253494']) + .style({ + opacity: 0.9, + }) + .scale('height', { + type: 'quantile', // 分位数分类 + }); +``` + +### 2. 多图层数据源复用 + +```javascript +// 创建共享数据源 +const vectorSource = new Source('https://tiles.example.com/{z}/{x}/{y}.pbf', { + parser: { + type: 'mvt', + maxZoom: 14, + }, +}); + +// 多个图层复用同一数据源 +const buildingLayer = new PolygonLayer({ + sourceLayer: 'buildings', +}) + .source(vectorSource) + .color('#5B8FF9'); + +const roadLayer = new LineLayer({ + sourceLayer: 'roads', +}) + .source(vectorSource) + .color('#5AD8A6'); + +// 只会请求一份瓦片数据 +scene.addLayer(buildingLayer); +scene.addLayer(roadLayer); +``` + +### 3. 配合掩模图层 + +矢量掩模图层常用于裁剪栅格图层: + +```javascript +import { MaskLayer, RasterLayer } from '@antv/l7'; + +// 创建掩模图层 +const maskLayer = new MaskLayer({ + sourceLayer: 'boundary', +}).source(vectorSource); + +// 创建栅格图层 +const rasterLayer = new RasterLayer({ + mask: true, // 启用掩模 +}).source(rasterUrl, { + parser: { type: 'rasterTile' }, +}); + +scene.addLayer(maskLayer); +scene.addLayer(rasterLayer); +``` + +### 4. 瓦片调试 + +使用调试图层查看瓦片加载状态: + +```javascript +import { TileDebugLayer } from '@antv/l7'; + +const debugLayer = new TileDebugLayer(); +scene.addLayer(debugLayer); + +// 调试图层会显示: +// - 瓦片边界 +// - 瓦片坐标 (z/x/y) +// - 加载状态 +``` + +### 5. 交互事件 + +矢量瓦片支持完整的交互能力: + +```javascript +// 鼠标悬停高亮 +layer.on('mousemove', (e) => { + if (e.feature) { + layer.setActive(e.feature.id); + } +}); + +// 点击显示详情 +layer.on('click', (e) => { + const { feature } = e; + const popup = new Popup().setLnglat(e.lngLat).setHTML(` +

${feature.name}

+

人口: ${feature.population}

+ `); + scene.addPopup(popup); +}); +``` + +> 💡 矢量瓦片图层同样支持所有通用图层方法(show/hide/setIndex 等)和事件,详见[图层通用方法和事件](./layer-common-api.md)。 + +```` + +## 实际应用场景 + +### 1. 全国行政区可视化 + +```javascript +const chinaLayer = new PolygonLayer({ + featureId: 'adcode', + sourceLayer: 'admin_bounds', +}) + .source( + 'https://tiles.example.com/china/{z}/{x}/{y}.pbf', + { + parser: { + type: 'mvt', + maxZoom: 12, + }, + } + ) + .shape('fill') + .color('gdp', [ + '#f7fcf5', + '#c7e9c0', + '#74c476', + '#238b45', + '#00441b', + ]) + .style({ + opacity: 0.8, + }) + .scale('gdp', { + type: 'quantile', + }); + +// 边界线图层 +const boundaryLayer = new LineLayer({ + sourceLayer: 'admin_bounds', +}) + .source(chinaLayer.getSource()) + .shape('line') + .size(1) + .color('#333'); + +scene.addLayer(chinaLayer); +scene.addLayer(boundaryLayer); + +// 适配到数据范围 +scene.on('loaded', () => { + chinaLayer.fitBounds(); +}); +```` + +### 2. 城市道路网络 + +```javascript +// 不同等级道路使用不同样式 +const highwayLayer = new LineLayer({ + sourceLayer: 'roads', + zIndex: 3, +}) + .source(vectorSource) + .filter((feature) => feature.type === 'highway') + .shape('line') + .size(4) + .color('#FF6B6B') + .style({ + opacity: 0.9, + }); + +const mainRoadLayer = new LineLayer({ + sourceLayer: 'roads', + zIndex: 2, +}) + .source(vectorSource) + .filter((feature) => feature.type === 'main') + .shape('line') + .size(2) + .color('#4ECDC4'); + +const secondaryRoadLayer = new LineLayer({ + sourceLayer: 'roads', + zIndex: 1, +}) + .source(vectorSource) + .filter((feature) => feature.type === 'secondary') + .shape('line') + .size(1) + .color('#95E1D3'); + +scene.addLayer(highwayLayer); +scene.addLayer(mainRoadLayer); +scene.addLayer(secondaryRoadLayer); +``` + +### 3. POI 密度热力 + +```javascript +const poiLayer = new PointLayer({ + sourceLayer: 'pois', +}) + .source(vectorSource) + .shape('circle') + .size('category', (type) => { + const sizeMap = { + restaurant: 10, + shop: 8, + bank: 12, + default: 6, + }; + return sizeMap[type] || sizeMap.default; + }) + .color('category', { + restaurant: '#FF6B6B', + shop: '#4ECDC4', + bank: '#FFD93D', + default: '#95E1D3', + }) + .style({ + opacity: 0.8, + }); + +// 根据缩放级别控制显示 +poiLayer.setMinZoom(12); // 地图缩放到 12 级以上才显示 +``` + +### 4. 建筑 3D 效果 + +```javascript +const buildingLayer = new PolygonLayer({ + sourceLayer: 'buildings', +}) + .source(vectorSource) + .shape('extrude') // 拉伸成 3D + .size('height', (h) => h * 0.3) // 根据建筑高度拉伸 + .color('usage', { + residential: '#5B8FF9', + commercial: '#5AD8A6', + industrial: '#5D7092', + }) + .style({ + opacity: 0.9, + pickingBuffer: 5, + }); + +scene.addLayer(buildingLayer); +``` + +## 性能优化 + +### 1. 控制瓦片范围 + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + // 限制请求范围(中国区域) + extent: [73.66, 3.86, 135.05, 53.55], + // 限制最大层级(避免请求过细瓦片) + maxZoom: 14, + }, +}); +``` + +### 2. 合理设置缩放范围 + +```javascript +// 避免在小缩放级别显示过多细节 +layer.setMinZoom(10); + +// 避免在大缩放级别请求过多瓦片 +layer.setMaxZoom(16); +``` + +### 3. 数据源复用 + +```javascript +// ✅ 推荐:多图层共享数据源 +const source = new Source(url, options); +layer1.source(source); +layer2.source(source); + +// ❌ 避免:重复创建数据源 +layer1.source(url, options); +layer2.source(url, options); // 会重复请求 +``` + +### 4. 按需加载图层 + +```javascript +// 根据缩放级别动态切换图层 +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + + if (zoom < 12) { + // 小级别:显示概览数据 + summaryLayer.show(); + detailLayer.hide(); + } else { + // 大级别:显示详细数据 + summaryLayer.hide(); + detailLayer.show(); + } +}); +``` + +## 常见问题 + +### Q: 如何指定 sourceLayer? + +A: sourceLayer 是 MVT 数据源中包含的图层名称,需要查看瓦片数据结构确定。可以使用调试工具查看: + +```javascript +const debugLayer = new TileDebugLayer(); +scene.addLayer(debugLayer); + +// 或查看网络请求的 PBF 文件内容 +``` + +### Q: 瓦片不显示怎么办? + +A: 检查以下几点: + +1. URL 模板是否正确(包含 {z}/{x}/{y}) +2. sourceLayer 名称是否正确 +3. extent 范围是否包含当前视野 +4. maxZoom/minZoom 是否合理 +5. 网络请求是否成功(查看 Network 面板) + +### Q: 矢量瓦片和 GeoJSON 如何选择? + +A: + +| 场景 | 推荐方案 | +| ------------- | -------- | +| 数据量 < 1万 | GeoJSON | +| 数据量 > 10万 | 矢量瓦片 | +| 需要全局分析 | GeoJSON | +| 只需可视化 | 矢量瓦片 | +| 需要离线使用 | GeoJSON | +| 需要按需加载 | 矢量瓦片 | + +### Q: 如何自定义瓦片请求? + +A: 使用 `getCustomData` 方法: + +```javascript +const source = new Source(url, { + parser: { + type: 'mvt', + getCustomData: (tile, callback) => { + const { x, y, z } = tile; + const customUrl = `https://api.example.com/tile?z=${z}&x=${x}&y=${y}&token=xxx`; + + fetch(customUrl) + .then((res) => res.arrayBuffer()) + .then((data) => callback(null, data)) + .catch((err) => callback(err, null)); + }, + }, +}); +``` + +### Q: 如何处理不同缩放级别的数据差异? + +A: 可以根据 zoom 动态调整样式: + +```javascript +scene.on('zoomchange', () => { + const zoom = scene.getZoom(); + + if (zoom < 10) { + layer.size(4); // 小缩放级别用小尺寸 + } else if (zoom < 14) { + layer.size(6); + } else { + layer.size(8); // 大缩放级别用大尺寸 + } +}); +``` + +## 注意事项 + +⚠️ **数据格式**:确保瓦片服务返回标准的 MVT/PBF 格式 + +⚠️ **坐标系统**:MVT 使用 Web Mercator 投影(EPSG:3857) + +⚠️ **图层名称**:sourceLayer 必须与 MVT 数据中的图层名称完全匹配 + +⚠️ **缩放范围**:合理设置 minZoom 和 maxZoom 避免性能问题 + +⚠️ **数据边界**:extent 参数用于限制请求范围,提升性能 + +⚠️ **复用数据源**:多个图层使用同一瓦片数据时,务必复用 Source 对象 + +## 相关技能 + +- [图层通用方法和事件](./layer-common-api.md) +- [MVT 数据源](../data/source-mvt.md) +- [栅格瓦片图层](./tile-raster.md) +- [性能优化](../performance/optimization.md) + +## 在线示例 + +查看更多示例:[L7 矢量瓦片示例](https://l7.antv.antgroup.com/examples/tile/vector) diff --git a/skills/l7/references/performance/optimization.md b/skills/l7/references/performance/optimization.md new file mode 100644 index 0000000..115be01 --- /dev/null +++ b/skills/l7/references/performance/optimization.md @@ -0,0 +1,375 @@ +# Performance Optimization Guide + +L7 性能优化完整指南。 + +## 数据优化 + +### 1. 数据过滤 + +```javascript +// 在 source 时过滤 +layer.source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + transforms: [ + { + type: 'filter', + callback: (item) => { + // 只显示值大于阈值的数据 + return item.value > 100; + }, + }, + ], +}); + +// 按视窗范围过滤 +scene.on('mapmove', () => { + const bounds = scene.getBounds(); + const filteredData = data.filter( + (item) => + item.lng >= bounds[0][0] && + item.lng <= bounds[1][0] && + item.lat >= bounds[0][1] && + item.lat <= bounds[1][1], + ); + layer.setData(filteredData); +}); +``` + +### 2. 数据聚合 + +```javascript +// 使用聚合图层 +import { HeatmapLayer } from '@antv/l7'; + +// 替代大量点的 PointLayer +const heatmap = new HeatmapLayer() + .source(massiveData, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + transforms: [ + { + type: 'hexagon', // 六边形聚合 + size: 500, // 聚合大小 + field: 'value', + method: 'sum', + }, + ], + }) + .size('sum', [10, 50]) + .color('sum', ['#ffffb2', '#fd8d3c', '#f03b20', '#bd0026']); + +scene.addLayer(heatmap); +``` + +### 3. 数据抽稀 + +```javascript +// 按比例抽样 +const sampledData = data.filter((_, index) => index % 10 === 0); + +// 按值抽样(只显示重要数据) +const sampledData = data.sort((a, b) => b.value - a.value).slice(0, 1000); // 只显示前 1000 个重要点 +``` + +### 4. 简化几何形状 + +```javascript +import * as turf from '@turf/turf'; + +// 简化多边形 +const simplified = turf.simplify(polygonData, { + tolerance: 0.01, // 简化容差 + highQuality: false, // 快速简化 +}); + +layer.source(simplified, { + parser: { type: 'geojson' }, +}); +``` + +## 图层管理 + +### 1. 按需显示图层 + +```javascript +// 根据缩放级别显示不同细节 +scene.on('zoom', () => { + const zoom = scene.getZoom(); + + if (zoom < 10) { + // 低缩放:显示汇总数据 + provinceLayer.show(); + cityLayer.hide(); + districtLayer.hide(); + } else if (zoom < 13) { + // 中缩放:显示城市数据 + provinceLayer.hide(); + cityLayer.show(); + districtLayer.hide(); + } else { + // 高缩放:显示详细数据 + provinceLayer.hide(); + cityLayer.hide(); + districtLayer.show(); + } +}); +``` + +### 2. 合并图层 + +```javascript +// ❌ 低效:多个小图层 +categories.forEach((cat) => { + const layer = new PointLayer().source(data.filter((d) => d.category === cat)); + scene.addLayer(layer); +}); + +// ✅ 高效:合并为一个图层 +const layer = new PointLayer().source(data).color('category', categoryColors); + +scene.addLayer(layer); +``` + +### 3. 移除不需要的图层 + +```javascript +// 移除图层释放资源 +scene.removeLayer(layer); + +// 清空所有图层 +scene.removeAllLayer(); +``` + +## 渲染优化 + +### 1. 降低渲染频率 + +```javascript +// 使用防抖 +import { debounce } from 'lodash'; + +const updateLayer = debounce(() => { + layer.setData(newData); +}, 300); + +// 在快速交互时使用 +inputElement.addEventListener('input', updateLayer); +``` + +### 2. 关闭不必要的特性 + +```javascript +layer.source(data).shape('circle').size(5).color('#5B8FF9').style({ + opacity: 1.0, // 完全不透明,避免混合计算 + blend: 'normal', // 普通混合模式 +}); +``` + +### 3. 使用 GPU 加速 + +```javascript +// L7 默认使用 WebGL,确保使用 GPU 友好的配置 + +// ✅ 固定大小(GPU 友好) +layer.size(5); + +// ⚠️ 动态大小(需要更多计算) +layer.size('value', [5, 20]); +``` + +## 瓦片服务 + +对于海量数据,使用瓦片服务: + +```javascript +import { PointLayer } from '@antv/l7'; + +// 矢量瓦片 +const tileLayer = new PointLayer() + .source('https://tiles.example.com/{z}/{x}/{y}.mvt', { + parser: { + type: 'mvt', + tileSize: 256, + maxZoom: 14, + }, + }) + .shape('circle') + .size(5) + .color('#5B8FF9'); + +scene.addLayer(tileLayer); +``` + +## 内存管理 + +### 1. 及时清理资源 + +```javascript +// 移除图层前清理事件监听 +layer.off('click'); +layer.off('mousemove'); + +// 移除图层 +scene.removeLayer(layer); + +// 销毁场景 +scene.destroy(); +``` + +### 2. 避免内存泄漏 + +```javascript +// ❌ 可能导致内存泄漏 +scene.on('mapmove', () => { + const newLayer = new PointLayer(); // 每次都创建新图层 + scene.addLayer(newLayer); +}); + +// ✅ 复用图层 +const layer = new PointLayer(); +scene.addLayer(layer); + +scene.on('mapmove', () => { + layer.setData(newData); // 更新数据而不是创建新图层 +}); +``` + +### 3. 分批加载数据 + +```javascript +// 分批加载大数据 +async function loadDataInBatches(dataUrl, batchSize = 1000) { + let offset = 0; + let hasMore = true; + + while (hasMore) { + const response = await fetch(`${dataUrl}?offset=${offset}&limit=${batchSize}`); + const batch = await response.json(); + + if (batch.length === 0) { + hasMore = false; + } else { + // 添加到图层 + const currentData = layer.getSource().data; + layer.setData([...currentData, ...batch]); + + offset += batchSize; + } + } +} +``` + +## 性能监控 + +### 1. FPS 监控 + +```javascript +let frameCount = 0; +let lastTime = Date.now(); + +scene.on('afterrender', () => { + frameCount++; + const now = Date.now(); + + if (now - lastTime >= 1000) { + const fps = frameCount; + console.log('FPS:', fps); + + frameCount = 0; + lastTime = now; + } +}); +``` + +### 2. 渲染时间 + +```javascript +console.time('render'); + +scene.on('loaded', () => { + layer.source(data).shape('circle').size(5).color('#5B8FF9'); + + scene.addLayer(layer); + + console.timeEnd('render'); +}); +``` + +### 3. 数据量统计 + +```javascript +// 图层数量 +console.log('图层数:', scene.getLayers().length); + +// 每个图层的数据量 +scene.getLayers().forEach((layer) => { + const source = layer.getSource(); + console.log(layer.name, '数据量:', source.data.dataArray.length); +}); +``` + +## 最佳实践 + +### 1. 数据量建议 + +| 数据量 | 推荐方案 | +| ------------ | --------------- | +| < 1万 | 直接渲染 | +| 1万 - 10万 | 数据过滤 + 抽稀 | +| 10万 - 100万 | 聚合 + 瓦片 | +| > 100万 | 瓦片服务 | + +### 2. 图层数量建议 + +- 尽量控制在 **10 个图层以内** +- 超过 20 个图层考虑合并或按需加载 +- 使用图层分组管理 + +### 3. 事件监听优化 + +```javascript +// ❌ 为每个要素绑定事件 +data.forEach((item) => { + // 创建单独图层并绑定事件 +}); + +// ✅ 在图层级别绑定事件 +layer.on('click', (e) => { + const feature = e.feature; + // 处理点击 +}); +``` + +### 4. 样式更新优化 + +```javascript +// ❌ 频繁更新样式 +slider.addEventListener('input', (e) => { + layer.size(e.target.value); + scene.render(); +}); + +// ✅ 防抖处理 +const updateSize = debounce((value) => { + layer.size(value); +}, 100); + +slider.addEventListener('input', (e) => { + updateSize(e.target.value); +}); +``` + +## 性能检查清单 + +- [ ] 数据量是否合理?考虑过滤或聚合 +- [ ] 是否有不必要的图层?考虑合并 +- [ ] 是否按需显示图层?根据缩放级别控制 +- [ ] 是否有内存泄漏?检查事件监听和图层清理 +- [ ] 样式更新是否频繁?使用防抖或节流 +- [ ] 是否使用了瓦片服务?对于海量数据考虑瓦片 +- [ ] 几何形状是否过于复杂?考虑简化 +- [ ] 监控 FPS 是否正常?目标 ≥ 30 FPS + +## 相关文档 + +- [data/source-parser.md](../data/source-parser.md) - 数据过滤和转换 +- [layers/heatmap.md](../layers/heatmap.md) - 热力图聚合 diff --git a/skills/l7/references/visual/mapping.md b/skills/l7/references/visual/mapping.md new file mode 100644 index 0000000..3f05c16 --- /dev/null +++ b/skills/l7/references/visual/mapping.md @@ -0,0 +1,547 @@ +--- +skill_id: visual-mapping +skill_name: 视觉映射 +category: visual +difficulty: intermediate +tags: [color, size, shape, scale, mapping, visual-encoding] +dependencies: [scene-initialization, point-layer] +version: 2.x +--- + +# 视觉映射 + +## 技能描述 + +将数据字段映射到图层的视觉属性(颜色、大小、形状等),实现数据的可视化编码。 + +## 何时使用 + +- ✅ 根据数值字段设置不同颜色(热力分布、分级着色) +- ✅ 根据数据值调整图形大小(气泡图、比例符号) +- ✅ 使用不同形状表示分类数据(POI 类型、事件类别) +- ✅ 多维数据可视化(颜色+大小+形状组合) +- ✅ 动态数据范围映射(自适应色阶) + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 已创建图层([点图层](../layers/point.md)、[线图层](../layers/line.md)等) +- 数据中包含用于映射的字段 + +## 核心概念 + +### 映射类型 + +| 映射方式 | 数据类型 | 适用场景 | 示例 | +| -------- | --------- | ---------- | --------------------------------------------- | +| 固定值 | - | 统一样式 | `.color('#5B8FF9')` | +| 字段映射 | 连续/分类 | 数据驱动 | `.color('temperature', [...])` | +| 回调函数 | 任意 | 自定义逻辑 | `.color(d => d.value > 100 ? 'red' : 'blue')` | + +## 代码示例 + +### 颜色映射 (color) + +#### 1. 固定颜色 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9'); // 所有点使用相同颜色 +``` + +#### 2. 字段映射 - 连续数值 + +```javascript +// 根据温度值映射颜色(蓝色到红色渐变) +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('temperature', [ + '#313695', // 低温 + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', // 高温 + ]); +``` + +#### 3. 字段映射 - 分类数据 + +```javascript +// 根据类型字段映射不同颜色 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('type', ['#FF6B6B', '#4ECDC4', '#95E1D3', '#F38181', '#CCCCCC']); + +// 如果需要固定 domain 顺序,可以设置 scale +layer + .scale('type', { + type: 'cat', + domain: ['餐饮', '购物', '娱乐', '酒店', '其他'], + }) + .color('type', ['#FF6B6B', '#4ECDC4', '#95E1D3', '#F38181', '#CCCCCC']); +``` + +#### 4. 回调函数映射 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color((feature) => { + const value = feature.value; + if (value > 1000) return '#d73027'; + if (value > 500) return '#fdae61'; + if (value > 100) return '#fee090'; + return '#abd9e9'; + }); +``` + +### 大小映射 (size) + +#### 1. 固定大小 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(15) // 所有点大小为 15 + .color('#5B8FF9'); +``` + +#### 2. 字段映射 - 数值范围 + +```javascript +// 根据人口数量映射点的大小 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size('population', [5, 50]) // 人口越多,点越大(5-50像素) + .color('#5B8FF9'); +``` + +#### 3. 字段映射 - 3D 高度 + +```javascript +// 3D 柱状图:根据数值映射高度 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('cylinder') + .size('value', [0, 1000]) // 映射到 0-1000 米高度 + .color('value', ['#ffffcc', '#800026']) + .style({ + opacity: 0.8, + }); +``` + +#### 4. 回调函数映射 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size((feature) => { + // 根据业务逻辑自定义大小 + const value = feature.sales; + return Math.sqrt(value) / 10; // 平方根缩放 + }) + .color('#5B8FF9'); +``` + +### 形状映射 (shape) + +#### 1. 固定形状 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') // 圆形 + .size(10) + .color('#5B8FF9'); +``` + +#### 2. 字段映射 - 分类形状 + +```javascript +// 根据类型使用不同形状 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('category', { + A类: 'circle', + B类: 'triangle', + C类: 'square', + D类: 'hexagon', + }) + .size(15) + .color('category', ['#FF6B6B', '#4ECDC4', '#95E1D3', '#F38181']); +``` + +#### 3. 图标映射 + +```javascript +// 使用自定义图标 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('icon', 'category') // 根据 category 字段映射图标 + .size(20) + .style({ + A类: 'restaurant', + B类: 'shop', + C类: 'hotel', + }); +``` + +### 多维映射组合 + +```javascript +// 同时映射颜色、大小、形状 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('type', { + 高风险: 'triangle', + 中风险: 'square', + 低风险: 'circle', + }) + .size('impact', [5, 30]) // 影响程度控制大小 + .color('severity', ['#fee5d9', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15']) + .style({ + opacity: 0.8, + strokeWidth: 2, + stroke: '#fff', + }); +``` + +## 色阶配置 + +### 内置色阶 + +```javascript +// ColorBrewer 色阶 +import { scales } from '@antv/l7'; + +layer.color('value', scales.quantize.Blues); // 蓝色系 +layer.color('value', scales.quantize.Reds); // 红色系 +layer.color('value', scales.quantize.Greens); // 绿色系 +layer.color('value', scales.diverging.RdYlBu); // 红-黄-蓝发散色 +``` + +### 自定义色阶 + +```javascript +// 线性插值色阶 +const colorScale = [ + '#f7fbff', + '#deebf7', + '#c6dbef', + '#9ecae1', + '#6baed6', + '#4292c6', + '#2171b5', + '#08519c', + '#08306b', +]; + +// threshold 类型示例 +layer + .scale('value', { + type: 'threshold', + domain: [100, 500, 1000, 5000], // N个断点对应 N+1 个颜色 + }) + .color('value', ['#ffffcc', '#c7e9b4', '#7fcdbb', '#41b6c4', '#2c7fb8', '#253494']); +``` + +## 高级用法 + +### 动态更新映射 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('value', ['#ffffcc', '#800026']); + +// 切换到另一个字段 +function switchField(fieldName) { + layer.color(fieldName, ['#fee5d9', '#a50f15']); + scene.render(); +} + +// 更新颜色范围 +function updateColors(colors) { + layer.color('value', colors); + scene.render(); +} +``` + +### Scale 配置 + +L7 使用 `scale()` 方法设置数据字段的映射方式,将地图数据值(数字、日期、类别等)转换为视觉属性(颜色、大小、形状等)。 + +```javascript +// 先配置 scale,再进行 color/size 映射 +layer + .scale('mag', { + type: 'linear', + domain: [1, 50], + }) + .color('id', ['#f00', '#ff0']) + .size('mag', [1, 80]); +``` + +### Scale 配置参数 + +```typescript +interface IScaleConfig { + type: + | 'linear' + | 'log' + | 'pow' + | 'quantile' + | 'quantize' + | 'threshold' + | 'cat' + | 'time' + | 'sequential' + | 'diverging' + | 'identity'; + domain?: any[]; // 数据值域范围 + range?: any[]; // 视觉值映射范围(通常使用 color/size 方法设置) + unknown?: string; // 未知值的默认映射 + nice?: boolean; // 是否优化边界值 + clamp?: boolean; // 是否限制在范围内 +} +``` + +### Scale 使用规则 + +| Scale 类型 | domain 设置 | 说明 | +| ---------- | ----------- | --------------------------------- | +| linear | 可选 | 不设置会自动计算 min、max | +| quantize | 可选 | 不设置会自动计算 min、max | +| quantile | 不可设置 | 必须自动计算,需要全量数据 | +| threshold | 必须设置 | N 个断点需要 N+1 个颜色 | +| diverging | 可选 | 不设置会自动计算 min、middle、max | +| cat | 可选 | 不设置会自动获取唯一值 | + +### 使用示例 + +```javascript +// 组合使用 scale 和映射 +const layer = new PointLayer() + .source(data, { + parser: { type: 'json', x: 'lng', y: 'lat' }, + }) + .scale('population', { + type: 'linear', + domain: [0, 10000000], + }) + .scale('category', { + type: 'cat', + }) + .size('population', [5, 50]) + .color('category', ['#FF6B6B', '#4ECDC4', '#95E1D3']); +``` + +## 常见场景 + +### 1. 人口密度分级着色 + +```javascript +const populationLayer = new PolygonLayer() + .source(districtData, { parser: { type: 'geojson' } }) + .shape('fill') + .scale('population_density', { + type: 'quantile', // 分位数映射 + }) + .color('population_density', [ + '#f7fbff', + '#deebf7', + '#c6dbef', + '#9ecae1', + '#6baed6', + '#4292c6', + '#2171b5', + '#084594', + ]) + .style({ + opacity: 0.8, + }); +``` + +### 2. 气泡图(双变量映射) + +```javascript +// 颜色表示类型,大小表示数量 +const bubbleLayer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size('amount', [10, 80]) // 交易金额 + .color('category', ['#FF6B6B', '#4ECDC4', '#95E1D3', '#F38181']) // 类别 + .style({ + opacity: 0.6, + strokeWidth: 2, + stroke: '#fff', + }); +``` + +### 3. 热力强度可视化 + +```javascript +const heatLayer = new PointLayer() + .source(eventData, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(15) + .color('intensity', ['#440154', '#31688e', '#35b779', '#fde724']) + .style({ + opacity: 0.8, + }); +``` + +### 4. 三维数据可视化 + +```javascript +// 建筑高度 + 颜色表示用途 +const buildingLayer = new PolygonLayer() + .source(buildingData, { parser: { type: 'geojson' } }) + .shape('extrude') + .size('height', [10, 500]) // 建筑高度 + .scale('usage', { + type: 'cat', + domain: ['住宅', '商业', '办公', '工业', '其他'], + }) + .color('usage', ['#FFE5B4', '#FF6B6B', '#4ECDC4', '#95E1D3', '#CCCCCC']) + .style({ + opacity: 0.8, + }); +``` + +## 性能优化 + +### 1. 避免频繁更新 + +```javascript +// ❌ 不好的做法:频繁重新映射 +data.forEach((item) => { + layer.color('value', colors); + scene.render(); +}); + +// ✅ 好的做法:批量更新 +layer.color('value', colors); +scene.render(); +``` + +### 简化映射逻辑 + +```javascript +// ❌ 复杂的回调函数会影响性能 +layer.color((d) => { + // 大量计算... + return complexCalculation(d); +}); + +// ✅ 预处理数据,使用 scale 配置 +data.forEach((d) => (d.colorCategory = complexCalculation(d))); +layer.scale('colorCategory', { type: 'cat' }).color('colorCategory', colors); +``` + +### 3. 使用合适的 Scale 类型 + +```javascript +// 大数据量时,quantize 比 quantile 快(不需要排序数据) +layer + .scale('value', { + type: 'quantize', + domain: [0, 100], + }) + .color('value', colorScale); +``` + +## 注意事项 + +⚠️ **数据范围**:确保映射字段在数据中存在且值有效 + +⚠️ **颜色数量**:色阶颜色数量建议 5-10 个,过多难以区分 + +⚠️ **可访问性**:考虑色盲友好的配色方案,避免红绿搭配 + +⚠️ **数据类型**:连续数据用渐变色,分类数据用区分色 + +⚠️ **Scale 类型**:根据数据分布选择合适的 scale 类型 + +⚠️ **默认值**:回调函数要处理空值情况,返回默认颜色 + +## 常见问题 + +### Q: 为什么颜色没有显示? + +A: 检查:1) 字段名是否正确;2) 数据中是否有该字段;3) 颜色数组是否有效 + +### Q: 如何实现数据驱动的透明度? + +A: 在 `style()` 中使用回调函数: + +```javascript +layer.style({ + opacity: (feature) => feature.value / 100, +}); +``` + +### Q: 映射后颜色分布不均匀? + +A: 尝试不同的 scale 类型: + +```javascript +// 使用 quantile 而不是 linear +layer.scale('value', { type: 'quantile' }).color('value', colors); + +// 或手动指定 domain 范围 +layer + .scale('value', { + type: 'linear', + domain: [0, 1000], // 明确数据范围 + }) + .color('value', colors); +``` + +### Q: 如何自定义图例? + +A: 参考 [components.md](../interaction/components.md) 中的图例组件 + +### Q: 能否同时映射多个字段到一个属性? + +A: 使用回调函数自定义逻辑: + +```javascript +layer.color((d) => { + if (d.type === 'A' && d.value > 100) return 'red'; + if (d.type === 'B') return 'blue'; + return 'gray'; +}); +``` + +## 相关技能 + +- [样式配置](./style.md) +- [点图层](../layers/point.md) +- [线图层](../layers/line.md) +- [面图层](../layers/polygon.md) +- [组件](../interaction/components.md) + +## 在线示例 + +查看更多示例: [L7 官方示例 - 数据映射](https://l7.antv.antgroup.com/examples) diff --git a/skills/l7/references/visual/style.md b/skills/l7/references/visual/style.md new file mode 100644 index 0000000..f983ac3 --- /dev/null +++ b/skills/l7/references/visual/style.md @@ -0,0 +1,581 @@ +--- +skill_id: layer-style +skill_name: 图层样式配置 +category: visual +difficulty: beginner +tags: [style, opacity, stroke, texture, blend, animation] +dependencies: [scene-initialization] +version: 2.x +--- + +# 图层样式配置 + +## 技能描述 + +配置图层的视觉样式属性,包括透明度、描边、纹理、混合模式等,提升可视化效果。 + +## 何时使用 + +- ✅ 调整图层透明度(叠加图层、突出重点) +- ✅ 添加描边效果(边界清晰、视觉层次) +- ✅ 使用纹理填充(特殊视觉效果) +- ✅ 设置混合模式(光晕效果、数据叠加) +- ✅ 配置动画效果(动态展示、吸引注意) + +## 前置条件 + +- 已完成[场景初始化](../core/scene.md) +- 已创建图层实例 + +## 代码示例 + +### 基础样式配置 + +```javascript +import { PointLayer } from '@antv/l7'; + +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9') + .style({ + opacity: 0.8, // 透明度 + strokeWidth: 2, // 描边宽度 + stroke: '#fff', // 描边颜色 + strokeOpacity: 1.0, // 描边透明度 + }); + +scene.addLayer(layer); +``` + +## 样式属性详解 + +### 1. 透明度 (Opacity) + +#### 整体透明度 + +```javascript +layer.style({ + opacity: 0.8, // 0 完全透明,1 完全不透明 +}); +``` + +#### 动态透明度(数据驱动) + +```javascript +layer.style({ + opacity: (feature) => { + return feature.value / 100; // 根据数值动态调整 + }, +}); +``` + +### 2. 描边样式 (Stroke) + +#### 基础描边 + +```javascript +// 点图层描边 +const pointLayer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(15) + .color('#5B8FF9') + .style({ + strokeWidth: 2, // 描边宽度(像素) + stroke: '#ffffff', // 描边颜色 + strokeOpacity: 1.0, // 描边透明度 + }); + +// 面图层描边 +const polygonLayer = new PolygonLayer() + .source(data, { parser: { type: 'geojson' } }) + .shape('fill') + .color('#5B8FF9') + .style({ + opacity: 0.6, + strokeWidth: 1, + stroke: '#333', + strokeOpacity: 0.8, + }); + +// 线图层样式 +const lineLayer = new LineLayer() + .source(data, { parser: { type: 'geojson' } }) + .shape('line') + .color('#5B8FF9') + .size(2) + .style({ + opacity: 0.8, + lineType: 'solid', // solid | dash + }); +``` + +#### 虚线样式 + +```javascript +const lineLayer = new LineLayer() + .source(data, { parser: { type: 'geojson' } }) + .shape('line') + .color('#FF6B6B') + .size(3) + .style({ + lineType: 'dash', + dashArray: [5, 5], // [实线长度, 间隔长度] + }); +``` + +### 3. 混合模式 (Blend) + +```javascript +const layer = new PointLayer({ + blend: 'additive', // normal | additive | subtractive | max +}) + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(20) + .color('#FF6B6B') + .style({ + opacity: 0.6, + }); +``` + +#### 混合模式说明 + +| 模式 | 效果 | 适用场景 | +| ------------- | -------- | ------------------ | +| `normal` | 默认混合 | 常规图层 | +| `additive` | 叠加变亮 | 热力效果、光晕效果 | +| `subtractive` | 叠加变暗 | 阴影效果 | +| `max` | 取最大值 | 密度分析 | + +### 4. 纹理填充 + +```javascript +// 使用图片纹理 +const polygonLayer = new PolygonLayer() + .source(data, { parser: { type: 'geojson' } }) + .shape('fill') + .style({ + texture: 'https://example.com/pattern.png', + textureBlend: 'normal', + opacity: 0.8, + }); +``` + +### 5. 3D 效果样式 + +#### 3D 柱状图 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('cylinder') + .size('value', [0, 500]) // 高度 + .color('value', ['#ffffcc', '#800026']) + .style({ + opacity: 0.8, + coverage: 0.8, // 覆盖度(柱子粗细) + angle: 0, // 旋转角度 + }); +``` + +#### 3D 建筑 + +```javascript +const buildingLayer = new PolygonLayer() + .source(buildingData, { parser: { type: 'geojson' } }) + .shape('extrude') + .size('height') + .color('#5B8FF9') + .style({ + opacity: 0.8, + pickingBuffer: 5, // 拾取缓冲区 + heightfixed: true, // 高度固定 + }); +``` + +### 6. 光晕效果 + +```javascript +const layer = new PointLayer({ + blend: 'additive', +}) + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(30) + .color('#FF6B6B') + .style({ + opacity: 0.3, + blur: 0.9, // 模糊程度,0-1 + }); +``` + +## 图层级别配置 + +### zIndex (层叠顺序) + +```javascript +const baseLayer = new PolygonLayer({ zIndex: 1 }).source(data).shape('fill').color('#f0f0f0'); + +const highlightLayer = new PolygonLayer({ zIndex: 2 }) + .source(highlightData) + .shape('fill') + .color('#FF6B6B'); + +scene.addLayer(baseLayer); +scene.addLayer(highlightLayer); // 显示在 baseLayer 上方 +``` + +### 图层可见性 + +```javascript +// 隐藏/显示图层 +layer.hide(); +layer.show(); + +// 检查可见性 +const isVisible = layer.isVisible(); + +// 切换可见性 +layer.toggleVisible(); +``` + +### 图层拾取控制 + +```javascript +layer.style({ + pickingBuffer: 5, // 拾取缓冲区大小(像素),增大可提高点击精度 +}); +``` + +## 常见场景 + +### 1. 半透明图层叠加 + +```javascript +// 底层:行政区划 +const districtLayer = new PolygonLayer({ zIndex: 1 }) + .source(districtData, { parser: { type: 'geojson' } }) + .shape('fill') + .color('name', ['#FEF0D9', '#FDD49E', '#FDBB84', '#FC8D59']) + .style({ + opacity: 0.6, + }); + +// 上层:道路网络 +const roadLayer = new LineLayer({ zIndex: 2 }) + .source(roadData, { parser: { type: 'geojson' } }) + .shape('line') + .color('#333333') + .size(2) + .style({ + opacity: 0.8, + }); + +scene.addLayer(districtLayer); +scene.addLayer(roadLayer); +``` + +### 2. 高亮选中效果 + +```javascript +const normalLayer = new PolygonLayer() + .source(data, { parser: { type: 'geojson' } }) + .shape('fill') + .color('#5B8FF9') + .style({ + opacity: 0.6, + strokeWidth: 1, + stroke: '#fff', + }); + +const highlightLayer = new PolygonLayer() + .source([], { parser: { type: 'geojson' } }) + .shape('fill') + .color('#FF6B6B') + .style({ + opacity: 0.8, + strokeWidth: 3, + stroke: '#fff', + }); + +scene.addLayer(normalLayer); +scene.addLayer(highlightLayer); + +// 点击高亮 +normalLayer.on('click', (e) => { + highlightLayer.setData(e.feature); +}); +``` + +### 3. 热力光晕效果 + +```javascript +const heatLayer = new PointLayer({ + blend: 'additive', + zIndex: 2, +}) + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size('value', [20, 100]) + .color('value', ['#440154', '#31688e', '#35b779', '#fde724']) + .style({ + opacity: 0.5, + blur: 0.8, + }); +``` + +### 4. 动画样式 + +```javascript +// 呼吸动画 +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(15) + .color('#FF6B6B') + .animate(true); // 开启动画 + +// 自定义动画 +layer.animate({ + enable: true, + type: 'wave', // wave | ripple + duration: 2000, // 动画时长(毫秒) + speed: 0.5, // 动画速度 + rings: 3, // 波纹圈数 +}); +``` + +### 5. 边界强调 + +```javascript +const boundaryLayer = new LineLayer() + .source(boundaryData, { parser: { type: 'geojson' } }) + .shape('line') + .color('#333') + .size(3) + .style({ + opacity: 1.0, + lineType: 'solid', + }); + +// 添加外发光效果 +const glowLayer = new LineLayer({ + blend: 'additive', +}) + .source(boundaryData, { parser: { type: 'geojson' } }) + .shape('line') + .color('#FF6B6B') + .size(8) + .style({ + opacity: 0.3, + blur: 0.8, + }); + +scene.addLayer(glowLayer); +scene.addLayer(boundaryLayer); +``` + +## 样式动态更新 + +### 运行时更新 + +```javascript +const layer = new PointLayer() + .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(10) + .color('#5B8FF9') + .style({ + opacity: 0.8, + }); + +scene.addLayer(layer); + +// 更新样式 +function updateOpacity(value) { + layer.style({ opacity: value }); + scene.render(); +} + +// UI 滑块控制 +document.getElementById('opacity-slider').addEventListener('input', (e) => { + updateOpacity(parseFloat(e.target.value)); +}); +``` + +### 批量样式更新 + +```javascript +function updateLayerStyle(options) { + layer.style({ + opacity: options.opacity || 0.8, + strokeWidth: options.strokeWidth || 2, + stroke: options.stroke || '#fff', + }); + scene.render(); +} + +// 使用 +updateLayerStyle({ + opacity: 0.6, + strokeWidth: 3, + stroke: '#333', +}); +``` + +## 性能优化 + +### 1. 避免频繁样式更新 + +```javascript +// ❌ 不好的做法 +setInterval(() => { + layer.style({ opacity: Math.random() }); + scene.render(); +}, 16); + +// ✅ 好的做法:使用动画 +layer.animate({ + enable: true, + interval: 0.5, + duration: 2000, +}); +``` + +### 2. 简化样式计算 + +```javascript +// ❌ 复杂的回调函数 +layer.style({ + opacity: (feature) => { + // 大量计算... + return complexCalculation(feature); + }, +}); + +// ✅ 预处理数据 +data.forEach((d) => (d.opacityValue = complexCalculation(d))); +layer.color('opacityValue', [0.1, 1.0]); +``` + +### 3. 合理使用混合模式 + +```javascript +// additive 模式会增加渲染负担,仅在需要时使用 +const layer = new PointLayer({ + blend: 'normal', // 默认模式性能最好 +}); +``` + +## 注意事项 + +⚠️ **透明度范围**:opacity 值范围 0-1,超出会被裁剪 + +⚠️ **描边性能**:大量数据时描边会影响性能,考虑简化 + +⚠️ **混合模式**:additive 模式对性能有影响,谨慎使用 + +⚠️ **样式更新**:更新样式后需调用 `scene.render()` 才能生效 + +⚠️ **zIndex 范围**:建议使用 0-999,过大可能导致问题 + +⚠️ **3D 效果**:3D 图层的渲染成本高于 2D 图层 + +## 常见问题 + +### Q: 修改样式后不生效? + +A: 确保调用了 `scene.render()` 触发重绘 + +### Q: 描边看不清? + +A: 增大 `strokeWidth`,或使用对比色作为描边颜色 + +### Q: 如何实现闪烁效果? + +A: 使用 `animate()` 方法或自定义定时器更新透明度 + +### Q: 图层顺序错乱? + +A: 设置合适的 `zIndex` 值,数值越大越在上层 + +### Q: 如何实现阴影效果? + +A: 创建两个图层,下层使用 `subtractive` 混合模式 + +### Q: 纹理图片不显示? + +A: 检查图片 URL 是否可访问,是否有 CORS 问题 + +## 完整示例 + +### 多层次可视化 + +```javascript +// 1. 底图图层 +const baseLayer = new PolygonLayer({ zIndex: 1 }) + .source(baseData, { parser: { type: 'geojson' } }) + .shape('fill') + .color('#f5f5f5') + .style({ + opacity: 0.8, + }); + +// 2. 数据填充层 +const dataLayer = new PolygonLayer({ zIndex: 2 }) + .source(valueData, { parser: { type: 'geojson' } }) + .shape('fill') + .color('value', ['#ffffcc', '#c7e9b4', '#7fcdbb', '#1d91c0', '#0c2c84']) + .style({ + opacity: 0.7, + strokeWidth: 0.5, + stroke: '#fff', + }); + +// 3. 高亮热点层(光晕效果) +const hotspotLayer = new PointLayer({ + blend: 'additive', + zIndex: 3, +}) + .source(hotspots, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('circle') + .size(50) + .color('#FF6B6B') + .style({ + opacity: 0.4, + blur: 0.9, + }); + +// 4. 标注层 +const labelLayer = new PointLayer({ zIndex: 4 }) + .source(labels, { parser: { type: 'json', x: 'lng', y: 'lat' } }) + .shape('name', 'text') + .size(14) + .color('#333') + .style({ + textAnchor: 'center', + textOffset: [0, 0], + strokeWidth: 2, + stroke: '#fff', + }); + +scene.addLayer(baseLayer); +scene.addLayer(dataLayer); +scene.addLayer(hotspotLayer); +scene.addLayer(labelLayer); +``` + +## 相关技能 + +- [视觉映射](./mapping.md) +- [图层动画](../animation/layer-animation.md) +- [点图层](../layers/point.md) +- [线图层](../layers/line.md) +- [面图层](../layers/polygon.md) + +## 在线示例 + +查看更多示例: [L7 官方示例](https://l7.antv.antgroup.com/examples)