用 workflow 转换 Google 地图 kmz 数据为 geojson 数据

用 workflow 转换 Google 地图 kmz 数据为 geojson 数据

May 30, 2024
编码 , 分享 ,

前言

最近终于把大发哥的地图功能算是移植过来了,可以在 https://1900.live/map/ 页面中查看初步效果,不过在后续如何方便的维护这些数据的方式上产生了一些思考。

想直接看代码可以通过右侧 TOC 跳转到最后。

方案

数据持久化无非是写在文件里或者数据库,所以我想了一下几种方案:

  • 直接手动维护一个 json 文件,坐标数据等信息去 google 地图里复制。
  • 之前做了 API 服务,也可以考虑写进 lokijs 的数据库里。
  • 直接用一些第三方的 API 服务。

第一个方案

不过一种方案有些麻烦,大概只在我脑子里停留了 2.5 秒后就被我踢出局了。

第二个方案

第二个方案我倒是认真想了很久,甚至做了一下原型,验证结果是做虽然可以做,但是很麻烦。

因为相当于我得重新写一套包含前后端的,基于 mapbox 的交互,如添加标点,删除标点、更新标点,如果要全部弄完很是挺麻烦的,我也没这个信心做好,所以在第二天放弃了。

第三个方案

emmm,好像没有现成的 API 服务。

我查了一下相关的自制地图服务,好像基本上都没有提供这个功能。

倒是我之前用的 google 地图的 mymap 服务倒是提供了一个导出功能,不过也没有提供现成的 API 读取数据。

本来是打算放弃了,采用第二种方案。

但是无意间发现 mymap 的导出链接似乎是固定的,我通过开启无痕浏览,在其他设备上使用链接下载,等方式验证,都是通过的,应该是不需要 cookie 或者密钥之类的东西,有链接就可以下载。

不过下载下来的格式是.kmz 的,无法直接使用,我尝试通过 google 到的一些在线工具转换后发现可以转换成一种 geojson 格式,所以我灵光一闪「能不能用 workflow 去下载 mymap 的数据再转换成 geojson 数据存在仓库里以供调用呢?」

所以我 用 ChatGPT40 写了个脚本测试,果然行得通。

只不过似乎定位坐标的转换不是很准确,会有一些偏差,可能是 mapbox 和 google map 之间的标准不一样。

不过这些之后可以慢慢处理。

数据结构

目前虽然数据能正常渲染了,但是地点相关的游记这个功能暂时还没做,不过我大概有一些想法了。

首先数据结构可能要重新做下调整,大发哥那边不知道是通过什么工具维护数据的,他的 description 属性是以个数组对象,而且还包含有 permalink 属性和 image 属性,google map 目前还不能这样做,所以我目前只能先把地点图片、游记标题和链接直接拼成字符串,在 python 脚本中再进分割处理。

其次,我想通过博客集成的 fuse.js 去搜索全站文章中和当前地点名字相关的博文进行展示,这部分可以在地图初始化的时候处理。

大概就是这样,之后再慢慢进行完善把。

代码实现

首先你需要一个库,我这边用的我转换 douban 数据的库。

新增一个新的 wordflow 配置文件 .github/workflows/mapsync.yml ,这个 action 可以手动触发或者每天凌晨 2 点执行一次。

先通过链接下载 kmz 文件,再通过仓库的 pyhon 脚本转换数据为 json 格式存在 data 目录下。

name: Google To GeoJSON

on:
  schedule:
    - cron: "0 2 * * *"
  workflow_dispatch:

jobs:
  download_and_convert:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.8'
    # 安装python环境
    - name: Install required Python packages
      run: |
        python -m pip install --upgrade pip
        pip install shapely requests
    # 下载kmz文件
    - name: Download KMZ file
      run: |
        URL="https://www.google.com/maps/d/kml?mid=103ngIYVR56ypc1eJp_4FntEU0Nc&cid=mp&cv=qNGi_t2KHPo.zh_CN"
        OUTPUT_FILE="downloaded_file.kmz"
        curl -o $OUTPUT_FILE $URL
      shell: bash
    # 调用python转换kmz文件为json文件,存在指定目录
    - name: Convert KMZ to GeoJSON
      run: |
        mkdir -p data
        python convert_kmz_to_geojson.py downloaded_file.kmz data/geojson.json
      shell: bash
    # 提交到代码库里。
    - name: Commit and push converted GeoJSON
      run: |
        git config --global user.name 'github-actions'
        git config --global user.email 'github-actions@github.com'
        git add data/geojson.json
        git commit -m 'Automated commit of converted GeoJSON'
        git push
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

workflow 配置

然后在仓库根目录新增一个 python 脚本 convert_kmz_to_geojson.py ,内容如下:

import os
import sys
import json
import zipfile
import xml.etree.ElementTree as ET
from shapely.geometry import shape, Point, LineString, Polygon, mapping

def convert_kmz_to_geojson(kmz_file_path, geojson_file_path):
    with zipfile.ZipFile(kmz_file_path, 'r') as kmz:
        kml_content = kmz.read('doc.kml')
        
        root = ET.fromstring(kml_content)
        geojson_content = kml_to_geojson(root)
        
        with open(geojson_file_path, 'w') as geojson_file:
            json.dump(geojson_content, geojson_file, indent=4)

def kml_to_geojson(element):
    geojson = {
        "type": "FeatureCollection",
        "features": []
    }

    for placemark in element.iterfind('.//{http://www.opengis.net/kml/2.2}Placemark'):
        feature = {
            "type": "Feature",
            "properties": {},
            "geometry": None
        }

        for name in placemark.iterfind('.//{http://www.opengis.net/kml/2.2}name'):
            feature["properties"]["name"] = name.text
        
        for description in placemark.iterfind('.//{http://www.opengis.net/kml/2.2}description'):
            feature["properties"]["description"] = description.text

        for point in placemark.iterfind('.//{http://www.opengis.net/kml/2.2}Point'):
            coords = point.find('{http://www.opengis.net/kml/2.2}coordinates').text.strip()
            lon, lat, _ = map(float, coords.split(','))
            feature["geometry"] = mapping(Point(lon, lat))
        
        for linestring in placemark.iterfind('.//{http://www.opengis.net/kml/2.2}LineString'):
            coords = linestring.find('{http://www.opengis.net/kml/2.2}coordinates').text.strip()
            points = [tuple(map(float, coord.split(',')))[:2] for coord in coords.split()]
            feature["geometry"] = mapping(LineString(points))
        
        for polygon in placemark.iterfind('.//{http://www.opengis.net/kml/2.2}Polygon'):
            outer_boundary = polygon.find('.//{http://www.opengis.net/kml/2.2}outerBoundaryIs')
            if outer_boundary is not None:
                coords = outer_boundary.find('.//{http://www.opengis.net/kml/2.2}coordinates').text.strip()
                points = [tuple(map(float, coord.split(',')))[:2] for coord in coords.split()]
                feature["geometry"] = mapping(Polygon([points]))
        
        if feature["geometry"] is not None:
            geojson["features"].append(feature)
    
    return geojson

if __name__ == "__main__":
    kmz_file_path = sys.argv[1]
    geojson_file_path = sys.argv[2]
    convert_kmz_to_geojson(kmz_file_path, geojson_file_path)

然后前端使用这个数据文件即可。

「 还好我们还有文字... 」

加入评论