From a01062f43d36112c00d5c007ac9d488c14d8fb0d Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:24:24 +0900 Subject: parks: アイコン・フィルタ・編集画面を更新し、公開を一時停止 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 遊具アイコンをSVGRepo素材に差し替え、施設種別をアイコン表示 - フィルタに種類・遊具・ピクニックを追加し、適用時の地図移動を停止 - 現在地ボタンを追跡トグルに変更し、初回以外は地図を動かさない - 編集画面を市内公園・周辺公園・公共施設の4タブに拡張 - 公園ページをドラフト化し、サイドバーとPWAの公開を停止 --- scripts/update-nearby-facilities.py | 179 ++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 scripts/update-nearby-facilities.py (limited to 'scripts/update-nearby-facilities.py') diff --git a/scripts/update-nearby-facilities.py b/scripts/update-nearby-facilities.py new file mode 100644 index 0000000..dcef5d6 --- /dev/null +++ b/scripts/update-nearby-facilities.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""小平市の境界から500m以内にある近隣市の公共施設をOpenStreetMapから抽出する。 + +使い方: + py -X utf8 scripts/update-nearby-facilities.py + +出力: + src/data/kodaira-nearby-facilities.json + +対象: amenity=library / community_centre / townhall / arts_centre / theatre +小平市内の施設は kodaira-facilities.json 側にあるため除外する。 +""" +import importlib.util +import json +import os +import urllib.parse +import urllib.request + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT = os.path.join(ROOT, "src", "data", "kodaira-nearby-facilities.json") +MAX_DIST_M = 500.0 +BBOX = (35.68, 139.39, 35.76, 139.56) +AMENITIES = "library|community_centre|townhall|arts_centre|theatre" +EXCLUDE_NAME_PARTS = ("付属図書館", "コミュニティ サロン") + +ENDPOINTS = [ + "https://overpass-api.de/api/interpreter", + "https://overpass.kumi.systems/api/interpreter", + "https://overpass.private.coffee/api/interpreter", +] + + +def load_module(path, name): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def fetch_overpass(): + south, west, north, east = BBOX + query = ( + "[out:json][timeout:180];" + "(" + f'node["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' + f'way["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' + f'relation["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' + ");" + "out tags center;" + ) + last_err = None + for endpoint in ENDPOINTS: + for attempt in range(2): + try: + req = urllib.request.Request( + endpoint, + data=query.encode("utf-8"), + headers={"User-Agent": "kodaira-nearby-facilities/1.0 (yasutakeyohei.com)"}, + ) + with urllib.request.urlopen(req, timeout=240) as res: + data = json.load(res) + if "elements" not in data: + raise ValueError("レスポンスに elements がありません") + print(f" {endpoint} から {len(data['elements'])} 件取得") + return data["elements"] + except Exception as exc: # noqa: BLE001 + last_err = exc + print(f" {endpoint} 失敗 ({attempt + 1}/2): {exc}") + raise SystemExit(f"Overpass APIの取得に失敗しました(出力は作成していません): {last_err}") + + +def facility_type(tags): + amenity = tags.get("amenity", "") + name = tags.get("name", "") + if amenity == "library": + return "図書館" + if amenity == "community_centre": + return "児童館" if "児童館" in name else "公民館" + if amenity == "townhall": + return "市役所支所" + if amenity in ("arts_centre", "theatre"): + return "文化施設" + return "" + + +def address_of(tags): + full = (tags.get("addr:full") or "").strip() + if full: + return full + city = (tags.get("addr:city") or "").strip() + neighbourhood = ( + tags.get("addr:neighbourhood") + or tags.get("addr:suburb") + or tags.get("addr:quarter") + or "" + ).strip() + street = (tags.get("addr:street") or "").strip() + block = (tags.get("addr:block_number") or "").strip() + number = (tags.get("addr:housenumber") or "").strip() + if block and number: + lot = f"{block}-{number}" + else: + lot = block or number + if street: + return f"{city}{street}{lot}" + if neighbourhood: + return f"{city}{neighbourhood}{lot}" + # 町名がない番地だけでは住所にならないため、市区町村だけにする。 + return city + + +def main(): + print("Overpass API から近隣施設を取得中...") + elements = fetch_overpass() + + np = load_module(os.path.join(ROOT, "scripts", "update-nearby-parks.py"), "nearby_parks") + boundary_ll = np.load_boundary() + lons = [lon for polygon in boundary_ll for ring in polygon for lon, _ in ring] + lats = [lat for polygon in boundary_ll for ring in polygon for _, lat in ring] + to_xy = np.projector(sum(lats) / len(lats), sum(lons) / len(lons)) + boundary_xy = [ + [[to_xy(lon, lat) for lon, lat in ring] for ring in polygon] + for polygon in boundary_ll + ] + boundary = np.BoundaryIndex(boundary_xy) + + features = [] + for el in elements: + tags = el.get("tags") or {} + name = (tags.get("name") or "").strip() + if not name: + continue + if any(part in name for part in EXCLUDE_NAME_PARTS): + continue + kind = facility_type(tags) + if not kind: + continue + center = el.get("center") or {} + lat = el.get("lat", center.get("lat")) + lon = el.get("lon", center.get("lon")) + if lat is None or lon is None: + continue + x, y = to_xy(lon, lat) + if boundary.contains(x, y): + continue + distance = boundary.min_distance([(x, y)]) + if distance > MAX_DIST_M: + continue + features.append( + { + "name": name, + "type": kind, + "address": address_of(tags), + "hours": (tags.get("opening_hours") or "").strip(), + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": f"市外・小平市境から約{int(round(distance))}m(OpenStreetMap)", + "source": f"https://www.openstreetmap.org/{el.get('type')}/{el.get('id')}", + "lat": lat, + "lon": lon, + "geocode": "OSM", + "city": (tags.get("addr:city") or "").strip(), + "distance_m": int(round(distance)), + "osm_type": el.get("type"), + "osm_id": el.get("id"), + "nearby": True, + } + ) + + features.sort(key=lambda f: (f["distance_m"], f["name"])) + with open(OUT, "w", encoding="utf-8") as fh: + json.dump(features, fh, ensure_ascii=False, indent=2) + fh.write("\n") + print(f"nearby facilities {len(features)} -> {OUT}") + + +if __name__ == "__main__": + main() -- cgit v1.3.1