From fa73b6a9f36268986af68192f65978539f4d059c Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:29:16 +0900 Subject: parks: シェルター等の統合と編集画面の刷新 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit パーゴラ・東屋を「シェルター等」に統合し、編集画面に確認チェック・遊具別入力・時計・選択行ハイライトを追加した。周辺公園と周辺施設の自治体名を補完し、公園データの文字化けを修正した。 --- scripts/fill-nearby-cities.py | 129 +++++++++++++++ scripts/park-editor.html | 311 +++++++++++++++++++++++++++++++++--- scripts/update-nearby-facilities.py | 21 ++- scripts/update-nearby-parks.py | 43 ++++- 4 files changed, 479 insertions(+), 25 deletions(-) create mode 100644 scripts/fill-nearby-cities.py (limited to 'scripts') diff --git a/scripts/fill-nearby-cities.py b/scripts/fill-nearby-cities.py new file mode 100644 index 0000000..9f2a087 --- /dev/null +++ b/scripts/fill-nearby-cities.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""周辺公園・周辺施設の city が空のとき、Nominatimで市区町村を補完する。 + +使い方: + py -X utf8 scripts/fill-nearby-cities.py + +- src/data/kodaira-nearby-parks.json の properties.city +- src/data/kodaira-nearby-facilities.json の city + +既に city が入っている行は変更しない。Nominatimの利用ポリシーに従い、 +逆ジオコーディングは1リクエストあたり約1.1秒待つ。 +""" +import json +import os +import re +import time +import urllib.parse +import urllib.request + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PARKS = os.path.join(ROOT, "src", "data", "kodaira-nearby-parks.json") +FACILITIES = os.path.join(ROOT, "src", "data", "kodaira-nearby-facilities.json") +UA = "yasutakeyohei.com nearby city fill (https://yasutakeyohei.com/)" + + +def centroid(feature): + geom = feature.get("geometry") or {} + if geom.get("type") == "Point": + lon, lat = geom["coordinates"][:2] + return lat, lon + if geom.get("type") == "Polygon": + ring = geom.get("coordinates", [[]])[0] + elif geom.get("type") == "MultiPolygon": + ring = geom.get("coordinates", [[[]]])[0][0] + else: + return None, None + if not ring: + return None, None + lon = sum(p[0] for p in ring) / len(ring) + lat = sum(p[1] for p in ring) / len(ring) + return lat, lon + + +def city_from_text(text): + text = text or "" + # 「東村山市」のように市区町村名の中に「村」が含まれるため、市→町→村の順で探す。 + for suffix in ("市", "町", "村"): + match = re.search(r"(?:東京都)?([^\s0-9]+?" + suffix + r")", text) + if match: + return match.group(1) + return "" + + +def reverse_city(lat, lon): + params = urllib.parse.urlencode( + { + "format": "jsonv2", + "lat": lat, + "lon": lon, + "zoom": 10, + "addressdetails": 1, + "accept-language": "ja", + } + ) + url = "https://nominatim.openstreetmap.org/reverse?" + params + req = urllib.request.Request(url, headers={"User-Agent": UA}) + try: + with urllib.request.urlopen(req, timeout=30) as res: + data = json.load(res) + except Exception as exc: # noqa: BLE001 + print(f" warning: reverse failed ({lat},{lon}): {exc}") + return "" + finally: + time.sleep(1.1) + address = data.get("address") or {} + for key in ("city", "town", "village", "municipality"): + value = (address.get(key) or "").strip() + if value: + return value + return "" + + +def main(): + parks_data = json.load(open(PARKS, encoding="utf-8")) + parks_changed = 0 + for feature in parks_data.get("features", []): + props = feature.setdefault("properties", {}) + if (props.get("city") or "").strip(): + continue + lat, lon = centroid(feature) + if lat is None or lon is None: + continue + city = reverse_city(lat, lon) + if city: + props["city"] = city + parks_changed += 1 + print(f" park: {props.get('name')} -> {city}") + + if parks_changed: + with open(PARKS, "w", encoding="utf-8") as fh: + json.dump(parks_data, fh, ensure_ascii=False, separators=(",", ":")) + fh.write("\n") + + facilities = json.load(open(FACILITIES, encoding="utf-8")) + facilities_changed = 0 + for item in facilities: + if (item.get("city") or "").strip(): + continue + city = city_from_text(item.get("address", "")) or city_from_text(item.get("name", "")) + if not city: + lat = item.get("lat") + lon = item.get("lon") + if lat is not None and lon is not None: + city = reverse_city(lat, lon) + if city: + item["city"] = city + facilities_changed += 1 + print(f" facility: {item.get('name')} -> {city}") + + if facilities_changed: + with open(FACILITIES, "w", encoding="utf-8") as fh: + json.dump(facilities, fh, ensure_ascii=False, indent=2) + fh.write("\n") + + print(f"updated parks {parks_changed}, facilities {facilities_changed}") + + +if __name__ == "__main__": + main() diff --git a/scripts/park-editor.html b/scripts/park-editor.html index 868a510..32e91c4 100644 --- a/scripts/park-editor.html +++ b/scripts/park-editor.html @@ -23,11 +23,26 @@ th, td { border: 1px solid #9994; padding: 0; } th { position: sticky; top: 0; background: Canvas; padding: .25rem .4rem; white-space: nowrap; z-index: 5; } td { padding: 0; } - td input { width: 100%; box-sizing: border-box; border: none; padding: .25rem .35rem; font: inherit; background: transparent; color: inherit; } - td input:focus { outline: 2px solid #3b82f6; } + td input:not([type="checkbox"]) { width: 100%; box-sizing: border-box; border: none; padding: .25rem .35rem; font: inherit; background: transparent; color: inherit; } + td input:not([type="checkbox"]):focus { outline: 2px solid #3b82f6; } tr.dirty td { background: #ff9d0033; } + tbody tr.is-focused td { background: #3b82f622 !important; } + tbody tr.is-focused td.rownum { color: #1d4ed8; font-weight: 700; opacity: 1; } + tbody tr.is-focused td:first-child { box-shadow: inset 3px 0 0 #2563eb; } + tbody tr.is-focused td:last-child { box-shadow: inset -3px 0 0 #2563eb; } + tbody tr.is-focused.dirty td.rownum::after { + content: " ●"; + color: #f59e0b; + font-size: 0.8em; + } tr[hidden] { display: none; } td.rownum { padding: 0 .4rem; text-align: right; opacity: .5; } + td.checkcell { padding: 0 .35rem; text-align: center; white-space: nowrap; } + td.checkcell input[type="checkbox"] { width: auto; height: auto; margin: 0; padding: 0; } + #check-filter { padding: .3rem .4rem; } + .checked-count { font-size: .82rem; opacity: .85; } + .check-note { font-size: .72rem; opacity: .6; } + #clear-checks { padding: .3rem .6rem; } td.mapcell { text-align: center; } td.mapcell a { text-decoration: none; padding: 0 .4rem; cursor: pointer; } @@ -38,6 +53,14 @@