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: '© <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'))