Created
February 23, 2026 04:14
-
-
Save yuskesuzki/2b6688a900b69ff782fc1ee8a97a735f to your computer and use it in GitHub Desktop.
地図上の2点間の直線距離を計測・可視化するアプリ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MapLibre 距離計測アプリ</title> | |
| <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script> | |
| <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" /> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Helvetica Neue', Arial, sans-serif; | |
| } | |
| #map { | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| width: 100%; | |
| } | |
| .ui-panel { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| z-index: 10; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 20px; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); | |
| width: 280px; | |
| } | |
| h3 { | |
| margin: 0 0 12px 0; | |
| color: #333; | |
| font-size: 18px; | |
| border-bottom: 2px solid #eee; | |
| padding-bottom: 8px; | |
| } | |
| .select-group { | |
| margin-bottom: 12px; | |
| } | |
| label { | |
| font-size: 12px; | |
| color: #666; | |
| font-weight: bold; | |
| } | |
| select { | |
| width: 100%; | |
| padding: 8px; | |
| margin-top: 4px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| } | |
| #result-area { | |
| margin-top: 15px; | |
| padding: 12px; | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| border-left: 4px solid #007bff; | |
| display: none; | |
| } | |
| .result-label { | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .result-value { | |
| font-size: 22px; | |
| font-weight: bold; | |
| color: #007bff; | |
| } | |
| .btn-reset { | |
| width: 100%; | |
| margin-top: 15px; | |
| padding: 10px; | |
| background: #6c757d; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="ui-panel"> | |
| <h3>距離計測</h3> | |
| <div class="select-group"> | |
| <label>地点 A (始点)</label> | |
| <select id="select-a"> | |
| <option value="">選択または地図クリック</option> | |
| </select> | |
| </div> | |
| <div class="select-group"> | |
| <label>地点 B (終点)</label> | |
| <select id="select-b"> | |
| <option value="">選択または地図クリック</option> | |
| </select> | |
| </div> | |
| <div id="result-area"> | |
| <div class="result-label">2点間の距離</div> | |
| <div id="distance-display" class="result-value">0.00 km</div> | |
| </div> | |
| <button class="btn-reset" onclick="fullReset()">リセット</button> | |
| </div> | |
| <div id="map"></div> | |
| <script> | |
| const cities = { | |
| "札幌": [141.3545, 43.0621], "青森": [140.7447, 40.8244], "仙台": [140.8717, 38.2682], | |
| "東京": [139.6917, 35.6895], "横浜": [139.6425, 35.4475], "名古屋": [136.9066, 35.1815], | |
| "大阪": [135.5023, 34.6937], "広島": [132.4594, 34.3853], "福岡": [130.4017, 33.5904], | |
| "那覇": [127.6792, 26.2124] | |
| }; | |
| const map = new maplibregl.Map({ | |
| container: 'map', | |
| style: { | |
| version: 8, | |
| sources: { | |
| 'osm': { type: 'raster', tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize: 256, attribution: '© OSM' } | |
| }, | |
| layers: [{ id: 'osm', type: 'raster', source: 'osm' }] | |
| }, | |
| center: [137, 38], zoom: 5 | |
| }); | |
| let points = []; | |
| let markers = []; | |
| map.on('load', () => { | |
| map.addSource('route', { | |
| 'type': 'geojson', | |
| 'data': { 'type': 'FeatureCollection', 'features': [] } | |
| }); | |
| map.addLayer({ | |
| 'id': 'route-line', 'type': 'line', 'source': 'route', | |
| 'layout': { 'line-join': 'round', 'line-cap': 'round' }, | |
| 'paint': { 'line-color': '#007bff', 'line-width': 4 } | |
| }); | |
| }); | |
| const selectA = document.getElementById('select-a'); | |
| const selectB = document.getElementById('select-b'); | |
| Object.keys(cities).forEach(name => { | |
| selectA.add(new Option(name, cities[name])); | |
| selectB.add(new Option(name, cities[name])); | |
| }); | |
| // 地図クリック時の処理 | |
| map.on('click', (e) => { | |
| if (points.length >= 2) clearMapOnly(); // 3点目を打つ前にクリア | |
| const lngLat = [e.lngLat.lng, e.lngLat.lat]; | |
| addPoint(lngLat); | |
| // セレクトボックスの選択を解除(任意地点のため) | |
| if (points.length === 1) selectA.value = ""; | |
| if (points.length === 2) selectB.value = ""; | |
| }); | |
| // セレクトボックス変更時の処理 | |
| const handleSelectChange = () => { | |
| if (selectA.value && selectB.value) { | |
| clearMapOnly(); // 地図上のマーカーとラインのみ消す | |
| addPoint(selectA.value.split(',').map(Number)); | |
| addPoint(selectB.value.split(',').map(Number)); | |
| } | |
| }; | |
| selectA.addEventListener('change', handleSelectChange); | |
| selectB.addEventListener('change', handleSelectChange); | |
| function addPoint(lngLat) { | |
| points.push(lngLat); | |
| const marker = new maplibregl.Marker({ color: points.length === 1 ? '#28a745' : '#dc3545' }) | |
| .setLngLat(lngLat).addTo(map); | |
| markers.push(marker); | |
| if (points.length === 2) { | |
| updateResult(); | |
| } | |
| } | |
| function updateResult() { | |
| const p1 = new maplibregl.LngLat(...points[0]); | |
| const p2 = new maplibregl.LngLat(...points[1]); | |
| const dist = (p1.distanceTo(p2) / 1000).toFixed(2); | |
| document.getElementById('distance-display').innerText = `${dist} km`; | |
| document.getElementById('result-area').style.display = 'block'; | |
| map.getSource('route').setData({ | |
| 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': points } | |
| }); | |
| const bounds = new maplibregl.LngLatBounds().extend(p1).extend(p2); | |
| map.fitBounds(bounds, { padding: 80 }); | |
| } | |
| // 地図上の要素だけ消す(セレクトボックスの値は維持する) | |
| function clearMapOnly() { | |
| markers.forEach(m => m.remove()); | |
| markers = []; | |
| points = []; | |
| if (map.getSource('route')) { | |
| map.getSource('route').setData({ 'type': 'FeatureCollection', 'features': [] }); | |
| } | |
| document.getElementById('result-area').style.display = 'none'; | |
| } | |
| // すべてを完全にリセットする | |
| function fullReset() { | |
| clearMapOnly(); | |
| selectA.value = ""; | |
| selectB.value = ""; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment