地図上のルート情報をKMLファイルに出力するPythonコード

IT・プログラミング

Leafletを使って表示された
ルートのサイトからKMLデータを取り出したかったので、そのPythonコードを書いてみました。

実行環境はGoogle Colabです。

SeleniumによるHTML取得

まずは、Seleniumで地図タイル情報とSVG情報を取得します。

Seleniumの準備

# 1. 必要な依存関係をインストール
!apt-get update
!apt-get install -y wget unzip
!apt-get install -y xvfb


# 2. Chromiumをインストール
!wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
!dpkg -i google-chrome-stable_current_amd64.deb || true
!apt-get -f install -y
!google-chrome --version  # インストールされたChromeのバージョン確認


# Google Chromeのバージョンに対応するChromeDriverをダウンロード
!wget https://storage.googleapis.com/chrome-for-testing-public/131.0.6778.85/linux64/chromedriver-linux64.zip -O chromedriver-linux64.zip

# ダウンロードしたChromeDriverを解凍
!unzip chromedriver-linux64.zip

# ChromeDriverに実行権限を付与して、適切なディレクトリに移動
!chmod +x chromedriver-linux64/chromedriver
!mv chromedriver-linux64/chromedriver /usr/local/bin/chromedriver

# ChromeDriverのバージョン確認
!chromedriver --version

!google-chrome --version
!chromedriver --version

# 4. Seleniumをインストール
!pip install selenium

HTMLの取得

url = "https://"
file_name = 'map.kml'

# 5. 必要なモジュールをインポート
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import time

# 6. Seleniumでヘッドレスブラウザをセットアップ
def get_rendered_html(url):
    # Chromeオプション設定
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')  # ヘッドレスモード
    options.add_argument('--no-sandbox')  # サンドボックス無効化(Colab用)
    options.add_argument('--disable-dev-shm-usage')  # メモリ使用制限を回避

    # ブラウザを起動
    driver = webdriver.Chrome(service=Service('/usr/local/bin/chromedriver'), options=options)
    driver.get(url)

    # JavaScriptの実行待機
    time.sleep(5)

    # レンダリング後のHTMLを取得
    html = driver.page_source
    driver.quit()
    return html

# 7. URLを指定してHTMLを取得
html = get_rendered_html(url)


取得したいページのURLと出力のファイル名を指定してください。

HTMLの解析

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
svg_element = soup.find('svg', class_='leaflet-zoom-animated')
viewbox = svg_element.get('viewbox').split(' ')
viewX = viewbox[0]
viewY = viewbox[1]
svg_element['id'] = 'svg'
import re
tile_element = soup.find('img', class_='leaflet-tile leaflet-tile-loaded')
tile_src = tile_element['src']
style_string = tile_element['style']
# 正規表現でtranslate3dの値を抽出
match = re.search(r'transform:\s*translate3d\(([^)]+)\)', style_string)

# 結果を処理
if match:
    # '133px, 97px, 0px' の部分を取り出す
    translate_value = match.group(1)

    # 値をコンマで分割して、'px'を取り除き整数に変換
    transX, transY, _ = [int(val.replace('px', '').strip()) for val in translate_value.split(',')]
z = tile_src.split('/')[-3]
x = tile_src.split('/')[-2]
y = tile_src.split('/')[-1].split('.')[0]

Leafletで地図とSVGパスを描画

%%writefile template.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Leaflet SVG Overlay</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
  <style>
    #map { height: 500px; }
  </style>
</head>
<body>
  <div id="map"></div>
<!-- KMLダウンロードボタン -->
<button id="downloadButton">KMLファイルをダウンロード</button>
<button onclick="downloadKml()">KMLファイルをダウンロード2</button>


   {{ svg_element }}
  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
  <script>

    var viewX = {{ viewX }}
    var viewY = {{ viewY }}

    var z = {{ z }};
    var x = {{ x }};
    var y = {{ y }};

    // translate3dの位置調整 (ピクセル)
    var transX = {{ transX }};
    var transY = {{ transY }};

    // タイル座標を緯度経度に変換する関数
    function tileToLatLng(z, x, y) {
        var n = Math.pow(2, z);
        var lon_deg = x / n * 360 - 180;
        var lat_deg = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
        return new L.LatLng(lat_deg, lon_deg);
    }


    // タイル座標から緯度経度を計算
    var latLng = tileToLatLng(z, x, y);

    // 地図の初期設定
    var map = L.map('map').setView(latLng, z); // 緯度経度とズームレベルを指定


    // 国土地理院の地図タイル
    L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">国土地理院</a>',
        maxZoom: 18,
        minZoom: 5
    }).addTo(map);

    // SVG要素を取得
    var svgElement = document.querySelector('#svg');

    // タイルサイズ
    var tileSize = 256;

    // 現在の地図中心の座標(緯度・経度)
    var centerLatLng = map.getCenter();
    var centerPoint = map.latLngToLayerPoint(centerLatLng);

    // SVGのピクセル座標を計算(地図レイヤー座標に対応)
    var svgTopLeftPoint = L.point(centerPoint.x + viewX-transX, centerPoint.y + viewY-transY);
    var svgBottomRightPoint = L.point(svgTopLeftPoint.x + svgElement.clientWidth, svgTopLeftPoint.y + svgElement.clientHeight);

    // SVGの地理的範囲を計算
    var svgTopLeftLatLng = map.layerPointToLatLng(svgTopLeftPoint);
    var svgBottomRightLatLng = map.layerPointToLatLng(svgBottomRightPoint);
    var svgLatLngBounds = L.latLngBounds(svgTopLeftLatLng, svgBottomRightLatLng);
    map.fitBounds(svgLatLngBounds);

    // SVGをオーバーレイとして追加
    var svgOverlay = L.svgOverlay(svgElement, svgLatLngBounds, {
        opacity: 0.7,
        interactive: true
    }).addTo(map);


// SVGOverlayからKMLを生成
        function svgOverlayToKml(svgOverlay) {
            const bounds = svgOverlay.getBounds();
            const topLeftLatLng = bounds.getNorthWest();
            const bottomRightLatLng = bounds.getSouthEast();
            const offset = 13;
            const svgElement = svgOverlay.getElement();
            const svgWidth = svgElement.viewBox.baseVal.width || svgElement.clientWidth;
            const svgHeight = svgElement.viewBox.baseVal.height || svgElement.clientHeight;
            console.log(svgWidth);
            console.log(svgHeight);
            let kmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n`;
            kmlContent += `<kml xmlns="http://www.opengis.net/kml/2.2">\n`;
            kmlContent += `<Document>\n`;
            kmlContent += '<name><![CDATA[Route]]></name>\n<Folder>\n'

            const paths = svgElement.querySelectorAll('path');
            paths.forEach((path, index) => {
                const d = path.getAttribute('d');
                if (!d) return;

                let coordinates = '';
                const commands = d.split(/[MLZ]/).filter(Boolean); // パスコマンドを分解
                commands.forEach((command) => {
                    const [x, y] = command.trim().split(/\s+/).map(Number);
                    if (x != null && y != null) {
                        const lng = topLeftLatLng.lng + (((x-viewX) / svgWidth) * (bottomRightLatLng.lng - topLeftLatLng.lng));
                        const lat = topLeftLatLng.lat - (((y-viewY) / svgHeight) * (topLeftLatLng.lat - bottomRightLatLng.lat));
                        coordinates += `${lng},${lat},0 `;
                    }
                });

                if (coordinates.trim()) {

                    kmlContent += `<Placemark>\n`;
                    kmlContent += `<name>Path ${index + 1}</name>\n`;
                    kmlContent += `<LineString>\n`;
                    kmlContent += `<coordinates>${coordinates.trim()}</coordinates>\n`;
                    kmlContent += `</LineString>\n`;
                    kmlContent += `</Placemark>\n`;
                }
            });
            kmlContent += `</Folder>\n`;
            kmlContent += `</Document>\n`;
            kmlContent += `</kml>`;
            return kmlContent;
        }

        // KMLダウンロード処理
        function downloadKmlFromSvgOverlay(svgOverlay) {
            const kmlData = svgOverlayToKml(svgOverlay);
            const blob = new Blob([kmlData], { type: 'application/vnd.google-earth.kml+xml' });
            const url = URL.createObjectURL(blob);
            const link = document.createElement('a');
            link.href = url;
            link.download = '{{ file_name }}';
            link.click();
            URL.revokeObjectURL(url);
        }

        // ダウンロードボタンにイベントリスナーを追加
        const downloadButton = document.getElementById('downloadButton');
        downloadButton.addEventListener('click', () => {
            downloadKmlFromSvgOverlay(svgOverlay);
        });

  </script>


  <!-- KMLダウンロードボタン -->
</body>
</html>

相対的な位置関係からうまくSVG要素の位置を調整する部分がポイントです。
KMLファイルへの出力時の基準点とLeafletのmapで指定するBoundsの基準点が異なるのがみそでした。

# ダウンロードされたファイル名を確認(最初のKMLファイル名を取得)
download_dir = '/root/Downloads/'
downloaded_files = os.listdir(download_dir)
kml_file = [file for file in downloaded_files if file.endswith('.kml')][0]

# KMLファイルのパス
kml_file_path = os.path.join(download_dir, kml_file)

# 目的のディレクトリにファイルを移動(例: /content)
shutil.move(kml_file_path, os.path.join('/content', kml_file))
from google.colab import files

# ダウンロード用のファイルパス
kml_file_path =os.path.join('/content', kml_file)

# Google Colabからファイルをダウンロード
files.download(kml_file_path)

GPXファイルへの変換

KMLをGPXにも変換できるようにします。

pip install gpxpy
import xml.etree.ElementTree as ET
import gpxpy
import gpxpy.gpx

def kml_to_gpx(kml_file, gpx_file):
    # KMLファイルを解析
    tree = ET.parse(kml_file)
    root = tree.getroot()

    # GPXオブジェクトの作成
    gpx = gpxpy.gpx.GPX()

    # KMLの名前空間を定義
    namespaces = {'kml': 'http://www.opengis.net/kml/2.2'}

    # KMLから座標情報を抽出してGPXに追加
    for placemark in root.findall('.//kml:Placemark', namespaces):
        name = placemark.find('kml:name', namespaces).text
        coordinates = placemark.find('.//kml:coordinates', namespaces).text.strip()

        # 座標を処理
        coord_list = coordinates.split()
        for coord in coord_list:
            lon, lat, _ = coord.split(',')
            # GPXにポイントを追加
            gpx.waypoints.append(gpxpy.gpx.GPXWaypoint(latitude=float(lat), longitude=float(lon), name=name))

    # GPXファイルとして保存
    with open(gpx_file, 'w') as f:
        f.write(gpx.to_xml())

# 使用例
gpx_file = 'output.gpx'  # 出力GPXファイルのパス

kml_to_gpx(kml_file_path, kml_file_path.replace('.kml','.gpx'))
タイトルとURLをコピーしました