Fast Car

Back

开头介绍#

作为一个热爱旅行的开发者,我一直想要一个能够记录和展示自己旅游足迹的功能。市面上虽然有很多地图应用,但大多功能复杂,而我只需要一个简单直观的方式来标记去过的地方和想去的地方。

于是我决定在自己的个人网站上实现一个旅游足迹地图功能。这个功能的核心需求很简单:

  • 在地图上标记去过的地方(绿色标记)
  • 标记想去的地方(橙色标记)
  • 支持点击查看详细信息
  • 适配网站的亮色/暗色主题
  • 在移动端也能良好展示

经过技术调研,我选择了 OpenLayers + 高德地图瓦片源的方案,既能满足功能需求,又能保证在国内的访问速度。

演示#

旅游足迹地图演示

我的主页:https://fastcar.fun

你可以在我的关于页面看到这个旅游足迹地图的实际效果,地图上标记了我去过的城市和想去的地方。

功能点详解#

技术选型考虑#

在实现这个功能时,我面临几个技术选型的问题:

地图库选择:

  • Google Maps API:需要翻墙,在国内访问不稳定
  • 百度地图 API:需要申请 key,有使用限制
  • 高德地图 API:同样需要申请 key
  • OpenLayers + 开放瓦片源:免费、灵活、无需 key

最终选择 OpenLayers 是因为它是一个功能强大的开源地图库,支持多种瓦片源,而且可以直接使用高德地图的公开瓦片服务,无需申请 API key。

瓦片源选择: 高德地图提供了公开的瓦片服务,支持多种样式:

  • 标准地图:style=8(亮色主题)
  • 暗色地图:style=7(暗色主题)
  • 卫星图:style=6
  • 路网图:style=1

这些瓦片源都支持中文标注,非常适合国内用户使用。

核心功能实现#

1. 地图初始化 使用 OpenLayers 创建地图实例,配置高德瓦片源,设置合适的中心点和缩放级别。

2. 双主题支持 创建亮色和暗色两个图层,根据网站主题动态切换显示。

3. 标记系统 使用矢量图层添加标记点,区分”去过”和”想去”两种类型,使用不同的颜色和图标。

4. 交互功能 实现点击标记显示详细信息的弹窗,包括地点名称、描述、访问时间等。

5. 响应式设计 适配移动端显示,调整地图高度和弹窗样式。

架构图解#

整体架构图#

graph TB A[Astro 组件] --> B[TravelMap.astro] B --> C[travel-map.json 数据] B --> D[OpenLayers 地图库] D --> E[高德瓦片源] D --> F[矢量图层] F --> G[已访问标记] F --> H[愿望清单标记] B --> I[主题检测系统] I --> J[亮色主题图层] I --> K[暗色主题图层] B --> L[交互系统] L --> M[点击事件处理] M --> N[弹窗显示]

数据流图#

flowchart LR A[JSON 数据文件] --> B[组件加载] B --> C[解析数据结构] C --> D[创建地图实例] D --> E[添加瓦片图层] E --> F[创建矢量图层] F --> G[遍历数据添加标记] G --> H[绑定交互事件] H --> I[地图渲染完成]

主题切换时序图#

sequenceDiagram participant U as 用户 participant S as 站点主题系统 participant M as MutationObserver participant T as TravelMap组件 participant L as 地图图层 U->>S: 切换主题 S->>S: 更新 DOM class M->>M: 检测到 DOM 变化 M->>T: 触发主题检测 T->>T: 判断当前主题 alt 暗色主题 T->>L: 隐藏亮色图层 T->>L: 显示暗色图层 else 亮色主题 T->>L: 显示亮色图层 T->>L: 隐藏暗色图层 end L->>U: 地图主题更新完成

交互流程图#

graph TD A[用户点击地图] --> B{点击位置有标记?} B -->|是| C[获取标记数据] B -->|否| D[无操作] C --> E[创建弹窗内容] E --> F[计算弹窗位置] F --> G[显示弹窗] G --> H[用户查看信息] H --> I{用户点击关闭?} I -->|是| J[移除弹窗] I -->|否| K[保持显示]

代码实现#

地图初始化核心代码#

function initMap() {
  // 亮色主题图层 - 高德地图标准地图
  lightLayer = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
      attributions: '© <a href="https://www.amap.com/">高德地图</a>',
      maxZoom: 18
    })
  })

  // 暗色主题图层 - 高德地图暗色标准地图
  darkLayer = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',
      attributions: '© <a href="https://www.amap.com/">高德地图</a>',
      maxZoom: 18
    }),
    visible: false
  })

  // 创建地图
  map = new ol.Map({
    target: 'travel-map',
    layers: [lightLayer, darkLayer],
    view: new ol.View({
      center: ol.proj.fromLonLat([105.0, 35.0]), // 中国中心
      zoom: 4,
      maxZoom: 18,
      minZoom: 2
    })
  })
}
javascript

实现要点:

  1. 使用高德地图的公开瓦片服务,URL 中的 {1-4} 表示负载均衡的服务器
  2. style=8 是标准地图,style=7 是暗色地图
  3. lang=zh_cn 确保中文标注
  4. 初始时暗色图层设置为不可见

标记添加核心代码#

function addMarkers() {
  const vectorSource = new ol.source.Vector()

  // 添加已去过的地方
  data.visited.forEach(location => {
    const feature = new ol.Feature({
      geometry: new ol.geom.Point(ol.proj.fromLonLat([location.coordinates[1], location.coordinates[0]])),
      type: 'visited',
      data: location
    })
    vectorSource.addFeature(feature)
  })

  // 添加想去的地方
  data.wishlist.forEach(location => {
    const feature = new ol.Feature({
      geometry: new ol.geom.Point(ol.proj.fromLonLat([location.coordinates[1], location.coordinates[0]])),
      type: 'wishlist',
      data: location
    })
    vectorSource.addFeature(feature)
  })

  const vectorLayer = new ol.layer.Vector({
    source: vectorSource,
    style: function(feature) {
      const type = feature.get('type')
      return new ol.style.Style({
        image: new ol.style.Circle({
          radius: 15,
          fill: new ol.style.Fill({
            color: type === 'visited' ? '#22c55e' : '#f59e0b'
          }),
          stroke: new ol.style.Stroke({
            color: type === 'visited' ? '#16a34a' : '#d97706',
            width: 3
          })
        }),
        text: new ol.style.Text({
          text: type === 'visited' ? '✓' : '♡',
          fill: new ol.style.Fill({ color: 'white' }),
          font: 'bold 16px sans-serif'
        })
      })
    }
  })

  map.addLayer(vectorLayer)
}
javascript

实现要点:

  1. 注意坐标转换:ol.proj.fromLonLat([经度, 纬度])
  2. 使用不同颜色区分标记类型:绿色表示已访问,橙色表示愿望清单
  3. 添加文字图标:✓ 和 ♡ 增强视觉识别

主题切换核心代码#

function detectSiteTheme() {
  // 检测站点当前主题
  const isDark = document.documentElement.classList.contains('dark') ||
                 document.body.classList.contains('dark') ||
                 document.documentElement.getAttribute('data-theme') === 'dark'

  currentTheme = isDark ? 'dark' : 'light'
  switchTheme(currentTheme)
}

function switchTheme(theme) {
  if (theme === 'dark') {
    // 暗色主题:显示高德暗色地图
    lightLayer.setVisible(false)
    darkLayer.setVisible(true)
  } else {
    // 亮色主题:显示高德标准地图
    lightLayer.setVisible(true)
    darkLayer.setVisible(false)
  }
}

function watchSiteThemeChanges() {
  // 使用MutationObserver监听站点主题变化
  const observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.type === 'attributes' &&
          (mutation.attributeName === 'class' || mutation.attributeName === 'data-theme')) {
        detectSiteTheme()
      }
    })
  })

  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['class', 'data-theme']
  })
}
javascript

实现要点:

  1. 支持多种主题检测方式,兼容不同的主题切换实现
  2. 使用 MutationObserver 监听 DOM 变化,实现主题自动切换
  3. 通过图层的 setVisible() 方法控制显示/隐藏

数据结构设计#

{
  "visited": [
    {
      "id": "beijing",
      "name": "北京",
      "nameEn": "Beijing",
      "coordinates": [39.9042, 116.4074],
      "description": "中国首都",
      "visitDate": "2023-05"
    }
  ],
  "wishlist": [
    {
      "id": "xian",
      "name": "西安",
      "nameEn": "Xi'an",
      "coordinates": [34.3416, 108.9398],
      "description": "古都西安,兵马俑故乡",
      "priority": "high"
    }
  ]
}
json

设计要点:

  1. 分离已访问和愿望清单数据
  2. 坐标使用 [纬度, 经度] 格式
  3. 支持中英文名称
  4. 可扩展字段:访问时间、优先级等

简单总结#

通过使用 OpenLayers + 高德瓦片源,我成功实现了一个功能完整的旅游足迹地图。这个方案的主要优势:

技术优势:

  • 无需申请 API key,降低使用门槛
  • 支持自定义样式和交互
  • 高德地图在国内访问速度快,中文支持好
  • OpenLayers 功能强大,扩展性好

功能特色:

  • 双主题自动切换,与网站整体风格保持一致
  • 直观的标记系统,清晰区分已访问和愿望清单
  • 响应式设计,移动端体验良好
  • 交互弹窗提供详细信息展示

后续改进方向:

  1. 添加路线规划功能,连接相邻的旅游点
  2. 支持照片上传,为每个地点添加旅游照片
  3. 增加统计功能,显示旅游里程、访问城市数量等
  4. 支持数据导入导出,方便备份和分享
  5. 添加搜索功能,快速定位特定地点
  6. 集成天气信息,显示各地实时天气

这个旅游足迹地图不仅满足了我个人记录旅游经历的需求,也为网站访客提供了一个了解我旅游足迹的有趣方式。通过开源的技术栈实现,也为其他开发者提供了一个可参考的实现方案。

使用 OpenLayers + 高德瓦片源实现旅游足迹地图
https://astro-pure.js.org/blog/travel-map-openlayers-amap
Author Oliver Yeung
Published at 2025年9月8日
Comment seems to stuck. Try to refresh?✨