diff options
| -rw-r--r-- | astro.config.mjs | 6 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | public/kodaira-data/parks/LICENSE.txt | 9 | ||||
| -rw-r--r-- | public/manifest.webmanifest | 6 | ||||
| -rw-r--r-- | public/sw.js | 13 | ||||
| -rw-r--r-- | scripts/make-park-qgis-kit.py | 65 | ||||
| -rw-r--r-- | scripts/park-editor.html | 254 | ||||
| -rw-r--r-- | scripts/park-editor.mjs | 106 | ||||
| -rw-r--r-- | scripts/update-nearby-facilities.py | 179 | ||||
| -rw-r--r-- | scripts/update-nearby-parks.py | 243 | ||||
| -rw-r--r-- | src/components/FacilitiesTable.astro | 70 | ||||
| -rw-r--r-- | src/components/ParkMap.astro | 446 | ||||
| -rw-r--r-- | src/components/ParkMapHelp.astro | 111 | ||||
| -rw-r--r-- | src/components/ParksFilters.astro | 184 | ||||
| -rw-r--r-- | src/components/ParksTable.astro | 260 | ||||
| -rw-r--r-- | src/content/docs/kodaira-data/parks.mdx | 90 | ||||
| -rw-r--r-- | src/data/kodaira-nearby-facilities.json | 306 | ||||
| -rw-r--r-- | src/data/kodaira-nearby-parks.json | 2 | ||||
| -rw-r--r-- | src/data/park-icons.ts | 15 | ||||
| -rw-r--r-- | src/data/parks-rows.ts | 141 | ||||
| -rw-r--r-- | src/styles/custom.css | 62 |
21 files changed, 2229 insertions, 341 deletions
diff --git a/astro.config.mjs b/astro.config.mjs index c93811b..07681ae 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -352,7 +352,8 @@ export default defineConfig({ label: "小平市のデータ", collapsed: true, items: [ - { label: "小平市の公園一覧", link: "/kodaira-data/parks/" }, + // 公園ページはドラフト公開停止中(公開時に戻す) + // { label: "公園さんぽマップ", link: "/kodaira-data/parks/" }, { label: "小平市ホームページのアーカイブ(暫定)等", collapsed: true, @@ -502,6 +503,8 @@ export default defineConfig({ content: "document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('[href=\"#_top\"] span').forEach(el => { if (el.textContent === 'トップへ戻る') el.textContent = ''; }); });", }, + // 公園ページはドラフト公開停止中のため、PWAのheadは無効化(公開時に戻す) + /* { tag: "link", attrs: { rel: "manifest", href: "/manifest.webmanifest" }, @@ -522,6 +525,7 @@ export default defineConfig({ tag: "meta", attrs: { name: "apple-mobile-web-app-title", content: "小平市公園" }, }, + */ ], components: { PageTitle: "./src/components/starlight/PageTitle.astro", diff --git a/package.json b/package.json index 0a06b50..4a57562 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "osm:update": "py -X utf8 scripts/update-osm-parks.py", "parks:kit": "py -X utf8 scripts/make-park-qgis-kit.py", "parks:import": "py -X utf8 scripts/import-manual-parks.py", + "parks:nearby": "py -X utf8 scripts/update-nearby-parks.py", + "parks:facilities": "py -X utf8 scripts/update-nearby-facilities.py", "preview": "astro preview", "postinstall": "patch-package", "astro": "astro" diff --git a/public/kodaira-data/parks/LICENSE.txt b/public/kodaira-data/parks/LICENSE.txt index 6149587..4422b13 100644 --- a/public/kodaira-data/parks/LICENSE.txt +++ b/public/kodaira-data/parks/LICENSE.txt @@ -8,3 +8,12 @@ https://creativecommons.org/licenses/by/4.0/deed.ja このポリゴンは正確な境界座標ではありません。参考としてご利用ください。 出典として「安竹洋平(https://yasutakeyohei.com/)」を明示すれば、自由に利用できます。 OSM由来のデータはOpenStreetMapのODbLライセンスです。 + +遊具アイコン: +SVGRepo(https://www.svgrepo.com/)の素材を利用しています。 +各素材の作者・ライセンスは SVGRepo の該当ページおよび +https://www.svgrepo.com/page/licensing/ をご確認ください。 + +使用素材(SVGRepo): +airplane-plane, climbing-bars, flying-fox, picnic, playground, +rocket-playground, shovel-repair, slide, soccer-ball, swing, teeter diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 91af084..06d85c6 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,8 +1,8 @@ { "id": "/kodaira-data/parks/", - "name": "小平市の公園一覧", - "short_name": "小平市公園", - "description": "小平市内の公園・公共施設・近隣公園を検索できる一覧", + "name": "こだいら 公園さんぽマップ", + "short_name": "公園さんぽ", + "description": "小平市と周辺の公園・公共施設を検索できる地図", "lang": "ja", "start_url": "/kodaira-data/parks/", "scope": "/", diff --git a/public/sw.js b/public/sw.js index 99af941..571f752 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,7 @@ -const CACHE = "kodaira-parks-v1"; +// 公園ページはドラフト公開停止中のため、公園ページをAPP_SHELLから外し、 +// キャッシュを v2 に更新して旧キャッシュを削除する。公開時に元に戻す。 +const CACHE = "kodaira-parks-v2"; const APP_SHELL = [ - "/kodaira-data/parks/", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png", @@ -34,7 +35,7 @@ self.addEventListener("fetch", (event) => { // OSMのタイルなど外部オリジンのものはキャッシュしない。 if (url.origin !== self.location.origin) return; - // ページ遷移はネットワーク優先。オフライン時はキャッシュ→公園ページ。 + // ページ遷移はネットワーク優先。オフライン時はキャッシュ。 if (request.mode === "navigate") { event.respondWith( fetch(request) @@ -43,11 +44,7 @@ self.addEventListener("fetch", (event) => { caches.open(CACHE).then((cache) => cache.put(request, copy)); return response; }) - .catch(() => - caches - .match(request) - .then((cached) => cached || caches.match("/kodaira-data/parks/")), - ), + .catch(() => caches.match(request)), ); return; } diff --git a/scripts/make-park-qgis-kit.py b/scripts/make-park-qgis-kit.py index 0df6d5a..b84630e 100644 --- a/scripts/make-park-qgis-kit.py +++ b/scripts/make-park-qgis-kit.py @@ -25,8 +25,10 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) OURS = os.path.join(ROOT, "src", "data", "kodaira-parks.json") OSM = os.path.join(ROOT, "src", "data", "kodaira-parks-osm.json") MANUAL = os.path.join(ROOT, "src", "data", "kodaira-parks-manual.json") +NEARBY = os.path.join(ROOT, "src", "data", "kodaira-nearby-parks.json") OUT_DIR = os.path.join(ROOT, "reference", "qgis") MANUAL_QGIS = os.path.join(OUT_DIR, "manual-parks.geojson") +NEARBY_MANUAL_QGIS = os.path.join(OUT_DIR, "nearby-parks-manual.geojson") if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") @@ -85,6 +87,7 @@ def main(): ours = load(OURS) osm = load(OSM) manual = load(MANUAL) + nearby = load(NEARBY) # QGISで描いたが、まだ取り込んでいないポリゴンの公園名 drawn_names = set() @@ -100,6 +103,19 @@ def main(): except (ValueError, OSError) as e: print(f" warning: {MANUAL_QGIS} を読めません: {e}", file=sys.stderr) + nearby_drawn_names = set() + if os.path.exists(NEARBY_MANUAL_QGIS): + try: + for feat in load(NEARBY_MANUAL_QGIS).get("features", []): + p = feat.get("properties") or {} + if not has_coords(feat.get("geometry")): + continue + name = str(p.get("our_name") or "").strip() + if name: + nearby_drawn_names.add(norm(name)) + except (ValueError, OSError) as e: + print(f" warning: {NEARBY_MANUAL_QGIS} を読めません: {e}", file=sys.stderr) + polygons = [] covered = set() osm_by_id = {} @@ -170,10 +186,42 @@ def main(): "drawn": norm(name) in drawn_names, "lat": lat, "lon": lon, + "source": "city", + }, + "geometry": {"type": "Point", "coordinates": [lon, lat]}, + } + ) + + # 市外の近隣公園のうち、まだポリゴンがないもの(点)も描画対象に含める。 + nearby_points = 0 + for feat in nearby.get("features", []): + geom = feat.get("geometry") or {} + if geom.get("type") != "Point": + continue + p = feat.get("properties") or {} + name = str(p.get("name") or "").strip() + if not name: + continue + lon, lat = geom["coordinates"][:2] + points.append( + { + "type": "Feature", + "properties": { + "no": len(points) + 1, + "our_name": name, + "address": f"{p.get('city') or '市外'}(市外)", + "kind": "市外の近隣公園", + "official_area_m2": p.get("area_m2", ""), + "drawn": norm(name) in nearby_drawn_names, + "lat": lat, + "lon": lon, + "source": "nearby", + "distance_m": p.get("distance_m", ""), }, "geometry": {"type": "Point", "coordinates": [lon, lat]}, } ) + nearby_points += 1 counts = { "covered": len(covered), @@ -181,6 +229,7 @@ def main(): "no_point": len(no_point), "already": len(already), "drawn": sum(1 for p in points if p["properties"]["drawn"]), + "nearby_points": nearby_points, } os.makedirs(OUT_DIR, exist_ok=True) @@ -193,6 +242,7 @@ def main(): - ポリゴン整備済み: {counts['covered']} 件 - OSM側に既にポリゴンあり(未リンク): {counts['already']} 件 - ポリゴン未整備: {counts['missing']} 件(うち手入力済み・未取り込み {counts['drawn']} 件、座標なし {counts['no_point']} 件) +- 市外の近隣公園(点): {counts['nearby_points']} 件 ## 1. QGIS に背景地図を追加 @@ -206,7 +256,7 @@ def main(): ## 2. 下地を読み込む -- `parks-without-polygon.geojson` … まだポリゴンがない公園(点)。`our_name` に公園名が入っています +- `parks-without-polygon.geojson` … まだポリゴンがない公園(点)。`our_name` に公園名が入っています。`source` が `city` なら小平市内、`nearby` なら市外の近隣公園です - `existing-parks.geojson` … 整備済みポリゴン。二重描き防止用 ## 3. ポリゴン用レイヤを作る @@ -225,6 +275,15 @@ def main(): > `our_name` を空欄にしても、取り込み時に最寄りの公園点から自動で判定します > (既定 60m 以内・曖昧でない場合のみ)。 +### 市外の近隣公園を描く場合 + +`parks-without-polygon.geojson` の `source` が `nearby` の点は、小平市外の近隣公園です。 +小平市内の公園とは別のファイルに描きます。 + +1. `reference/qgis/nearby-parks-manual.geojson` を新規作成(Polygon / EPSG:4326、`our_name` フィールド) +2. 公園の輪郭を描き、`our_name` に点の名前を入れる +3. `py -X utf8 scripts/update-nearby-parks.py` で取り込む + ## 4. 取り込む ```bash @@ -234,11 +293,12 @@ py -X utf8 scripts/import-manual-parks.py reference/qgis/manual-parks.geojson `--dry-run` で公園名・面積・台帳面積との倍率・警告を確認してから本実行してください。 台帳面積の 0.7〜1.5 倍を外れると警告が出ます。 +市外の近隣公園は `py -X utf8 scripts/update-nearby-parks.py` で取り込みます。 取り込み後に `npm run build` で地図と表を確認します。 ## 進捗の管理 -`parks-without-polygon.geojson` の `drawn` は、`manual-parks.geojson` に同じ `our_name` のポリゴンがあると `true` になります。 +`parks-without-polygon.geojson` の `drawn` は、`manual-parks.geojson` または `nearby-parks-manual.geojson` に同じ `our_name` のポリゴンがあると `true` になります。 - 属性テーブルで `drawn` を確認できます - スタイルを `drawn = true` で灰色にすると、残りが分かりやすくなります @@ -258,6 +318,7 @@ py -X utf8 scripts/import-manual-parks.py reference/qgis/manual-parks.geojson print(f"ポリゴン整備済み: {counts['covered']} 件") print(f"OSM側に既にポリゴンあり(未リンク): {counts['already']} 件") print(f"ポリゴン未整備: {counts['missing']} 件(座標なし {counts['no_point']} 件)") + print(f"市外の近隣公園(点): {counts['nearby_points']} 件") if no_point: print("座標がないため地図に出せない公園(先に kodaira-parks.json の緯度・経度を埋める):") for n in no_point: diff --git a/scripts/park-editor.html b/scripts/park-editor.html index 8c519e9..868a510 100644 --- a/scripts/park-editor.html +++ b/scripts/park-editor.html @@ -3,17 +3,22 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> -<title>公園データ編集</title> +<title>公園・施設データ編集</title> <style> :root { color-scheme: light dark; } body { font-family: system-ui, -apple-system, "Hiragino Kaku Gothic ProN", sans-serif; margin: 0; } - header { position: sticky; top: 0; background: Canvas; border-bottom: 1px solid #9995; padding: .5rem .75rem; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; z-index: 10; } + header { position: sticky; top: 0; background: Canvas; border-bottom: 1px solid #9995; padding: .45rem .75rem; z-index: 20; } + .head-row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; } header h1 { font-size: 1rem; margin: 0 .5rem 0 0; } #q { padding: .3rem .5rem; } #count { font-size: .85rem; opacity: .7; } #save { padding: .35rem .9rem; font-weight: bold; } #status { font-size: .85rem; } - .wrap { overflow: auto; height: calc(100vh - 3.2rem); } + #tabs { display: flex; gap: .3rem; flex-wrap: wrap; margin-top: .45rem; } + #tabs button { padding: .3rem .7rem; border: 1px solid #9996; border-radius: 999px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: .85rem; } + #tabs button.active { background: #3b82f6; border-color: #3b82f6; color: #fff; } + #warning { margin: 0; padding: .35rem .75rem; background: #ff9d0022; border-bottom: 1px solid #9995; font-size: .8rem; } + .wrap { overflow: auto; height: calc(100vh - 7.4rem); } table { border-collapse: collapse; font-size: .8rem; } th, td { border: 1px solid #9994; padding: 0; } th { position: sticky; top: 0; background: Canvas; padding: .25rem .4rem; white-space: nowrap; z-index: 5; } @@ -29,123 +34,268 @@ </head> <body> <header> - <h1>公園データ編集</h1> - <input id="q" placeholder="絞り込み(公園名・町名・住所)" size="24"> - <span id="count"></span> - <button id="save">保存</button> - <button id="revert">再読込</button> - <span id="status"></span> + <div class="head-row"> + <h1>公園・施設データ編集</h1> + <input id="q" placeholder="絞り込み(名称・住所・備考など)" size="24"> + <span id="count"></span> + <button id="save">保存</button> + <button id="revert">再読込</button> + <span id="status"></span> + </div> + <nav id="tabs"></nav> </header> +<p id="warning" hidden></p> <div class="wrap"><table id="tbl"><thead></thead><tbody></tbody></table></div> <script> -const FIELDS = ["公園名","住所","町名","面積㎡","500㎡以上","種別","南西部指定管理","設置者","トイレ","水飲み場","ベンチ・椅子","テーブル","パーゴラ","東屋","防災用具","ボール遊び","遊具","緯度","経度","備考"]; -let parks = []; +const FIELD = (key, label) => ({ key, label }); + +const DATASETS = { + parks: { + label: "市内公園", + fields: [ + FIELD("公園名", "公園名"), FIELD("住所", "住所"), FIELD("町名", "町名"), + FIELD("面積㎡", "面積㎡"), FIELD("500㎡以上", "500㎡以上"), FIELD("種別", "種別"), + FIELD("南西部指定管理", "南西部指定管理"), FIELD("設置者", "設置者"), FIELD("状態", "状態"), + FIELD("トイレ", "トイレ"), FIELD("水飲み場", "水飲み場"), FIELD("ベンチ・椅子", "ベンチ・椅子"), + FIELD("テーブル", "テーブル"), FIELD("パーゴラ", "パーゴラ"), FIELD("東屋", "東屋"), + FIELD("防災用具", "防災用具"), FIELD("ボール遊び", "ボール遊び"), FIELD("遊具", "遊具"), + FIELD("緯度", "緯度"), FIELD("経度", "経度"), FIELD("備考", "備考"), + ], + }, + nearbyParks: { + label: "周辺の公園", + warning: "周辺の公園は update-nearby-parks.py で再生成すると上書きされます。編集内容は別途保存してください。", + fields: [ + FIELD("name", "名称"), FIELD("city", "市"), FIELD("distance_m", "距離(m)"), + FIELD("area_m2", "面積(㎡)"), FIELD("note", "備考"), + FIELD("osm_type", "OSM種別"), FIELD("osm_id", "OSM ID"), + FIELD("name_source", "名称の出所"), + ], + }, + facilities: { + label: "市内の公共施設", + fields: [ + FIELD("name", "名称"), FIELD("type", "種別"), FIELD("address", "住所"), + FIELD("hours", "開館時間"), FIELD("closed", "休館日"), FIELD("toilet", "トイレ"), + FIELD("water", "給水"), FIELD("note", "備考"), FIELD("source", "出典URL"), + FIELD("lat", "緯度"), FIELD("lon", "経度"), FIELD("city", "市"), + FIELD("distance_m", "距離(m)"), + ], + }, + nearbyFacilities: { + label: "周辺の公共施設", + warning: "周辺の公共施設は update-nearby-facilities.py で再生成すると上書きされます。編集内容は別途保存してください。", + fields: [ + FIELD("name", "名称"), FIELD("type", "種別"), FIELD("address", "住所"), + FIELD("hours", "開館時間"), FIELD("closed", "休館日"), FIELD("toilet", "トイレ"), + FIELD("water", "給水"), FIELD("note", "備考"), FIELD("source", "出典URL"), + FIELD("lat", "緯度"), FIELD("lon", "経度"), FIELD("city", "市"), + FIELD("distance_m", "距離(m)"), + ], + }, +}; + +const NUMERIC = { + parks: new Set(), + nearbyParks: new Set(["distance_m", "area_m2"]), + facilities: new Set(["lat", "lon", "distance_m"]), + nearbyFacilities: new Set(["lat", "lon", "distance_m"]), +}; + const tbl = document.getElementById("tbl"); const thead = tbl.querySelector("thead"); const tbody = tbl.querySelector("tbody"); const q = document.getElementById("q"); const count = document.getElementById("count"); -const status = document.getElementById("status"); +const statusEl = document.getElementById("status"); +const warningEl = document.getElementById("warning"); +const tabsEl = document.getElementById("tabs"); + +let currentId = null; +let data = null; +let rows = []; +let isFC = false; +let fields = []; -function mapsUrl(p) { - if (p["緯度"] && p["経度"]) { - return "https://www.google.com/maps/search/?api=1&query=" + p["緯度"] + "," + p["経度"]; +function hasDirty() { + return Boolean(tbody.querySelector("tr.dirty")); +} + +function featureCentroid(feature) { + const g = feature && feature.geometry; + if (!g) return null; + if (g.type === "Point") return [g.coordinates[1], g.coordinates[0]]; + const ring = g.type === "Polygon" ? g.coordinates[0] : g.coordinates[0] && g.coordinates[0][0]; + if (!ring || !ring.length) return null; + let lat = 0; + let lon = 0; + for (const [x, y] of ring) { + lon += x; + lat += y; + } + return [lat / ring.length, lon / ring.length]; +} + +function mapsUrl(row, props) { + const lat = props["緯度"] != null && props["緯度"] !== "" ? props["緯度"] : props.lat; + const lon = props["経度"] != null && props["経度"] !== "" ? props["経度"] : props.lon; + let query = ""; + if (lat != null && lat !== "" && lon != null && lon !== "") { + query = `${lat},${lon}`; + } else if (isFC) { + const c = featureCentroid(row); + if (c) query = `${c[0]},${c[1]}`; } - return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent("小平市" + (p["公園名"] || "")); + if (!query && props.city && props.name) query = `${props.city}${props.name}`; + if (!query && props["公園名"]) query = `小平市${props["公園名"]}`; + if (!query && props.name) query = props.name; + if (!query) query = "小平市"; + return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(query); +} + +function renderTabs() { + tabsEl.innerHTML = ""; + Object.entries(DATASETS).forEach(([id, dataset]) => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.textContent = dataset.label; + btn.dataset.id = id; + btn.addEventListener("click", () => { + if (id === currentId) return; + if (hasDirty() && !confirm("未保存の変更は破棄されます。よろしいですか?")) return; + load(id); + }); + tabsEl.appendChild(btn); + }); } -async function load() { - const res = await fetch("/api/parks", { cache: "no-store" }); - parks = await res.json(); - render(); +function updateTabs() { + tabsEl.querySelectorAll("button").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.id === currentId); + }); +} + +async function load(id) { + currentId = id; + fields = DATASETS[id].fields; + updateTabs(); + warningEl.hidden = !DATASETS[id].warning; + warningEl.textContent = DATASETS[id].warning || ""; + statusEl.textContent = "読み込み中…"; + try { + const res = await fetch("/api/data/" + id, { cache: "no-store" }); + if (!res.ok) throw new Error(await res.text()); + data = await res.json(); + isFC = data.type === "FeatureCollection"; + rows = isFC ? data.features : data; + render(); + statusEl.textContent = `${DATASETS[id].label}: ${rows.length}件`; + } catch (e) { + statusEl.textContent = "読み込みに失敗: " + e.message; + } } function render() { - thead.innerHTML = "<tr>" + ["#", "地図"].concat(FIELDS).map(function (f) { return "<th>" + f + "</th>"; }).join("") + "</tr>"; + thead.innerHTML = "<tr>" + ["#", "地図"].concat(fields.map((f) => f.label)).map((label) => "<th>" + label + "</th>").join("") + "</tr>"; tbody.innerHTML = ""; - parks.forEach(function (p, r) { + rows.forEach((row, r) => { + const props = isFC ? row.properties || {} : row; const tr = document.createElement("tr"); - const th = document.createElement("td"); - th.className = "rownum"; - th.textContent = String(r + 1); - tr.appendChild(th); + const rowNum = document.createElement("td"); + rowNum.className = "rownum"; + rowNum.textContent = String(r + 1); + tr.appendChild(rowNum); + const mapTd = document.createElement("td"); mapTd.className = "mapcell"; const mapA = document.createElement("a"); mapA.textContent = "🗺"; mapA.title = "Googleマップで開く"; - mapA.href = mapsUrl(p); + mapA.href = mapsUrl(row, props); mapA.target = "_blank"; mapA.rel = "noopener"; - mapA.addEventListener("click", function (ev) { - ev.preventDefault(); - const name = tr.querySelector('input[data-c="0"]').value.trim(); - if (name) window.open(mapsUrl(name), "_blank", "noopener"); - }); mapTd.appendChild(mapA); tr.appendChild(mapTd); - FIELDS.forEach(function (f, c) { + + fields.forEach((field, c) => { const td = document.createElement("td"); const inp = document.createElement("input"); - inp.value = p[f] != null ? p[f] : ""; + inp.value = props[field.key] != null ? props[field.key] : ""; inp.dataset.r = r; inp.dataset.c = c; - inp.addEventListener("input", function () { + inp.addEventListener("input", () => { tr.classList.add("dirty"); - status.textContent = "未保存の変更があります"; + statusEl.textContent = "未保存の変更があります"; }); td.appendChild(inp); tr.appendChild(td); }); - tr.dataset.search = (p["公園名"] || "") + " " + (p["町名"] || "") + " " + (p["住所"] || ""); + + tr.dataset.search = fields.map((f) => String(props[f.key] != null ? props[f.key] : "")).join(" "); tbody.appendChild(tr); }); filter(); } function filter() { - const s = q.value.trim(); + const s = q.value.trim().toLowerCase(); let shown = 0; - Array.prototype.forEach.call(tbody.children, function (tr) { - const ok = !s || tr.dataset.search.indexOf(s) >= 0; + Array.prototype.forEach.call(tbody.children, (tr) => { + const ok = !s || tr.dataset.search.toLowerCase().indexOf(s) >= 0; tr.hidden = !ok; if (ok) shown++; }); - count.textContent = shown + " / " + parks.length + " 件"; + count.textContent = shown + " / " + rows.length + " 件"; } function collect() { - Array.prototype.forEach.call(tbody.querySelectorAll("input"), function (inp) { - parks[Number(inp.dataset.r)][FIELDS[Number(inp.dataset.c)]] = inp.value; + Array.prototype.forEach.call(tbody.querySelectorAll("input"), (inp) => { + const row = rows[Number(inp.dataset.r)]; + const props = isFC ? row.properties || (row.properties = {}) : row; + const field = fields[Number(inp.dataset.c)]; + let value = inp.value; + if (NUMERIC[currentId].has(field.key)) { + const t = value.trim(); + if (t === "") { + value = null; + } else { + const n = Number(t); + value = Number.isFinite(n) ? n : null; + } + } + props[field.key] = value; }); } document.getElementById("save").onclick = async function () { collect(); - status.textContent = "保存中…"; + statusEl.textContent = "保存中…"; try { - const res = await fetch("/api/parks", { + const res = await fetch("/api/data/" + currentId, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(parks), + body: JSON.stringify(data), }); if (res.ok) { - status.textContent = "保存しました(" + parks.length + "件)。git にコミットを忘れずに。"; - Array.prototype.forEach.call(tbody.children, function (tr) { tr.classList.remove("dirty"); }); + const result = await res.json(); + statusEl.textContent = `保存しました(${DATASETS[currentId].label} ${result.count}件)。git にコミットを忘れずに。`; + tbody.querySelectorAll("tr.dirty").forEach((tr) => tr.classList.remove("dirty")); } else { - status.textContent = "保存に失敗: " + (await res.text()); + statusEl.textContent = "保存に失敗: " + (await res.text()); } } catch (e) { - status.textContent = "保存に失敗: " + e.message; + statusEl.textContent = "保存に失敗: " + e.message; } }; document.getElementById("revert").onclick = function () { - if (confirm("保存していない変更は破棄されます。よろしいですか?")) load(); + if (hasDirty() && !confirm("保存していない変更は破棄されます。よろしいですか?")) return; + load(currentId); }; q.addEventListener("input", filter); -load(); +renderTabs(); +load("parks"); </script> </body> </html> diff --git a/scripts/park-editor.mjs b/scripts/park-editor.mjs index 7a6c2fa..a872008 100644 --- a/scripts/park-editor.mjs +++ b/scripts/park-editor.mjs @@ -1,9 +1,10 @@ -// 公園データの簡易エディタ(ローカル専用)。 +// 公園・公共施設データの簡易エディタ(ローカル専用)。 // // 使い方: -// node scripts/park-editor.mjs +// npm run park-editor // → 表示された URL(既定 http://localhost:8787/)をブラウザで開く -// → 表のセルを直接編集し、「保存」を押すと src/data/kodaira-parks.json を更新 +// → タブで「市内公園 / 周辺の公園 / 市内の公共施設 / 周辺の公共施設」を切り替え +// → 表のセルを直接編集し、「保存」を押すと該当するJSONを更新 // → 反映確認後、git にコミットする // // 注意: ローカル(127.0.0.1)にのみ公開します。追加パッケージは不要です。 @@ -13,36 +14,109 @@ import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const DATA = join(root, "src/data/kodaira-parks.json"); const HTML = join(root, "scripts/park-editor.html"); const PORT = Number(process.env.PORT || 8787); +const DATASETS = { + parks: { + label: "市内公園", + path: join(root, "src/data/kodaira-parks.json"), + pretty: false, + }, + nearbyParks: { + label: "周辺の公園", + path: join(root, "src/data/kodaira-nearby-parks.json"), + pretty: false, + }, + facilities: { + label: "市内の公共施設", + path: join(root, "src/data/kodaira-facilities.json"), + pretty: true, + }, + nearbyFacilities: { + label: "周辺の公共施設", + path: join(root, "src/data/kodaira-nearby-facilities.json"), + pretty: true, + }, +}; + function send(res, code, type, body) { res.writeHead(code, { "content-type": type }); res.end(body); } +function datasetFromUrl(url) { + const match = /^\/api\/data\/([A-Za-z]+)$/.exec(url); + if (!match) return null; + return DATASETS[match[1]] ? { id: match[1], dataset: DATASETS[match[1]] } : null; +} + const server = createServer(async (req, res) => { try { const url = (req.url || "").split("?")[0]; if (req.method === "GET" && (url === "/" || url === "/index.html")) { send(res, 200, "text/html; charset=utf-8", await readFile(HTML, "utf-8")); - } else if (req.method === "GET" && url === "/api/parks") { - send(res, 200, "application/json; charset=utf-8", await readFile(DATA, "utf-8")); - } else if (req.method === "POST" && url === "/api/parks") { + return; + } + + if (req.method === "GET" && url === "/api/datasets") { + send( + res, + 200, + "application/json; charset=utf-8", + JSON.stringify( + Object.entries(DATASETS).map(([id, dataset]) => ({ id, label: dataset.label })), + ), + ); + return; + } + + const target = datasetFromUrl(url); + if (!target) { + send(res, 404, "text/plain; charset=utf-8", "not found"); + return; + } + + if (req.method === "GET") { + send( + res, + 200, + "application/json; charset=utf-8", + await readFile(target.dataset.path, "utf-8"), + ); + return; + } + + if (req.method === "POST") { let body = ""; for await (const chunk of req) body += chunk; - const parks = JSON.parse(body); - if (!Array.isArray(parks) || parks.length === 0) { + const data = JSON.parse(body); + + if (target.id === "nearbyParks") { + if (!data || data.type !== "FeatureCollection" || !Array.isArray(data.features)) { + throw new Error("周辺の公園は FeatureCollection ではありません"); + } + } else if (!Array.isArray(data) || data.length === 0) { throw new Error("データが配列ではありません"); } - // 元ファイルと同じコンパクトな1行JSONで保存する(日本語はそのまま)。 - await writeFile(DATA, JSON.stringify(parks), "utf-8"); - console.log(`保存しました: ${parks.length} 件 → ${DATA}`); - send(res, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, count: parks.length })); - } else { - send(res, 404, "text/plain; charset=utf-8", "not found"); + + const text = target.dataset.pretty + ? `${JSON.stringify(data, null, 2)}\n` + : `${JSON.stringify(data)}\n`; + await writeFile(target.dataset.path, text, "utf-8"); + + const count = target.id === "nearbyParks" ? data.features.length : data.length; + console.log(`保存しました: ${target.dataset.label} ${count}件 → ${target.dataset.path}`); + send( + res, + 200, + "application/json; charset=utf-8", + JSON.stringify({ ok: true, count }), + ); + return; } + + send(res, 405, "text/plain; charset=utf-8", "method not allowed"); } catch (e) { console.error(e); send(res, 500, "text/plain; charset=utf-8", String(e)); @@ -50,5 +124,5 @@ const server = createServer(async (req, res) => { }); server.listen(PORT, "127.0.0.1", () => { - console.log(`公園データ編集: http://localhost:${PORT}/ (Ctrl+C で終了)`); + console.log(`公園・施設データ編集: http://localhost:${PORT}/ (Ctrl+C で終了)`); }); 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() diff --git a/scripts/update-nearby-parks.py b/scripts/update-nearby-parks.py index 39268cb..ef30f53 100644 --- a/scripts/update-nearby-parks.py +++ b/scripts/update-nearby-parks.py @@ -20,6 +20,7 @@ OpenStreetMapのデータは ODbL 1.0。地図表示時は import json import math import os +import re import sys import time import urllib.request @@ -29,11 +30,29 @@ DATA_DIR = os.path.join(ROOT, "src", "data") BOUNDARY_IN = os.path.join(DATA_DIR, "kodaira-boundary.json") EXISTING_IN = os.path.join(DATA_DIR, "kodaira-parks-osm.json") OUT = os.path.join(DATA_DIR, "kodaira-nearby-parks.json") +NEARBY_MANUAL = os.path.join(ROOT, "reference", "qgis", "nearby-parks-manual.geojson") BBOX = (35.68, 139.39, 35.76, 139.56) # south, west, north, east MAX_DIST_M = 500.0 M_PER_DEG_LAT = 111320.0 NOTE = "市外・小平市境界から500m以内" +PARK_NAME_RE = re.compile(r"(公園|緑地|広場|遊園|庭園|森)") + +# OSMに公園ポリゴンがない近隣公園。位置・面積は自治体の公開情報による。 +# 地図では点(範囲未取得)として表示する。 +MANUAL_FEATURES = [ + { + "name": "北町パンダ公園", + "distance_m": 94, + "area_m2": 1675, + "osm_type": "manual", + "osm_id": "hokucho-panda", + "city": "国分寺市", + "lat": 35.721831, + "lon": 139.447225, + "note": "市外・小平市境界から500m以内(公園の範囲は未取得。位置は国分寺市の公園情報による)", + }, +] ENDPOINTS = [ "https://overpass-api.de/api/interpreter", @@ -50,6 +69,7 @@ def fetch_overpass(): "(" f'way["leisure"="park"]({south},{west},{north},{east});' f'relation["leisure"="park"]({south},{west},{north},{east});' + f'node["name"~"公園|緑地|広場|遊園|庭園|森"]({south},{west},{north},{east});' ");" "out geom;" ) @@ -141,6 +161,39 @@ def point_in_ring(x, y, ring): return inside +def polygon_contains(polygon, x, y): + if not point_in_ring(x, y, polygon[0]): + return False + return not any(point_in_ring(x, y, hole) for hole in polygon[1:]) + + +def name_from_named_node(polygons_ll, named_nodes, used_names): + """名前のない公園ポリゴンの中にある名前付きノードから公園名を推測する。 + + 公園ポリゴンの名称がノード側にしかない場合(例: 窪東公園)に使う。 + 既に別の公園で使われている名前は候補から除く。 + 複数ある場合は重心に最も近いものを選ぶ。 + """ + best_name = "" + best_dist = float("inf") + for polygon in polygons_ll: + ring = polygon[0] + xs = [pt[0] for pt in ring] + ys = [pt[1] for pt in ring] + cx = sum(xs) / len(xs) + cy = sum(ys) / len(ys) + for name, lon, lat in named_nodes: + if name in used_names: + continue + if not polygon_contains(polygon, lon, lat): + continue + dist = (lon - cx) ** 2 + (lat - cy) ** 2 + if dist < best_dist: + best_dist = dist + best_name = name + return best_name + + def way_polygons(el): pts = [(p["lon"], p["lat"]) for p in el.get("geometry") or [] if p and "lon" in p] if len(pts) < 3: @@ -270,6 +323,120 @@ def existing_keys(): } +def exclusion_polygons(): + """近隣公園から除外する大きな公園のポリゴン(経緯度)。 + + 都立小金井公園の内側にある広場・公園は、独立した近隣公園として + 重複表示しない。小金井公園本体は小平市の一覧側に残す。 + """ + data = json.load(open(EXISTING_IN, encoding="utf-8")) + polygons = [] + for feature in data["features"]: + name = feature["properties"].get("our_name") or "" + if name != "都立小金井公園": + continue + geom = feature["geometry"] + if geom["type"] == "Polygon": + polygons.append(geom["coordinates"]) + elif geom["type"] == "MultiPolygon": + polygons.extend(geom["coordinates"]) + return polygons + + +def manual_polygons_ll(geom): + if geom.get("type") == "Polygon": + return [geom.get("coordinates") or []] + if geom.get("type") == "MultiPolygon": + return geom.get("coordinates") or [] + return [] + + +def merge_nearby_manual(features, boundary, to_xy): + """reference/qgis/nearby-parks-manual.geojson の手入力ポリゴンを反映する。 + + `our_name` が既存の近隣公園と一致すれば、点やOSMポリゴンを + 手入力ポリゴンで置き換える。一致しなければ新しい公園として追加する。 + """ + if not os.path.exists(NEARBY_MANUAL): + return 0 + try: + data = json.load(open(NEARBY_MANUAL, encoding="utf-8")) + except (OSError, ValueError) as exc: + print(f" warning: {NEARBY_MANUAL} を読めません: {exc}", file=sys.stderr) + return 0 + + by_name = {f["properties"]["name"]: f for f in features} + count = 0 + for index, feat in enumerate(data.get("features", []), 1): + props = feat.get("properties") or {} + name = (props.get("our_name") or props.get("name") or "").strip() + geom = feat.get("geometry") or {} + polygons_ll = manual_polygons_ll(geom) + if not name or not polygons_ll: + continue + + polygons = [] + for polygon in polygons_ll: + rings = [] + for ring in polygon: + pts = [(pt[0], pt[1]) for pt in ring if len(pt) >= 2] + if len(pts) < 3: + continue + if not same_pt(pts[0], pts[-1]): + pts.append(pts[0]) + rings.append(pts) + if rings: + polygons.append(rings) + if not polygons: + continue + + polygons_xy = [ + [[to_xy(lon, lat) for lon, lat in ring] for ring in polygon] + for polygon in polygons + ] + area = 0.0 + cx = cy = 0.0 + for polygon in polygons_xy: + (px, py), poly_area = polygon_centroid(polygon) + cx += px * poly_area + cy += py * poly_area + area += poly_area + if area > 1e-9: + cx /= area + cy /= area + else: + vertices = [pt for polygon in polygons_xy for ring in polygon for pt in ring] + cx = sum(pt[0] for pt in vertices) / len(vertices) + cy = sum(pt[1] for pt in vertices) / len(vertices) + + points = [(cx, cy)] + for polygon in polygons_xy: + for ring in polygon: + points.extend(ring) + distance = boundary.min_distance(points) + + new_props = { + "name": name, + "distance_m": int(round(distance)), + "area_m2": int(round(area)), + "osm_type": "manual", + "osm_id": f"nearby-{index}", + "city": str(props.get("city") or "").strip(), + "note": "市外・小平市境界から500m以内(手入力ポリゴン)", + "name_source": "manual", + } + existing = by_name.get(name) + if existing: + existing["properties"].update(new_props) + existing["geometry"] = geom + else: + feature = {"type": "Feature", "properties": new_props, "geometry": geom} + features.append(feature) + by_name[name] = feature + count += 1 + return count + + def geojson_geometry(polygons): def out_ring(ring): return [[round(x, 7), round(y, 7)] for x, y in ring] @@ -286,6 +453,25 @@ def main(): print("Overpass API から取得中...") elements = fetch_overpass() + # 名前のない公園ポリゴンに名前を付けるための、公園らしい名前のノード。 + named_nodes = [] + park_feature_names = set() + for el in elements: + if el.get("type") == "node": + tags = el.get("tags") or {} + name = (tags.get("name") or "").strip() + lat = el.get("lat") + lon = el.get("lon") + if len(name) < 3 or not PARK_NAME_RE.search(name): + continue + if lat is None or lon is None: + continue + named_nodes.append((name, lon, lat)) + else: + name = ((el.get("tags") or {}).get("name") or "").strip() + if name: + park_feature_names.add(name) + boundary_ll = 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] @@ -297,19 +483,37 @@ def main(): boundary = BoundaryIndex(boundary_xy) skip = existing_keys() + exclusions = exclusion_polygons() features = [] skipped_inside = 0 + used_names = set(park_feature_names) for el in elements: - if (el.get("type"), el.get("id")) in skip: + if el.get("type") == "node": continue - tags = el.get("tags") or {} - name = (tags.get("name") or "").strip() - if not name: + if (el.get("type"), el.get("id")) in skip: continue polygons_ll = build_polygons(el) if not polygons_ll: continue + + # 小金井公園の内側にある広場・公園は、独立した近隣公園にしない。 + if exclusions: + vertices = [pt for polygon in polygons_ll for pt in polygon[0]] + cx_ll = sum(pt[0] for pt in vertices) / len(vertices) + cy_ll = sum(pt[1] for pt in vertices) / len(vertices) + if any(polygon_contains(poly, cx_ll, cy_ll) for poly in exclusions): + continue + + tags = el.get("tags") or {} + name = (tags.get("name") or "").strip() + name_source = "osm" + if not name: + name = name_from_named_node(polygons_ll, named_nodes, used_names) + name_source = "nearby_node" + if not name: + continue + used_names.add(name) polygons = [ [[to_xy(lon, lat) for lon, lat in ring] for ring in polygon] for polygon in polygons_ll @@ -353,11 +557,38 @@ def main(): "osm_id": el.get("id"), "city": (tags.get("addr:city") or "").strip(), "note": NOTE, + "name_source": name_source, }, "geometry": geojson_geometry(polygons_ll), } ) + existing_names = {f["properties"]["name"] for f in features} + for manual in MANUAL_FEATURES: + if manual["name"] in existing_names: + continue + features.append( + { + "type": "Feature", + "properties": { + "name": manual["name"], + "distance_m": manual["distance_m"], + "area_m2": manual["area_m2"], + "osm_type": manual["osm_type"], + "osm_id": manual["osm_id"], + "city": manual["city"], + "note": manual["note"], + "name_source": "manual", + }, + "geometry": { + "type": "Point", + "coordinates": [manual["lon"], manual["lat"]], + }, + } + ) + + manual_count = merge_nearby_manual(features, boundary, to_xy) + features.sort(key=lambda f: (f["properties"]["distance_m"], f["properties"]["name"])) out = {"type": "FeatureCollection", "features": features} with open(OUT, "w", encoding="utf-8") as fp: @@ -368,10 +599,10 @@ def main(): dists = [f["properties"]["distance_m"] for f in features] print( f"features {len(features)} (distance {min(dists)}-{max(dists)}m, " - f"市内で除外 {skipped_inside}) -> {OUT}" + f"市内で除外 {skipped_inside}, 手入力 {manual_count}) -> {OUT}" ) else: - print(f"features 0 (市内で除外 {skipped_inside}) -> {OUT}") + print(f"features 0 (市内で除外 {skipped_inside}, 手入力 {manual_count}) -> {OUT}") if __name__ == "__main__": diff --git a/src/components/FacilitiesTable.astro b/src/components/FacilitiesTable.astro index 2974581..797bb1a 100644 --- a/src/components/FacilitiesTable.astro +++ b/src/components/FacilitiesTable.astro @@ -1,7 +1,9 @@ --- -// 地域センター・公民館・図書館・市役所支所・文化施設の一覧。 +// 地域センター・公民館・児童館・図書館・市役所支所・文化施設の一覧。 +// 小平市の施設に加えて、市境500m以内の市外施設(図書館・公民館・児童館)も表示する。 // トイレは市の公共施設として基本的に利用できる。給水は公式ページに明記が少ないため「未確認」が多い。 import facilitiesData from "@/data/kodaira-facilities.json"; +import nearbyFacilitiesData from "@/data/kodaira-nearby-facilities.json"; type Facility = { name: string; @@ -13,12 +15,32 @@ type Facility = { water: string; note: string; source: string; + nearby?: boolean; + city?: string; + distance_m?: number; }; -const facilities = facilitiesData as Facility[]; -const types = ["地域センター", "公民館", "図書館", "市役所支所", "文化施設"]; -const mapsUrl = (name: string) => - `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`小平市${name}`)}`; +const facilities = [ + ...(facilitiesData as Facility[]), + ...(nearbyFacilitiesData as Facility[]), +]; +const types = [ + "地域センター", + "公民館", + "児童館", + "図書館", + "市役所支所", + "文化施設", +]; +const mapsUrl = (f: Facility) => { + // 市外の施設は「小平市」を付けない。名前が市名を含む場合は二重にしない。 + const query = f.nearby + ? f.city && !f.name.includes(f.city) + ? `${f.city}${f.name}` + : f.name + : `小平市${f.name}`; + return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; +}; --- <div class="facilities" data-fac-root> @@ -60,7 +82,7 @@ const mapsUrl = (name: string) => data-type={f.type} data-toilet={f.toilet || ""} data-water={f.water || ""} - data-search={`${f.name} ${f.type} ${f.address} ${f.note}`.toLowerCase()} + data-search={`${f.name} ${f.type} ${f.address} ${f.city || ""} ${f.note}`.toLowerCase()} > <td> {f.source ? ( @@ -68,10 +90,13 @@ const mapsUrl = (name: string) => ) : ( f.name )} + {f.nearby ? ( + <span class="fac-nearby" title="小平市外の施設">市外</span> + ) : null} </td> <td>{f.type}</td> <td> - <a href={mapsUrl(f.name)} target="_blank" rel="noopener"> + <a href={mapsUrl(f)} target="_blank" rel="noopener"> {f.address || "—"} </a> </td> @@ -96,9 +121,12 @@ const mapsUrl = (name: string) => </table> </div> - <p class="fac-note"> - トイレは市の公共施設として基本的に利用できます(○)。給水は公式ページに明記が少ないため「未確認」が多いです。最新の情報は各施設の公式ページで確認してください。地図の位置は住所・名称からの概略です。 - </p> + <details class="fac-help"> + <summary>設備情報について</summary> + <p class="fac-note"> + 小平市の施設は、公共施設のためトイレを利用できます(○)。市外の近隣施設は情報がないため「未確認」としています。給水は公式ページに明記が少ないため「未確認」が多いです。最新の情報は各施設の公式ページで確認してください。市外施設の情報はOpenStreetMapによるもので、地図の位置は住所・名称からの概略です。 + </p> + </details> </div> <script> @@ -178,9 +206,29 @@ const mapsUrl = (name: string) => .fac-status.is-no { color: #b71c1c; } - .fac-note { + .fac-nearby { + display: inline-block; + margin-left: 0.35rem; + padding: 0.02rem 0.35rem; + border: 1px solid currentColor; + border-radius: 999px; + color: #6d4c41; + font-size: 0.7rem; + font-weight: 600; + line-height: 1.5; + vertical-align: 0.08em; + white-space: nowrap; + } + .fac-help { margin-top: 0.5rem; color: var(--sl-color-gray-2); font-size: 0.82em; } + .fac-help summary { + cursor: pointer; + color: var(--sl-color-text-accent); + } + .fac-note { + margin: 0.35rem 0 0; + } </style> diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro index 1c75bee..e4f226b 100644 --- a/src/components/ParkMap.astro +++ b/src/components/ParkMap.astro @@ -10,22 +10,6 @@ role="region" aria-label="小平市の公園の位置(OpenStreetMap)" ></div> - <div class="park-map-legend" aria-hidden="true"> - <span><i class="swatch swatch-matched"></i>一覧と一致</span> - <span><i class="swatch swatch-unmatched"></i>一覧にない公園・緑地</span> - <span><i class="swatch swatch-nearby"></i>市外の近い公園(500m以内)</span> - <span><i class="swatch swatch-facility"></i>公共施設</span> - <span><i class="swatch swatch-boundary"></i>小平市の境界</span> - <span><i class="swatch-fac swatch-fac-toilet">WC</i>トイレ</span> - <span><i class="swatch-fac swatch-fac-water"><svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true"><path fill="currentColor" d="M12 2.7c-3.4 4.2-6 8.1-6 11.3a6 6 0 0 0 12 0c0-3.2-2.6-7.1-6-11.3z" /></svg></i>水飲み場(拡大時)</span> - </div> - <p class="park-map-note"> - 公園の範囲を赤で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園と公共施設は別レイヤーで表示し、右上のレイヤ切替でON/OFFできます。トイレと水飲み場のアイコンは、地図を拡大すると表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © <a - href="https://www.openstreetmap.org/copyright" - target="_blank" - rel="noopener">OpenStreetMap</a - > contributors - </p> <p class="park-map-locate-status" data-locate-status hidden></p> </div> @@ -94,6 +78,10 @@ background: #8e24aa; border-radius: 50%; } + .swatch-facility-nearby { + background: #fb8c00; + border-radius: 50%; + } .swatch-boundary { height: 0; border-top: 4px solid #6a1b9a; @@ -106,18 +94,46 @@ font-size: 0.8rem; } :global(.park-locate) { - width: 34px; - height: 34px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; border: 0; + border-radius: 50%; background: #fff; color: #1e88e5; - font-size: 20px; - line-height: 1; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); cursor: pointer; } :global(.park-locate:hover) { background: #f0f7ff; } + :global(.park-locate.is-tracking) { + background: #1e88e5; + color: #fff; + animation: park-locate-pulse 1.8s infinite; + } + :global(.park-locate.is-tracking:hover) { + background: #1565c0; + } + @keyframes park-locate-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(30, 136, 229, 0.45); + } + 70% { + box-shadow: 0 0 0 9px rgba(30, 136, 229, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(30, 136, 229, 0); + } + } + :global(.park-locate svg) { + display: block; + width: 20px; + height: 20px; + } .park-map-note { margin-top: 0.4rem; color: var(--sl-color-gray-2); @@ -218,6 +234,122 @@ :global(.park-fac svg) { display: block; } + :global(.park-facility-marker) { + background: none; + border: 0; + } + :global(.park-facility-emoji) { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 22px; + height: 22px; + border: 2px solid #8e24aa; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + font-size: 13px; + line-height: 1; + } + :global(.park-facility-marker.is-nearby .park-facility-emoji) { + border-color: #fb8c00; + } + :global(.park-play-markers), + :global(.park-play-list) { + pointer-events: none; + } + :global(.park-play-list) { + display: inline-flex; + gap: 2px; + transform: translate(-50%, -50%) translateY(-24px); + } + :global(.park-play) { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: 1px solid currentColor; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + font-size: 9px; + font-weight: 700; + line-height: 1; + } + :global(.park-play-plane) { + color: #c62828; + } + :global(.park-play-swing) { + color: #ef6c00; + } + :global(.park-play-seesaw) { + color: #6d4c41; + } + :global(.park-play-spring) { + color: #d81b60; + } + :global(.park-play-slide) { + color: #2e7d32; + } + :global(.park-play-jungle) { + color: #6a1b9a; + } + :global(.park-play-sand) { + color: #3e2723; + } + :global(.park-play-sand svg *) { + vector-effect: non-scaling-stroke; + } + :global(.park-play-complex) { + color: #0277bd; + } + :global(.park-play-ball) { + color: #1b5e20; + } + :global(.park-play-rope) { + color: #00838f; + } + :global(.park-locate-marker) { + border: 0; + background: none; + } + :global(.park-locate-arrow) { + display: block; + transform-origin: 50% 50%; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35)); + } + .park-map-help { + margin-top: 0.4rem; + } + .park-map-help summary { + cursor: pointer; + color: var(--sl-color-text-accent); + font-size: 0.85rem; + } + .park-play-legend { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.8rem; + margin: 0.5rem 0 0; + color: var(--sl-color-gray-2); + font-size: 0.78rem; + } + .park-play-legend-item { + display: inline-flex; + align-items: center; + gap: 0.3rem; + } + :global(.park-play svg) { + display: block; + width: 100%; + height: 100%; + } + :global(.park-play-swing svg) { + width: 70%; + height: 70%; + } /* StarlightのMarkdown余白ルールがLeafletの各ペインに margin-top:1rem を 付けて地図を下へずらすため、地図内のペインは余白なしにする。 */ .park-map :global(.leaflet-pane) { @@ -252,6 +384,8 @@ import boundary from "@/data/kodaira-boundary.json"; import nearbyData from "@/data/kodaira-nearby-parks.json"; import facilities from "@/data/kodaira-facilities.json"; + import nearbyFacilities from "@/data/kodaira-nearby-facilities.json"; + import { PARK_ICONS } from "@/data/park-icons"; import parks from "@/data/kodaira-parks.json"; const MATCHED_STYLE = { @@ -285,6 +419,13 @@ fillColor: "#42a5f5", fillOpacity: 0.25, }; + const NEARBY_POINT_STYLE = { + radius: 7, + color: "#0d47a1", + weight: 2, + fillColor: "#42a5f5", + fillOpacity: 0.85, + }; const NEARBY_SELECTED_STYLE = { color: "#0d47a1", weight: 3, @@ -365,7 +506,9 @@ if (name) nearbyInfoByName.set(name, p); }); const nearbyMapsQuery = (name: string, city = "") => - name ? (city ? `${city}${name}` : name) : ""; + name ? (city && !name.includes(city) ? `${city}${name}` : name) : ""; + const nearbyBaseStyle = (lyr: any) => + lyr._isPoint ? NEARBY_POINT_STYLE : NEARBY_STYLE; const parkDataByName = new Map<string, any>(); (parks as any[]).forEach((p) => parkDataByName.set(p["公園名"], p)); const ownerByName = new Map<string, string>(); @@ -536,12 +679,17 @@ // 市外の近い公園(500m以内)は別レイヤーにする。 // 小平市内の公園と同じく、クリックで一覧を絞り込み、左下に情報を表示する。 const nearbyLayer = L.geoJSON(nearbyData as any, { - style: NEARBY_STYLE, + // 範囲が取得できない公園は点(丸)で表示する。 + pointToLayer: (_feature: any, latlng: any) => + L.circleMarker(latlng, NEARBY_POINT_STYLE), + style: (feature: any) => + feature?.geometry?.type === "Point" ? NEARBY_POINT_STYLE : NEARBY_STYLE, onEachFeature: (feature: any, lyr: any) => { const p = feature.properties || {}; const name = String(p.name || "").trim(); const city = String(p.city || "").trim(); const key = `${p.osm_type || ""}/${p.osm_id || ""}`; + lyr._isPoint = feature.geometry?.type === "Point"; if (name) nearbyEntries.push({ name, layer: lyr, city, key }); lyr.on("click", (ev: any) => { if (ev?.originalEvent) L.DomEvent.stopPropagation(ev.originalEvent); @@ -552,35 +700,51 @@ }, }).addTo(map); - // 地域センター・公民館・図書館・市役所支所。 + // 地域センター・公民館・図書館・市役所支所・文化施設。 + // 市外の近隣施設はオレンジで表示する。 const publicFacilityLayer = L.layerGroup(); - (facilities as any[]).forEach((f) => { - if (f.lat == null || f.lon == null) return; - L.circleMarker([f.lat, f.lon], { - radius: 6, - color: "#4a148c", - weight: 1.5, - fillColor: "#8e24aa", - fillOpacity: 0.85, - }) - .bindPopup( - `<strong>${f.name}</strong><br>${f.type}<br>${f.hours || ""}${f.closed ? `<br>休: ${f.closed}` : ""}<br>トイレ: ${f.toilet || "未確認"} / 給水: ${f.water || "未確認"}`, - ) - .addTo(publicFacilityLayer); - }); + const facilityMarkers: { name: string; layer: any }[] = []; + const FACILITY_ICON: Record<string, string> = { + 地域センター: "📍", + 公民館: "🏛", + 児童館: "🧒", + 図書館: "📚", + 市役所支所: "🏢", + 文化施設: "🎭", + }; + const setFacilityOpacity = (layer: any, show: boolean) => { + if (typeof layer.setOpacity === "function") { + layer.setOpacity(show ? 1 : 0.2); + } else if (typeof layer.setStyle === "function") { + layer.setStyle({ + opacity: show ? 1 : 0.15, + fillOpacity: show ? 0.85 : 0.1, + }); + } + }; + const addFacilities = (list: any[], nearby: boolean) => { + list.forEach((f) => { + if (f.lat == null || f.lon == null) return; + const icon = FACILITY_ICON[f.type] || ""; + const marker = L.marker([f.lat, f.lon], { + icon: L.divIcon({ + className: `park-facility-marker${nearby ? " is-nearby" : ""}`, + html: `<span class="park-facility-emoji">${icon || "🏢"}</span>`, + iconSize: [22, 22], + iconAnchor: [11, 11], + }), + }) + .bindPopup( + `<strong>${icon} ${f.name}</strong>${nearby ? "(市外)" : ""}<br>${f.type}<br>${f.hours || ""}${f.closed ? `<br>休: ${f.closed}` : ""}<br>トイレ: ${f.toilet || "未確認"} / 給水: ${f.water || "未確認"}`, + ) + .addTo(publicFacilityLayer); + facilityMarkers.push({ name: String(f.name || ""), layer: marker }); + }); + }; + addFacilities(facilities as any[], false); + addFacilities(nearbyFacilities as any[], true); publicFacilityLayer.addTo(map); - L.control - .layers( - null, - { - "市外の近い公園(500m以内)": nearbyLayer, - "公共施設": publicFacilityLayer, - }, - { collapsed: false, position: "topright" }, - ) - .addTo(map); - // 拡大時だけ表示する設備アイコン(トイレ・水飲み場) // 一覧と一致した公園のポリゴン内にだけ表示する。 const facilityLayer = L.layerGroup(); @@ -612,12 +776,76 @@ }).addTo(facilityLayer); }); + // 拡大時だけ表示する遊具アイコン。 + // 公園ポリゴンの中心に、遊具の種類を絵アイコンで表示する。 + const PLAY_DEFS = [ + { tokens: ["ヒコーキ", "飛行機"], key: "airplane", title: PARK_ICONS.airplane.title, cls: "park-play-plane", icon: PARK_ICONS.airplane.svg }, + { tokens: ["ジャングル"], key: "climbing", title: PARK_ICONS.climbing.title, cls: "park-play-jungle", icon: PARK_ICONS.climbing.svg }, + { tokens: ["ブランコ"], key: "swing", title: PARK_ICONS.swing.title, cls: "park-play-swing", icon: PARK_ICONS.swing.svg }, + { tokens: ["シーソー"], key: "seesaw", title: PARK_ICONS.teeter.title, cls: "park-play-seesaw", icon: PARK_ICONS.teeter.svg }, + { tokens: ["スプリング"], key: "spring", title: PARK_ICONS.spring.title, cls: "park-play-spring", icon: PARK_ICONS.spring.svg }, + { tokens: ["滑り台", "滑台"], key: "slide", title: PARK_ICONS.slide.title, cls: "park-play-slide", icon: PARK_ICONS.slide.svg }, + { tokens: ["ロープウェイ"], key: "flyingfox", title: PARK_ICONS.flyingfox.title, cls: "park-play-rope", icon: PARK_ICONS.flyingfox.svg }, + { tokens: ["砂場"], key: "sand", title: PARK_ICONS.sand.title, cls: "park-play-sand", icon: PARK_ICONS.sand.svg }, + { tokens: ["複合遊具"], key: "playground", title: PARK_ICONS.playground.title, cls: "park-play-complex", icon: PARK_ICONS.playground.svg }, + { tokens: [], key: "ball", title: PARK_ICONS.ball.title, cls: "park-play-ball", icon: PARK_ICONS.ball.svg }, + ]; + const playLegend = document.querySelector<HTMLElement>("[data-play-legend]"); + if (playLegend) { + playLegend.innerHTML = PLAY_DEFS.map( + (def) => + `<span class="park-play-legend-item"><span class="park-play ${def.cls}" aria-hidden="true">${def.icon}</span>${def.title}</span>`, + ).join(""); + } + const playLayer = L.layerGroup(); + layersByName.forEach((lyr, name) => { + const p = parkDataByName.get(name); + if (!p) return; + const text = (p["遊具"] || "").trim(); + const ballText = (p["ボール遊び"] || "").trim(); + const hasBall = Boolean(ballText && ballText !== "禁止"); + if (!text && !hasBall) return; + const seen = new Set<string>(); + const icons: string[] = []; + for (const def of PLAY_DEFS) { + const matched = + def.key === "ball" + ? hasBall + : def.tokens.some((token) => text.includes(token)); + if (!matched) continue; + if (seen.has(def.key)) continue; + // ヒコーキジャングルはジャングルジムの一種なので、飛があればジは出さない。 + if (def.key === "climbing" && seen.has("airplane")) continue; + seen.add(def.key); + icons.push( + `<span class="park-play ${def.cls}" title="${def.title}" aria-label="${def.title}">${def.icon}</span>`, + ); + } + if (!icons.length) return; + const center = lyr.getBounds().getCenter(); + L.marker([center.lat, center.lng], { + icon: L.divIcon({ + className: "park-play-markers", + html: `<span class="park-play-list">${icons.join("")}</span>`, + iconSize: null, + iconAnchor: [0, 0], + }), + riseOnHover: false, + interactive: false, + }).addTo(playLayer); + }); + const updateFacilities = () => { if (map.getZoom() >= 16) { if (!map.hasLayer(facilityLayer)) facilityLayer.addTo(map); } else if (map.hasLayer(facilityLayer)) { map.removeLayer(facilityLayer); } + if (map.getZoom() >= 17) { + if (!map.hasLayer(playLayer)) playLayer.addTo(map); + } else if (map.hasLayer(playLayer)) { + map.removeLayer(playLayer); + } }; map.on("zoomend", updateFacilities); updateFacilities(); @@ -646,7 +874,8 @@ if (key) { const nearbyHit = nearbyEntries.find((entry) => entry.key === key); if (nearbyHit) { - map.fitBounds(nearbyHit.layer.getBounds(), { padding: [40, 40], maxZoom: 17 }); + const group = L.featureGroup([nearbyHit.layer]); + map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 17 }); showSelected( name, name, @@ -702,7 +931,6 @@ const names = new Set<string>(detail.names || []); const active = Boolean(detail.active); const fromMap = Boolean(detail.fromMap); - const visible: any[] = []; if (!fromMap && selectedBar) selectedBar.hidden = true; @@ -718,7 +946,12 @@ } }); nearbyEntries.forEach(({ name, layer: lyr }) => { - lyr.setStyle(name && names.has(name) ? NEARBY_SELECTED_STYLE : NEARBY_STYLE); + lyr.setStyle( + name && names.has(name) ? NEARBY_SELECTED_STYLE : nearbyBaseStyle(lyr), + ); + }); + facilityMarkers.forEach(({ layer }) => { + setFacilityOpacity(layer, true); }); return; } @@ -731,7 +964,6 @@ } if (name && names.has(name)) { lyr.setStyle(MATCHED_STYLE); - visible.push(lyr); } else { lyr.setStyle(DIM_STYLE); } @@ -739,26 +971,23 @@ nearbyEntries.forEach(({ name, layer: lyr }) => { if (!active) { - lyr.setStyle(NEARBY_STYLE); + lyr.setStyle(nearbyBaseStyle(lyr)); return; } if (name && names.has(name)) { - lyr.setStyle(NEARBY_STYLE); - visible.push(lyr); + lyr.setStyle(nearbyBaseStyle(lyr)); } else { lyr.setStyle(NEARBY_DIM_STYLE); } }); - if (active && visible.length && visible.length < 30) { - const bounds = L.featureGroup(visible).getBounds(); - if (bounds.isValid()) { - map.fitBounds(bounds, { padding: [40, 40], maxZoom: 16 }); - } - } + // 公共施設のマーカーも絞り込みに合わせて表示する。 + facilityMarkers.forEach(({ name, layer }) => { + setFacilityOpacity(layer, !active || names.has(name)); + }); }); - // 現在地表示(スマホでは自動で取得を試みる) + // 現在地表示(ボタン操作時のみ取得) const statusEl = document.querySelector<HTMLElement>("[data-locate-status]"); const setStatus = (msg: string) => { if (!statusEl) return; @@ -769,30 +998,72 @@ let locationMarker: any = null; let accuracyCircle: any = null; let watchId: number | null = null; + let tracking = false; + let firstFix = true; + let locateButton: HTMLButtonElement | null = null; + + const LOCATE_ICON = + '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M12 2.2 20 21.5 12 16.8 4 21.5Z" fill="#1e88e5" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/></svg></span>'; + const TRACKING_ICON = + '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="3.5" fill="currentColor"/><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>'; + + const updateLocateButton = () => { + if (!locateButton) return; + locateButton.classList.toggle("is-tracking", tracking); + locateButton.setAttribute("aria-pressed", tracking ? "true" : "false"); + const label = tracking ? "現在地の追跡を停止" : "現在地を表示"; + locateButton.title = label; + locateButton.setAttribute("aria-label", label); + locateButton.innerHTML = tracking ? TRACKING_ICON : LOCATE_ICON; + }; + + const stopTracking = () => { + if (watchId !== null) { + navigator.geolocation.clearWatch(watchId); + watchId = null; + } + tracking = false; + updateLocateButton(); + }; - const locate = (center = false) => { + const locate = () => { + if (tracking) { + stopTracking(); + setStatus("現在地の追跡を停止しました。"); + return; + } if (!navigator.geolocation) { setStatus("このブラウザでは現在地を取得できません。"); return; } setStatus("現在地を取得しています…"); + firstFix = true; const onSuccess = (pos: GeolocationPosition) => { const { latitude, longitude, accuracy } = pos.coords; if (!locationMarker) { - locationMarker = L.circleMarker([latitude, longitude], { - radius: 7, - color: "#ffffff", - weight: 2, - fillColor: "#1e88e5", - fillOpacity: 1, - }).bindPopup("現在地", { autoPan: false }); - locationMarker.on("add", () => { - locationMarker.getElement()?.setAttribute("data-locate", "true"); + const icon = L.divIcon({ + className: "park-locate-marker", + html: '<span class="park-locate-arrow"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path d="M12 2.2 20 21.5 12 16.8 4 21.5Z" fill="#1e88e5" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/></svg></span>', + iconSize: [24, 24], + iconAnchor: [12, 12], }); - locationMarker.addTo(map); + locationMarker = L.marker([latitude, longitude], { + icon, + interactive: false, + keyboard: false, + }).addTo(map); } else { locationMarker.setLatLng([latitude, longitude]); } + const heading = + typeof pos.coords.heading === "number" && + Number.isFinite(pos.coords.heading) + ? pos.coords.heading + : 0; + const arrow = locationMarker + .getElement() + ?.querySelector<HTMLElement>(".park-locate-arrow"); + if (arrow) arrow.style.transform = `rotate(${heading}deg)`; if (!accuracyCircle) { accuracyCircle = L.circle([latitude, longitude], { radius: Math.min(accuracy, 1000), @@ -807,13 +1078,21 @@ .setLatLng([latitude, longitude]) .setRadius(Math.min(accuracy, 1000)); } - setStatus(`現在地を表示しています(精度 約${Math.round(accuracy)}m)。`); - if (center) map.setView([latitude, longitude], Math.max(map.getZoom(), 15)); + if (firstFix) { + map.setView([latitude, longitude], Math.max(map.getZoom(), 15)); + firstFix = false; + } + setStatus( + `現在地を追跡中です(精度 約${Math.round(accuracy)}m)。もう一度押すと停止します。`, + ); }; const onError = () => { setStatus("現在地を取得できませんでした。位置情報の利用を許可してください。"); + stopTracking(); }; if (watchId !== null) navigator.geolocation.clearWatch(watchId); + tracking = true; + updateLocateButton(); watchId = navigator.geolocation.watchPosition(onSuccess, onError, { enableHighAccuracy: true, timeout: 10000, @@ -823,23 +1102,22 @@ const LocateControl = L.Control.extend({ onAdd: () => { - const btn = L.DomUtil.create("button", "leaflet-bar park-locate"); + const btn = L.DomUtil.create("button", "park-locate") as HTMLButtonElement; btn.setAttribute("type", "button"); - btn.title = "現在地を表示"; - btn.setAttribute("aria-label", "現在地を表示"); - btn.innerHTML = '<span aria-hidden="true">◎</span>'; + locateButton = btn; L.DomEvent.disableClickPropagation(btn); - btn.addEventListener("click", () => locate(true)); + btn.addEventListener("click", () => locate()); + updateLocateButton(); return btn; }, }); new LocateControl({ position: "topright" }).addTo(map); - if (window.matchMedia("(max-width: 640px)").matches) { - locate(true); - } + // 位置情報の許可確認は、ボタン操作(ユーザー操作)のときだけ出す。 + // ページ読み込み時に自動で watchPosition を呼ぶと、ブラウザによっては + // 許可ダイアログが出ずに失敗することがあるため。 window.addEventListener("pagehide", () => { - if (watchId !== null) navigator.geolocation.clearWatch(watchId); + stopTracking(); }); // レイアウト確定後にサイズを再計算し、タイルを再配置する。 diff --git a/src/components/ParkMapHelp.astro b/src/components/ParkMapHelp.astro new file mode 100644 index 0000000..26d9917 --- /dev/null +++ b/src/components/ParkMapHelp.astro @@ -0,0 +1,111 @@ +--- +// 地図の見方・凡例。地図と一覧表を近づけるため、一覧表の下に表示する。 +// 遊具アイコンの凡例は ParkMap.astro のスクリプトが [data-play-legend] に流し込む。 +--- + +<details class="park-map-help"> + <summary>地図の見方・凡例</summary> + <div class="park-map-legend" aria-hidden="true"> + <span><i class="swatch swatch-matched"></i>一覧と一致</span> + <span><i class="swatch swatch-unmatched"></i>一覧にない公園・緑地</span> + <span><i class="swatch swatch-nearby"></i>市外の近い公園(500m以内)</span> + <span><i class="swatch swatch-facility"></i>公共施設</span> + <span><i class="swatch swatch-facility-nearby"></i>市外の公共施設</span> + <span><i class="swatch swatch-boundary"></i>小平市の境界</span> + <span><i class="swatch-fac swatch-fac-toilet">WC</i>トイレ</span> + <span><i class="swatch-fac swatch-fac-water"><svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true"><path fill="currentColor" d="M12 2.7c-3.4 4.2-6 8.1-6 11.3a6 6 0 0 0 12 0c0-3.2-2.6-7.1-6-11.3z" /></svg></i>水飲み場(拡大時)</span> + </div> + <div class="park-play-legend" data-play-legend></div> + <p class="park-map-note"> + 公園の範囲を赤、市外の近い公園を青、公共施設を紫(市外はオレンジ)で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園のうち、範囲が取得できないものは丸で表示しています。トイレと水飲み場のアイコンは拡大すると表示されます。さらに拡大すると、遊具アイコン(上の凡例)が表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © <a + href="https://www.openstreetmap.org/copyright" + target="_blank" + rel="noopener">OpenStreetMap</a + > contributors + </p> +</details> + +<style> + .park-map-legend { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 1rem; + align-items: center; + margin-top: 0.5rem; + color: var(--sl-color-gray-2); + font-size: 0.8rem; + } + .park-map-legend span { + display: inline-flex; + align-items: center; + gap: 0.35rem; + } + .swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + .swatch-matched { + background: #e53935; + } + .swatch-unmatched { + background: #ffcdd2; + } + .swatch-nearby { + background: #42a5f5; + } + .swatch-facility { + background: #8e24aa; + border-radius: 50%; + } + .swatch-facility-nearby { + background: #fb8c00; + border-radius: 50%; + } + .swatch-boundary { + height: 0; + border-top: 4px solid #6a1b9a; + border-radius: 0; + background: transparent; + } + .swatch-fac { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + margin-right: 0.3rem; + color: #fff; + font-style: normal; + font-size: 7px; + font-weight: 700; + line-height: 1; + } + .swatch-fac-toilet { + background: #455a64; + border-radius: 4px; + } + .swatch-fac-water { + background: #0277bd; + border-radius: 50%; + } + .park-map-note { + margin-top: 0.4rem; + color: var(--sl-color-gray-2); + font-size: 0.8rem; + } + .park-play-legend { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.8rem; + margin: 0.5rem 0 0; + color: var(--sl-color-gray-2); + font-size: 0.78rem; + } + .park-play-legend-item { + display: inline-flex; + align-items: center; + gap: 0.3rem; + } +</style> diff --git a/src/components/ParksFilters.astro b/src/components/ParksFilters.astro new file mode 100644 index 0000000..bf65d6e --- /dev/null +++ b/src/components/ParksFilters.astro @@ -0,0 +1,184 @@ +--- +// 公園・公共施設の一覧を絞り込むコントロール。地図の上に表示する。 +// 実際の絞り込みは ParksTable.astro のスクリプトが行う。 +import { parks, towns, total } from "@/data/parks-rows"; +import { PARK_ICONS } from "@/data/park-icons"; + +const facilityTypes = [ + "図書館", + "公民館", + "児童館", + "地域センター", + "市役所支所", + "文化施設", +].filter((type) => + parks.some((p) => p["施設"] === "1" && p["種別"] === type), +); +--- + +<div class="parks-controls" data-parks-filters> + <label> + <span>種類</span> + <select data-parks-kind> + <option value="">すべて</option> + <option value="park">公園</option> + {facilityTypes.map((type) => ( + <option value={type}>{type}</option> + ))} + </select> + </label> + <label> + <span>町名</span> + <select data-parks-town> + <option value="">すべての町名</option> + {towns.map((town) => ( + <option value={town}>{town}</option> + ))} + </select> + </label> + <label> + <span>キーワード</span> + <input type="search" data-parks-search placeholder="名称・住所・設備など" /> + </label> + <fieldset class="parks-facilities"> + <legend>設備・遊具で絞る</legend> + <label><input type="checkbox" data-fac-filter value="toilet" />トイレ</label> + <label><input type="checkbox" data-fac-filter value="water" />水飲み場・給水</label> + <label> + <input type="checkbox" data-fac-filter value="picnic" /> + <span class="filter-icon" set:html={PARK_ICONS.picnic.svg} />ピクニック・広場 + </label> + <label><input type="checkbox" data-fac-filter value="toy" />遊具あり</label> + <label> + <input type="checkbox" data-fac-filter value="sand" /> + <span class="filter-icon" set:html={PARK_ICONS.sand.svg} />砂場 + </label> + <label> + <input type="checkbox" data-fac-filter value="swing" /> + <span class="filter-icon" set:html={PARK_ICONS.swing.svg} />ブランコ + </label> + <label> + <input type="checkbox" data-fac-filter value="seesaw" /> + <span class="filter-icon" set:html={PARK_ICONS.teeter.svg} />シーソー + </label> + <label> + <input type="checkbox" data-fac-filter value="spring" /> + <span class="filter-icon" set:html={PARK_ICONS.spring.svg} />スプリング遊具 + </label> + <label> + <input type="checkbox" data-fac-filter value="jungle" /> + <span class="filter-icon" set:html={PARK_ICONS.climbing.svg} />ジャングルジム + </label> + <label> + <input type="checkbox" data-fac-filter value="plane" /> + <span class="filter-icon" set:html={PARK_ICONS.airplane.svg} />ヒコーキ + </label> + <label> + <input type="checkbox" data-fac-filter value="slide" /> + <span class="filter-icon" set:html={PARK_ICONS.slide.svg} />滑り台 + </label> + <label> + <input type="checkbox" data-fac-filter value="complex" /> + <span class="filter-icon" set:html={PARK_ICONS.playground.svg} />複合遊具 + </label> + <label> + <input type="checkbox" data-fac-filter value="rope" /> + <span class="filter-icon" set:html={PARK_ICONS.flyingfox.svg} />ロープウェイ + </label> + <label> + <input type="checkbox" data-fac-filter value="ball" /> + <span class="filter-icon" set:html={PARK_ICONS.ball.svg} />ボール遊び + </label> + </fieldset> + <span class="parks-count" data-parks-count aria-live="polite">{total}件</span> +</div> + +<p class="parks-disclaimer"> + 情報には誤りが含まれる場合があります。最新の情報は各施設の公式ページでご確認ください。 +</p> + +<style> + .parks-controls { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0.9rem; + align-items: center; + margin: 0 0 0.55rem; + } + .parks-controls label { + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 2.1rem; + font-size: 0.9rem; + line-height: 1.2; + } + .parks-controls label > span { + line-height: 1.2; + } + .parks-controls select, + .parks-controls input[type="search"] { + height: 2.1rem; + box-sizing: border-box; + padding: 0.25rem 0.5rem; + border: 1px solid var(--sl-color-hairline, #ccc); + border-radius: 0.3rem; + background: var(--sl-color-bg, #fff); + color: var(--sl-color-text, inherit); + font: inherit; + line-height: 1.2; + } + .parks-facilities { + display: flex; + flex-wrap: wrap; + gap: 0.1rem 0.6rem; + align-items: center; + margin: 0; + padding: 0.2rem 0.5rem; + border: 1px solid var(--sl-color-hairline, #ccc); + border-radius: 0.3rem; + } + .parks-facilities legend { + padding: 0 0.2rem; + font-size: 0.78em; + color: inherit; + opacity: 0.7; + } + .parks-facilities label { + display: inline-flex; + align-items: center; + gap: 0.2rem; + min-height: 1.5rem; + font-size: 0.8rem; + line-height: 1.2; + } + .parks-facilities input[type="checkbox"] { + width: 0.9em; + height: 0.9em; + margin: 0; + } + .filter-icon { + display: inline-flex; + width: 18px; + height: 18px; + flex: none; + } + .filter-icon :global(svg) { + display: block; + width: 100%; + height: 100%; + } + :global(.filter-icon svg *) { + vector-effect: non-scaling-stroke; + } + .parks-count { + margin-left: auto; + font-size: 0.9rem; + opacity: 0.75; + } + .parks-disclaimer { + margin: 0 0 0.55rem; + color: var(--sl-color-gray-2); + font-size: 0.78rem; + } +</style> diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro index bea8ff2..65b8412 100644 --- a/src/components/ParksTable.astro +++ b/src/components/ParksTable.astro @@ -1,78 +1,9 @@ --- -// 小平市の公園一覧(/kodaira-data/parks/)を表示する。 -// データは src/data/kodaira-parks.json(市立332件+市立以外)。 +// 小平市の公園・周辺の公園・公共施設の一覧(/kodaira-data/parks/)を表示する。 +// データは src/data/parks-rows.ts で組み立てる。 // DOM を抑えるため、設備は1セルにまとめ、アイコンは CSS の擬似要素で描く。 // 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 -import parksData from "@/data/kodaira-parks.json"; -import osmData from "@/data/parks-geojson"; -import nearbyData from "@/data/kodaira-nearby-parks.json"; - -type Park = Record<string, string>; - -function nearbyCentroid(feature: any): [number, number] { - const g = feature.geometry; - const ring = g.type === "Polygon" ? g.coordinates[0] : g.coordinates[0][0]; - if (!ring?.length) return [0, 0]; - let lat = 0; - let lon = 0; - for (const [x, y] of ring) { - lon += x; - lat += y; - } - return [lat / ring.length, lon / ring.length]; -} - -// 市境500m以内の市外公園。小平市の公園と同じ表に「市外」バッジ付きで並べる。 -const nearbyRows: Park[] = ((nearbyData as any).features ?? []).map((f: any) => { - const p = f.properties || {}; - const [lat, lon] = nearbyCentroid(f); - const area = Math.round(Number(p.area_m2) || 0); - const name = String(p.name || "").trim(); - // 市外の公園も公園名で開く。市名が分かる場合は市名を付ける。 - const mapsQuery = name - ? p.city - ? `${p.city}${name}` - : name - : `${lat},${lon}`; - return { - "公園名": name, - "住所": p.city ? `${p.city}(市外)` : "市外", - "町名": "市外", - "面積㎡": area ? String(area) : "", - "500㎡以上": "", - "種別": "市外", - "南西部指定管理": "", - "トイレ": "", - "水飲み場": "", - "ベンチ・椅子": "", - "テーブル": "", - "パーゴラ": "", - "東屋": "", - "防災用具": "", - "ボール遊び": "", - "遊具": "", - "駐車場": "", - "その他設備": "", - "緯度": String(lat), - "経度": String(lon), - "備考": `市外・小平市境から約${Math.round(Number(p.distance_m) || 0)}m`, - "設置者": "市外", - "状態": "", - "地図キー": `${p.osm_type || ""}/${p.osm_id || ""}`, - "地図URL": `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(mapsQuery)}`, - }; -}); - -const parks = [...(parksData as Park[]), ...nearbyRows]; - -// OpenStreetMapにポリゴンがあり、地図から位置を表示できる公園名。 -// 市外の近隣公園も地図に表示しているので、同じように地図で見られる。 -const mappedNames = new Set<string>([ - ...(osmData as any).features.map((f: any) => f.properties?.our_name).filter(Boolean), - ...((nearbyData as any).features ?? []) - .map((f: any) => String(f.properties?.name || "").trim()) - .filter(Boolean), -]); +import { parks, mappedNames, type Park } from "@/data/parks-rows"; const EMPTY = "—"; @@ -87,6 +18,12 @@ const TYPE_LABEL: Record<string, string> = { 団地内公園: "団地内公園", 児童遊園: "児童遊園", 市外: "市外の近隣公園", + 地域センター: "地域センター", + 公民館: "公民館", + 児童館: "児童館", + 図書館: "図書館", + 市役所支所: "市役所支所", + 文化施設: "文化施設", }; const TYPE_CLASS: Record<string, string> = { 街区: "parks-type--block", @@ -98,6 +35,30 @@ const TYPE_CLASS: Record<string, string> = { 団地内公園: "parks-type--danchi", 児童遊園: "parks-type--jidou", 市外: "parks-type--green", + 地域センター: "parks-type--facility", + 公民館: "parks-type--facility", + 児童館: "parks-type--facility", + 図書館: "parks-type--facility", + 市役所支所: "parks-type--facility", + 文化施設: "parks-type--facility", +}; +// 種別は列幅を小さくするため、漢字1文字の記号で表示する。 +const TYPE_MARK: Record<string, string> = { + 街区: "街", + 近隣: "近", + 運動: "運", + 広域: "広", + 広場: "場", + 緑地: "緑", + 団地内公園: "団", + 児童遊園: "児", + 市外: "外", + 地域センター: "📍", + 公民館: "🏛", + 児童館: "🧒", + 図書館: "📚", + 市役所支所: "🏢", + 文化施設: "🎭", }; // 市立以外の公園に付けるバッジ(設置者が空欄なら市立として扱う)。 @@ -125,6 +86,9 @@ function facilityBadges(p: Park): Badge[] { if (p["水飲み場"]) { badges.push({ kind: "water", label: "水飲み場", title: `水飲み場: ${p["水飲み場"]}` }); } + if (p["給水"]) { + badges.push({ kind: "water", label: "給水", title: `給水: ${p["給水"]}` }); + } if (p["遊具"]) { badges.push({ kind: "toy", label: "遊具", title: `遊具: ${p["遊具"]}` }); } @@ -150,12 +114,34 @@ function facTokens(p: Park): string { const tokens: string[] = []; if (p["トイレ"]) tokens.push("toilet"); if (p["水飲み場"]) tokens.push("water"); + if (p["給水"]) tokens.push("water"); if (p["遊具"]) tokens.push("toy"); + const play = p["遊具"] || ""; + if (play.includes("砂場")) tokens.push("sand"); + if (play.includes("ブランコ")) tokens.push("swing"); + if (play.includes("シーソー")) tokens.push("seesaw"); + if (play.includes("スプリング")) tokens.push("spring"); + if (play.includes("ジャングル")) tokens.push("jungle"); + if (play.includes("ヒコーキ") || play.includes("飛行機")) tokens.push("plane"); + if (play.includes("滑り台") || play.includes("滑台")) tokens.push("slide"); + if (play.includes("複合遊具")) tokens.push("complex"); + if (play.includes("ロープウェイ")) tokens.push("rope"); if (p["パーゴラ"]) tokens.push("pergola"); if (p["東屋"]) tokens.push("azumaya"); const ball = p["ボール遊び"]; if (ball && ball !== "禁止") tokens.push("ball"); if (p["南西部指定管理"]) tokens.push("cm"); + // ピクニック広場・芝生広場・テーブルのある公園。 + const picnicText = `${p["公園名"] || ""} ${p["種別"] || ""} ${p["その他設備"] || ""} ${p["備考"] || ""}`; + if ( + (p["テーブル"] || "").trim() || + p["種別"] === "広場" || + picnicText.includes("芝生") || + picnicText.includes("ピクニック") || + picnicText.includes("広場") + ) { + tokens.push("picnic"); + } return tokens.join(" "); } @@ -228,56 +214,32 @@ function searchIndex(p: Park): string { p["状態"], p["トイレ"], p["水飲み場"], + p["給水"], p["ボール遊び"], ...DETAIL_FIELDS.map((f) => p[f.key]), ] .join(" ") .toLowerCase(); } - -const towns = [...new Set(parks.map((p) => p["町名"]).filter(Boolean))].sort((a, b) => - a.localeCompare(b, "ja"), -); -const total = parks.length; --- <div class="parks" data-parks> - <div class="parks-controls"> - <label> - <span>町名</span> - <select data-parks-town> - <option value="">すべての町名</option> - {towns.map((town) => ( - <option value={town}>{town}</option> - ))} - </select> - </label> - <label> - <span>キーワード</span> - <input type="search" data-parks-search placeholder="公園名・住所・遊具など" /> - </label> - <fieldset class="parks-facilities"> - <legend>設備・管理で絞る</legend> - <label><input type="checkbox" data-fac-filter value="toilet" />トイレ</label> - <label><input type="checkbox" data-fac-filter value="water" />水飲み場</label> - <label><input type="checkbox" data-fac-filter value="toy" />遊具</label> - <label><input type="checkbox" data-fac-filter value="pergola" />パーゴラ</label> - <label><input type="checkbox" data-fac-filter value="azumaya" />東屋</label> - <label><input type="checkbox" data-fac-filter value="ball" />ボール遊び</label> - <label><input type="checkbox" data-fac-filter value="cm" />指定管理</label> - </fieldset> - <span class="parks-count" data-parks-count aria-live="polite">{total}件</span> - </div> - <p class="parks-hint">設備の情報は、市の資料などに基づきます。設備で絞ると、この情報がある公園だけが対象になります。</p> - <p class="parks-none" data-parks-none hidden>条件に合う公園が見つかりませんでした。</p> + <p class="parks-none" data-parks-none hidden>条件に合う公園・施設が見つかりませんでした。</p> <div class="parks-scroll"> <table> + <colgroup> + <col class="col-name" /> + <col class="col-address" /> + <col class="col-area" /> + <col class="col-type" /> + <col class="col-facility" /> + </colgroup> <thead> <tr> <th scope="col" aria-sort="none"> <button type="button" class="parks-sort" data-sort="name"> - 公園名<span class="parks-sort-mark" aria-hidden="true"></span> + 名称<span class="parks-sort-mark" aria-hidden="true"></span> </button> </th> <th scope="col">住所</th> @@ -293,13 +255,14 @@ const total = parks.length; <tbody> {parks.map((park) => ( <tr + data-kind={park["施設"] === "1" ? park["種別"] : "park"} data-town={park["町名"]} data-area={park["面積㎡"]} data-name={park["公園名"]} data-fac={facTokens(park)} data-search={searchIndex(park)} > - <td class="is-name" data-label="公園名"> + <td class="is-name" data-label="名称"> {mappedNames.has(park["公園名"]) ? ( <button type="button" @@ -308,6 +271,14 @@ const total = parks.length; data-focus-key={park["地図キー"] || ""} title="地図でこの公園の位置を表示" >{park["公園名"]}</button> + ) : park["公式URL"] ? ( + <a + class="park-name-link" + href={park["公式URL"]} + target="_blank" + rel="noopener" + title="公式ページを開く" + >{park["公園名"]}</a> ) : ( park["公園名"] )} @@ -344,7 +315,7 @@ const total = parks.length; title={TYPE_LABEL[park["種別"]]} aria-label={TYPE_LABEL[park["種別"]]} > - {park["種別"]} + <span aria-hidden="true">{TYPE_MARK[park["種別"]] || park["種別"].slice(0, 1)}</span> </span> ) : ( EMPTY @@ -424,16 +395,35 @@ const total = parks.length; opacity: 1; } .parks-scroll { - overflow-x: auto; + max-height: 70vh; + overflow: auto; + overscroll-behavior: contain; -webkit-overflow-scrolling: touch; border: 1px solid var(--sl-color-hairline, #ddd); border-radius: 0.3rem; } .parks table { width: 100%; + min-width: 760px; + table-layout: fixed; border-collapse: collapse; font-size: 0.85rem; } + .parks col.col-name { + width: 24%; + } + .parks col.col-address { + width: 18%; + } + .parks col.col-area { + width: 11%; + } + .parks col.col-type { + width: 7%; + } + .parks col.col-facility { + width: 40%; + } .parks th, .parks td { border-bottom: 1px solid var(--sl-color-hairline, #ddd); @@ -481,6 +471,9 @@ const total = parks.length; } .parks .is-name { font-weight: 600; + white-space: normal; + overflow-wrap: anywhere; + line-height: 1.5; } .parks-owner { display: inline-block; @@ -521,6 +514,8 @@ const total = parks.length; white-space: nowrap; } .park-name-link { + display: inline-block; + max-width: 100%; padding: 0; border: 0; background: none; @@ -532,6 +527,8 @@ const total = parks.length; text-decoration: underline; text-decoration-color: var(--sl-color-gray-4); text-underline-offset: 2px; + white-space: normal; + overflow-wrap: anywhere; } .park-name-link:hover { color: var(--sl-color-accent); @@ -579,16 +576,15 @@ const total = parks.length; .parks-type { display: inline-flex; align-items: center; - gap: 0.3em; - font-size: 0.9em; - } - .parks-type::before { - content: ""; - width: 0.7em; - height: 0.7em; - border-radius: 50%; - background: var(--type-color, #888); - flex: none; + justify-content: center; + width: 1.5em; + height: 1.5em; + border-radius: 0.35em; + background: color-mix(in srgb, var(--type-color, #888) 18%, transparent); + color: var(--type-color, #888); + font-size: 0.8em; + font-weight: 700; + line-height: 1; } .parks-type--block { --type-color: #2e9e4f; @@ -614,6 +610,10 @@ const total = parks.length; .parks-type--jidou { --type-color: #d81b60; } + .parks-type--facility { + --type-color: #00838f; + font-size: 1em; + } .parks .is-facility { white-space: normal; min-width: 12rem; @@ -674,12 +674,15 @@ const total = parks.length; <script> document.querySelectorAll("[data-parks]").forEach((root) => { - const select = root.querySelector<HTMLSelectElement>("[data-parks-town]"); - const search = root.querySelector<HTMLInputElement>("[data-parks-search]"); - const count = root.querySelector("[data-parks-count]"); + const select = document.querySelector<HTMLSelectElement>("[data-parks-town]"); + const kindSelect = document.querySelector<HTMLSelectElement>("[data-parks-kind]"); + const search = document.querySelector<HTMLInputElement>("[data-parks-search]"); + const count = document.querySelector("[data-parks-count]"); const none = root.querySelector<HTMLElement>("[data-parks-none]"); const tbody = root.querySelector("tbody"); - const checks = Array.from(root.querySelectorAll<HTMLInputElement>("[data-fac-filter]")); + const checks = Array.from( + document.querySelectorAll<HTMLInputElement>("[data-fac-filter]"), + ); const sortButtons = Array.from(root.querySelectorAll<HTMLButtonElement>("[data-sort]")); if (!tbody) return; @@ -703,17 +706,19 @@ const total = parks.length; let selectedName = ""; const applyFilter = () => { + const kind = kindSelect ? kindSelect.value : ""; const town = select ? select.value : ""; const query = search ? search.value.trim().toLowerCase() : ""; const wanted = checks.filter((c) => c.checked).map((c) => c.value); let shown = 0; allRows().forEach((row) => { const facs = (row.dataset.fac || "").split(" "); + const okKind = !kind || row.dataset.kind === kind; const okTown = !town || row.dataset.town === town; const okText = !query || (row.dataset.search || "").includes(query); const okFac = wanted.every((token) => facs.includes(token)); const okSelected = !selectedName || row.dataset.name === selectedName; - const ok = okTown && okText && okFac && okSelected; + const ok = okKind && okTown && okText && okFac && okSelected; row.classList.toggle("is-hidden", !ok); if (ok) shown += 1; }); @@ -729,7 +734,7 @@ const total = parks.length; new CustomEvent("park:filter", { detail: { names: visibleNames, - active: Boolean(town || query || wanted.length || selectedName), + active: Boolean(kind || town || query || wanted.length || selectedName), fromMap, }, }), @@ -765,6 +770,10 @@ const total = parks.length; clearSelected(); applyFilter(); }); + kindSelect?.addEventListener("change", () => { + clearSelected(); + applyFilter(); + }); search?.addEventListener("input", () => { clearSelected(); applyFilter(); @@ -784,6 +793,7 @@ const total = parks.length; selectedName = name; if (search) search.value = name; if (select) select.value = ""; + if (kindSelect) kindSelect.value = ""; checks.forEach((c) => { c.checked = false; }); diff --git a/src/content/docs/kodaira-data/parks.mdx b/src/content/docs/kodaira-data/parks.mdx index b1480d4..429e21a 100644 --- a/src/content/docs/kodaira-data/parks.mdx +++ b/src/content/docs/kodaira-data/parks.mdx @@ -1,32 +1,33 @@ --- -title: 小平市の公園一覧 -description: 小平市内の公園355件と市境500m以内の市外公園46件を、町名・キーワード・設備で検索できる一覧にまとめました。都立公園や都営・UR住宅内の公園、所有者未確認の緑地も含みます。小平市議会議員 安竹洋平の活動記録です。問題の経緯、市の対応、議会での質疑や答弁、関連する資料や会議録へのリンクをまとめています。市政の課題解決に向けた取り組みの記録としてご覧ください。 +title: こだいら 公園さんぽマップ +draft: true +description: 小平市と周辺の公園・公共施設一覧(愛称:こだいら 公園さんぽマップ)。小平市内の公園355件と市境500m以内の市外公園48件、周辺の図書館・公民館・児童館などの公共施設を、町名・キーワード・設備で検索できる一覧にまとめました。都立公園や都営・UR住宅内の公園、所有者未確認の緑地も含みます。小平市議会議員 安竹洋平の活動記録です。 --- +import ParksFilters from '@/components/ParksFilters.astro'; import ParksTable from '@/components/ParksTable.astro'; import ParkMap from '@/components/ParkMap.astro'; -import FacilitiesTable from '@/components/FacilitiesTable.astro'; +import ParkMapHelp from '@/components/ParkMapHelp.astro'; import PwaRegister from '@/components/PwaRegister.astro'; -小平市内の公園と、市外にまたがる都立小金井公園、所有者未確認の緑地などの一覧です。市境500m以内の市外公園も同じ表に「市外」バッジ付きで含めています。町名や設備で絞り込んだり、公園名や住所、遊具などのキーワードで検索したりできます。公園名と面積の見出しを押すと、並べ替えられます。住所を押すと、公園名でGoogleマップを開きます。公園名の横の「都立」「都営」「UR」「民間」「未確認」などのバッジは、市立以外または所有者・名称が未確認の公園・緑地であることを示します。スマートフォンでは表を横にスクロールしてご覧ください。 +<ParksFilters /> -:::note -設備(トイレ・水飲み場・遊具など)の情報は、すべての公園についてそろっているわけではありません。情報が不足している公園については、今後、順次追記していく予定です。 -::: +<ParkMap /> -:::note -対応ブラウザでは、アドレスバーのインストールアイコンから、このページをアプリとして保存できます。ホーム画面に追加すると、地図・一覧をアプリのように開けます。オフライン時は、一度開いたページと表を表示できます(地図タイルは通信が必要です)。 -::: +<ParksTable /> -<PwaRegister /> +<ParkMapHelp /> -## 公園の位置 +<details class="page-help"> +<summary>設備情報について</summary> -<ParkMap /> +設備(トイレ・水飲み場・給水・遊具など)の情報は、すべての公園・施設についてそろっているわけではありません。情報が不足している公園については、今後、順次追記していく予定です。上のフィルタは、公園と公共施設の両方に適用されます。「設備・遊具で絞る」では砂場・ブランコ・シーソー・スプリング遊具・ジャングルジム・ヒコーキ・滑り台・複合遊具・ロープウェイで絞れます。「ピクニック・広場」は、広場・テーブル・芝生のある公園をまとめて表示します。 -<ParksTable /> +</details> + +<details class="page-help"> +<summary>種別の見方</summary> -:::note 「種別」の「街区」「近隣」「運動」「広域」は、都市公園法に基づく公園の区分です。「広場」「緑地」「団地内公園」「児童遊園」は、この一覧独自の分類です。 - **街区公園**:主に近くに住む人が利用する公園。おおむね2,500㎡が標準。 @@ -37,40 +38,65 @@ import PwaRegister from '@/components/PwaRegister.astro'; - **緑地**:公園台帳に載っていない緑地・樹林地など。 - **団地内公園**:都営住宅などの団地内にある公園。 - **児童遊園**:地域の児童遊園。 -::: -:::note -「市外」バッジの公園は、小平市の市境から500m以内にある市外の公園です。小平市の公園一覧(355件)とは別に、同じ表に含めています。距離は市境までの目安です。Googleマップのリンクは公園名で開きます。 -::: +表の「種別」は列幅を小さくするため、漢字1文字の記号で表示しています。街=街区公園、近=近隣公園、運=運動公園、広=広域公園、場=広場、緑=緑地、団=団地内公園、児=児童遊園、外=市外の近隣公園です。公共施設は、📍=地域センター、🏛=公民館、🧒=児童館、📚=図書館、🏢=市役所支所、🎭=文化施設です。記号にカーソルを合わせると正式名が表示されます。 -:::note -**手入力ポリゴンのライセンス**: このページの手入力ポリゴン(公園の範囲)は安竹洋平が作成したものです。正確な境界座標ではありません。出典として「安竹洋平(https://yasutakeyohei.com/)」を明示すれば、自由に利用できます。ライセンスは [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) です([ライセンス全文](/kodaira-data/parks/LICENSE.txt))。OSM由来のデータはODbLです。 -::: +</details> -## 暑さ・寒さをしのげる公共施設 +<details class="page-help"> +<summary>市外の近隣公園について</summary> -地域センター・公民館・図書館・市役所支所・文化施設の一覧です。トイレ・給水・開館時間を確認できます。暑さ・寒さをしのぐ場所を探すときの参考にしてください。 +「市外」バッジの公園は、小平市の市境から500m以内にある市外の公園です。小平市の公園一覧(355件)とは別に、同じ表に含めています。距離は市境までの目安です。Googleマップのリンクは公園名で開きます。範囲が取得できない公園は、地図では丸(点)で表示しています。 -<FacilitiesTable /> +</details> -## この一覧について +<details class="page-help"> +<summary>公共施設について</summary> + +小平市内の地域センター・公民館・図書館・市役所支所・文化施設と、市境500m以内の市外の図書館・公民館・児童館を、公園と同じ表に含めています。公共施設の行は、種別・開館時間・休館日・トイレ・給水を「施設・備考」にまとめています。市外施設の情報はOpenStreetMapによるもので、トイレ・給水は「未確認」としています。 + +</details> + +<details class="page-help"> +<summary>この一覧について</summary> + +「こだいら 公園さんぽマップ」は、小平市と周辺の公園・公共施設の一覧です。 -:::note - 面積の「小・中・大」は目安です。小は1000㎡未満、中は1000〜10000㎡、大は10000㎡以上としています。 - 「種別」は一部の公園のみ掲載しています。全市分は公開されていません。 -- 公園名の横に「都立」「都営」「UR」「民間」「未確認」などのバッジがある公園は、市立以外または所有者・名称が未確認の公園・緑地です。 +- 名称の横に「都立」「都営」「UR」「民間」「未確認」などのバッジがある公園は、市立以外または所有者・名称が未確認の公園・緑地です。 - 「都立」は都立公園、「都営」は東京都営住宅内の公園・児童遊園、「UR」はUR都市機構の団地内の公園です。 -- 「市外」バッジの公園は市外の近隣公園です。小平市の公園一覧(355件)には含めていませんが、表には表示しています。 +- 「市外」バッジの公園・施設は、小平市の市境から500m以内にある市外の公園・公共施設です。小平市の公園一覧(355件)には含めていませんが、表には表示しています。 - 「閉鎖」バッジの公園は現在閉鎖中です。 - 名前のない緑地は「緑地(OSM 番号)」と表示しています。番号はOpenStreetMapのIDです。 -- 設備の情報は、すべての公園についてそろっているわけではありません。設備で絞ると、この情報がある公園だけが対象になります。 +- 公共施設の行は、種別・開館時間・休館日・トイレ・給水を「施設・備考」にまとめています。市外の公共施設はOpenStreetMapをもとにしています。 +- 設備の情報は、すべての公園・施設についてそろっているわけではありません。設備で絞ると、この情報がある公園・施設だけが対象になります。 - 設備の情報は、現在と異なる場合や、記載の誤りが含まれる場合があります。ご了承ください。 - 面積が不明な11公園も掲載しています。面積は「—」としています。 - 空欄は「—」で示しています。情報の記載がないという意味であり、設備がないという意味ではありません。 -::: -## ほかのサイトでの公園紹介 +</details> + +<details class="page-help"> +<summary>アプリとして保存</summary> + +対応ブラウザでは、アドレスバーのインストールアイコンから、このページをアプリとして保存できます。ホーム画面に追加すると、地図・一覧をアプリのように開けます。オフライン時は、一度開いたページと表を表示できます(地図タイルは通信が必要です)。 + +</details> + +<PwaRegister /> + +<details class="page-help"> +<summary>ほかのサイトでの公園紹介</summary> PARKFULというサイトでも、小平市の公園の紹介が掲載されています。 - [PARKFUL(外部サイト)](https://parkful.net/) + +</details> + +## ライセンス + +:::note +**手入力ポリゴンのライセンス**: このページの手入力ポリゴン(公園の範囲)は安竹洋平が作成したものです。正確な境界座標ではありません。出典として「安竹洋平(https://yasutakeyohei.com/)」を明示すれば、自由に利用できます。ライセンスは [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) です([ライセンス全文](/kodaira-data/parks/LICENSE.txt))。OSM由来のデータはODbLです。地図の遊具アイコン(ブランコ、シーソー、スプリング遊具、砂場、複合遊具、ピクニック・広場、ジャングルジム、ヒコーキ、滑り台、ロープウェイ、ボール遊び)は [SVGRepo](https://www.svgrepo.com/) の素材を利用しています。各素材の作者・ライセンスは SVGRepo の該当ページおよび[ライセンス説明](https://www.svgrepo.com/page/licensing/)をご確認ください。 +::: diff --git a/src/data/kodaira-nearby-facilities.json b/src/data/kodaira-nearby-facilities.json new file mode 100644 index 0000000..bb0cb99 --- /dev/null +++ b/src/data/kodaira-nearby-facilities.json @@ -0,0 +1,306 @@ +[ + { + "name": "若葉児童館", + "type": "児童館", + "address": "", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約56m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420796574", + "lat": 35.723946, + "lon": 139.437154, + "geocode": "OSM", + "city": "", + "distance_m": 56, + "osm_type": "node", + "osm_id": 1420796574, + "nearby": true + }, + { + "name": "西集会場", + "type": "公民館", + "address": "", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約62m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/2137169484", + "lat": 35.723583, + "lon": 139.438922, + "geocode": "OSM", + "city": "", + "distance_m": 62, + "osm_type": "node", + "osm_id": 2137169484, + "nearby": true + }, + { + "name": "東久留米市立滝山図書館", + "type": "図書館", + "address": "東久留米市滝山四丁目1-10", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約198m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420782537", + "lat": 35.7448193, + "lon": 139.5044328, + "geocode": "OSM", + "city": "東久留米市", + "distance_m": 198, + "osm_type": "node", + "osm_id": 1420782537, + "nearby": true + }, + { + "name": "東村山市立萩山図書館", + "type": "図書館", + "address": "東村山市萩山町二丁目13-1", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約203m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420785828", + "lat": 35.741577, + "lon": 139.478659, + "geocode": "OSM", + "city": "東村山市", + "distance_m": 203, + "osm_type": "node", + "osm_id": 1420785828, + "nearby": true + }, + { + "name": "滝山児童館", + "type": "児童館", + "address": "", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約209m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420789295", + "lat": 35.7448811, + "lon": 139.504537, + "geocode": "OSM", + "city": "", + "distance_m": 209, + "osm_type": "node", + "osm_id": 1420789295, + "nearby": true + }, + { + "name": "東村山市立富士見図書館", + "type": "図書館", + "address": "東京都東村山市富士見町1-7-35", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約236m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420785824", + "lat": 35.744013, + "lon": 139.461267, + "geocode": "OSM", + "city": "", + "distance_m": 236, + "osm_type": "node", + "osm_id": 1420785824, + "nearby": true + }, + { + "name": "西東京市芝久保児童館", + "type": "児童館", + "address": "", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約285m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/12609116867", + "lat": 35.7247015, + "lon": 139.5257321, + "geocode": "OSM", + "city": "", + "distance_m": 285, + "osm_type": "node", + "osm_id": 12609116867, + "nearby": true + }, + { + "name": "くぬぎ児童館", + "type": "児童館", + "address": "東久留米市前沢四丁目6-3", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約304m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420763963", + "lat": 35.744363, + "lon": 139.512841, + "geocode": "OSM", + "city": "東久留米市", + "distance_m": 304, + "osm_type": "node", + "osm_id": 1420763963, + "nearby": true + }, + { + "name": "栄町児童館", + "type": "児童館", + "address": "東村山市栄町三丁目14-1", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約325m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420786210", + "lat": 35.746611, + "lon": 139.469178, + "geocode": "OSM", + "city": "東村山市", + "distance_m": 325, + "osm_type": "node", + "osm_id": 1420786210, + "nearby": true + }, + { + "name": "西東京市芝久保コミュニティセンター", + "type": "公民館", + "address": "", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約395m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/12592346994", + "lat": 35.7291938, + "lon": 139.5218464, + "geocode": "OSM", + "city": "", + "distance_m": 395, + "osm_type": "node", + "osm_id": 12592346994, + "nearby": true + }, + { + "name": "富士見児童館", + "type": "児童館", + "address": "東京都東村山市富士見町5-4-51", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約415m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420775215", + "lat": 35.746689, + "lon": 139.462891, + "geocode": "OSM", + "city": "", + "distance_m": 415, + "osm_type": "node", + "osm_id": 1420775215, + "nearby": true + }, + { + "name": "立川市若葉図書館", + "type": "図書館", + "address": "東京都立川市若葉町3-34-1", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約426m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420795509", + "lat": 35.720189, + "lon": 139.439191, + "geocode": "OSM", + "city": "", + "distance_m": 426, + "osm_type": "node", + "osm_id": 1420795509, + "nearby": true + }, + { + "name": "東大和市立さくらがおか児童館", + "type": "児童館", + "address": "東京都東大和市桜が丘3-44-13", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約440m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420785706", + "lat": 35.733778, + "lon": 139.420016, + "geocode": "OSM", + "city": "", + "distance_m": 440, + "osm_type": "node", + "osm_id": 1420785706, + "nearby": true + }, + { + "name": "東大和市立桜が丘図書館", + "type": "図書館", + "address": "東京都東大和市桜が丘3-44-13", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約440m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420785723", + "lat": 35.733789, + "lon": 139.420043, + "geocode": "OSM", + "city": "", + "distance_m": 440, + "osm_type": "node", + "osm_id": 1420785723, + "nearby": true + }, + { + "name": "国分寺市立本多図書館", + "type": "図書館", + "address": "国分寺市本多一丁目7-1", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約475m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420771584", + "lat": 35.705356, + "lon": 139.482337, + "geocode": "OSM", + "city": "国分寺市", + "distance_m": 475, + "osm_type": "node", + "osm_id": 1420771584, + "nearby": true + }, + { + "name": "本多児童館", + "type": "児童館", + "address": "国分寺市本多一丁目7-1", + "hours": "", + "closed": "", + "toilet": "未確認", + "water": "未確認", + "note": "市外・小平市境から約486m(OpenStreetMap)", + "source": "https://www.openstreetmap.org/node/1420781732", + "lat": 35.7052411, + "lon": 139.4823082, + "geocode": "OSM", + "city": "国分寺市", + "distance_m": 486, + "osm_type": "node", + "osm_id": 1420781732, + "nearby": true + } +] diff --git a/src/data/kodaira-nearby-parks.json b/src/data/kodaira-nearby-parks.json index bf93411..81985d3 100644 --- a/src/data/kodaira-nearby-parks.json +++ b/src/data/kodaira-nearby-parks.json @@ -1 +1 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"すみれ公園","distance_m":4,"area_m2":1118,"osm_type":"way","osm_id":542686107,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.49887,35.7458891],[139.4987681,35.745741],[139.4984301,35.7459239],[139.4986072,35.7462004],[139.4988244,35.7460828],[139.4987949,35.7459696],[139.49887,35.7458891]]]}},{"type":"Feature","properties":{"name":"若葉緑地","distance_m":4,"area_m2":3395,"osm_type":"way","osm_id":591305877,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4348041,35.7255946],[139.4341234,35.7255259],[139.4341039,35.7250579],[139.4344686,35.7250685],[139.4344816,35.7252139],[139.4349962,35.725235],[139.4350255,35.7252958],[139.4348041,35.7255946]]]}},{"type":"Feature","properties":{"name":"あじさい公園","distance_m":5,"area_m2":2317,"osm_type":"way","osm_id":1521995587,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5066696,35.7418588],[139.5062131,35.7420472],[139.5059587,35.7416542],[139.5064167,35.7414578],[139.5066696,35.7418588]]]}},{"type":"Feature","properties":{"name":"富士見町1丁目第2仲よし広場","distance_m":5,"area_m2":1727,"osm_type":"way","osm_id":965224418,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4551618,35.7387211],[139.4563827,35.739249],[139.4557987,35.7392483],[139.4557816,35.7390298],[139.4551012,35.7389491],[139.4551099,35.7388152],[139.4551618,35.7387211]]]}},{"type":"Feature","properties":{"name":"青梅橋公園","distance_m":22,"area_m2":388,"osm_type":"way","osm_id":754232094,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4324421,35.7321674],[139.4327844,35.7321617],[139.4328439,35.7321892],[139.4328557,35.7321666],[139.4324314,35.7319798],[139.4324421,35.7321674]]]}},{"type":"Feature","properties":{"name":"桜が丘一丁目公園","distance_m":28,"area_m2":3067,"osm_type":"way","osm_id":754232096,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4306727,35.7316731],[139.4314557,35.7316397],[139.4323502,35.7321376],[139.4323444,35.732073],[139.4323392,35.7320163],[139.4323165,35.7319804],[139.430697,35.7312184],[139.4306426,35.7312446],[139.4306727,35.7316731]]]}},{"type":"Feature","properties":{"name":"青梅橋東公園","distance_m":28,"area_m2":148,"osm_type":"way","osm_id":754232095,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4329793,35.7324124],[139.4332489,35.7324111],[139.4329747,35.7323036],[139.4329793,35.7324124]]]}},{"type":"Feature","properties":{"name":"南台公園","distance_m":34,"area_m2":7777,"osm_type":"way","osm_id":591322526,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4586433,35.7420302],[139.4591958,35.7420476],[139.4592366,35.7420302],[139.4593017,35.7410812],[139.4591617,35.7410363],[139.4592431,35.7408433],[139.4588588,35.7406583],[139.4586439,35.740986],[139.4586433,35.7420302]]]}},{"type":"Feature","properties":{"name":"用水北緑地","distance_m":51,"area_m2":2199,"osm_type":"way","osm_id":693736574,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4426016,35.7373346],[139.4426667,35.7367311],[139.4427431,35.736731],[139.4425606,35.7382661],[139.4427828,35.7382803],[139.4427783,35.7383217],[139.4425624,35.7383098],[139.4424377,35.7392776],[139.4424678,35.7393173],[139.4423803,35.7393139],[139.4424744,35.7384284],[139.4423704,35.7384089],[139.4422463,35.7384134],[139.4422664,35.7382484],[139.4425005,35.7382633],[139.4426016,35.7373346]]]}},{"type":"Feature","properties":{"name":"富士見町2丁目第2仲よし広場","distance_m":64,"area_m2":76,"osm_type":"way","osm_id":965225385,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4548178,35.7393066],[139.454867,35.73931],[139.4549317,35.7392079],[139.4548302,35.7392088],[139.4548178,35.7393066]]]}},{"type":"Feature","properties":{"name":"萩山公園","distance_m":69,"area_m2":9856,"osm_type":"way","osm_id":1462182280,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.473544,35.743935],[139.4735794,35.7439436],[139.474243,35.7435759],[139.4737767,35.7426251],[139.4728893,35.743005],[139.4729362,35.7431142],[139.4730193,35.7432377],[139.473544,35.743935]]]}},{"type":"Feature","properties":{"name":"コウヤマキ公園","distance_m":76,"area_m2":234,"osm_type":"way","osm_id":817978652,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4935696,35.7084541],[139.4935476,35.708332],[139.4937318,35.7083087],[139.4937538,35.708417],[139.4937284,35.7084376],[139.4935696,35.7084541]]]}},{"type":"Feature","properties":{"name":"萩山四季の森公園","distance_m":76,"area_m2":9260,"osm_type":"way","osm_id":375322224,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4753838,35.7425524],[139.4754466,35.7428113],[139.4754044,35.7430008],[139.474911,35.7431718],[139.4742411,35.7435494],[139.4737718,35.7426025],[139.4742316,35.7424093],[139.4745409,35.7428655],[139.4753838,35.7425524]]]}},{"type":"Feature","properties":{"name":"北町公園","distance_m":77,"area_m2":4613,"osm_type":"way","osm_id":591177446,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4510419,35.721191],[139.4516704,35.721111],[139.4514732,35.7203655],[139.4510357,35.7204256],[139.4510542,35.7205457],[139.4508817,35.7205657],[139.4510419,35.721191]]]}},{"type":"Feature","properties":{"name":"芝久保三丁目ふれあい公園","distance_m":88,"area_m2":2550,"osm_type":"way","osm_id":1362560908,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5191406,35.7293977],[139.5191246,35.729057],[139.5189069,35.7290647],[139.5189106,35.7291702],[139.5187826,35.7291731],[139.5187789,35.7290676],[139.5183128,35.7290842],[139.5183419,35.7292174],[139.5183864,35.7294214],[139.5187654,35.7294077],[139.5187763,35.7294125],[139.518822,35.7294106],[139.5191406,35.7293977]]]}},{"type":"Feature","properties":{"name":"萩山公園","distance_m":105,"area_m2":2063,"osm_type":"way","osm_id":558149589,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4753972,35.7422672],[139.4753234,35.7420898],[139.4752765,35.7419395],[139.4757566,35.7417251],[139.475935,35.74208],[139.4753972,35.7422672]]]}},{"type":"Feature","properties":{"name":"さつき公園","distance_m":113,"area_m2":1974,"osm_type":"way","osm_id":543026519,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5096639,35.7411551],[139.5094266,35.7407812],[139.5098426,35.7406074],[139.5100752,35.74099],[139.5096639,35.7411551]]]}},{"type":"Feature","properties":{"name":"桜町こぶし公園","distance_m":121,"area_m2":152,"osm_type":"way","osm_id":593771700,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5094476,35.7133134],[139.5096078,35.7133184],[139.5096113,35.7132257],[139.5094432,35.7132222],[139.5094476,35.7133134]]]}},{"type":"Feature","properties":{"name":"富士見町1丁目第3仲よし広場","distance_m":184,"area_m2":1041,"osm_type":"way","osm_id":668907354,"city":"東村山市","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4624137,35.7440997],[139.4619222,35.7440867],[139.4619188,35.7440971],[139.4619148,35.7442438],[139.4619138,35.7442969],[139.4624022,35.7443098],[139.4624137,35.7440997]]]}},{"type":"Feature","properties":{"name":"ひょうたん公園","distance_m":214,"area_m2":3515,"osm_type":"way","osm_id":563674138,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4974454,35.7085293],[139.497419,35.708478],[139.4973493,35.7082461],[139.4973206,35.708145],[139.4973091,35.7081114],[139.4972735,35.7079576],[139.4972279,35.707808],[139.4975624,35.7077753],[139.4977517,35.7077645],[139.4978495,35.7085013],[139.4976645,35.7085113],[139.4974454,35.7085293]]]}},{"type":"Feature","properties":{"name":"東大和南公園","distance_m":218,"area_m2":100408,"osm_type":"way","osm_id":165197251,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4209315,35.7317592],[139.4210368,35.7335495],[139.4228427,35.7334823],[139.4251194,35.7333976],[139.4252208,35.7346496],[139.4239703,35.7347091],[139.4222181,35.734803],[139.4222618,35.735362],[139.4224708,35.7353468],[139.4225686,35.7366797],[139.4227799,35.7369273],[139.4214214,35.7372722],[139.4213175,35.7372214],[139.4208196,35.7356856],[139.4207725,35.735498],[139.4207539,35.7353756],[139.4205485,35.7319114],[139.4206145,35.731831],[139.4209315,35.7317592]]]}},{"type":"Feature","properties":{"name":"伊豆殿公園","distance_m":219,"area_m2":3314,"osm_type":"way","osm_id":559748180,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4712554,35.7457789],[139.470588,35.745467],[139.4706381,35.7453434],[139.4706724,35.7451599],[139.471454,35.745248],[139.4712554,35.7457789]]]}},{"type":"Feature","properties":{"name":"上新堀公園","distance_m":229,"area_m2":1400,"osm_type":"way","osm_id":249323019,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.446034,35.7371745],[139.4454807,35.7371991],[139.4454475,35.7372268],[139.4454378,35.7374589],[139.4457612,35.7373604],[139.4458173,35.7373613],[139.4458653,35.737449],[139.4460174,35.737471],[139.4460316,35.7374293],[139.4460628,35.7371958],[139.446034,35.7371745]]]}},{"type":"Feature","properties":{"name":"東村山中央公園","distance_m":253,"area_m2":110590,"osm_type":"relation","osm_id":905357,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4630754,35.7451259],[139.4631504,35.7451587],[139.4631614,35.7452243],[139.4632129,35.745581],[139.4633325,35.7461754],[139.4623581,35.7466046],[139.4626251,35.7470134],[139.4559699,35.7499453],[139.4550786,35.7486398],[139.4555606,35.7484224],[139.4557073,35.7483551],[139.4561157,35.7481626],[139.4565651,35.7481079],[139.4566696,35.748064],[139.4570957,35.7478672],[139.4576711,35.7476035],[139.457848,35.747517],[139.4580643,35.7478251],[139.4594136,35.747169],[139.4591983,35.7468401],[139.4595307,35.7466209],[139.459648,35.7465698],[139.4604244,35.7462363],[139.4612164,35.7459074],[139.4630754,35.7451259]],[[139.4556474,35.7486024],[139.455629,35.7486708],[139.4556505,35.7486844],[139.4556765,35.7487043],[139.4556934,35.7487317],[139.4557041,35.7487553],[139.4557072,35.7487889],[139.4556964,35.7488125],[139.4556811,35.7488287],[139.4556627,35.7488399],[139.4557684,35.7489556],[139.4558619,35.7490302],[139.4559646,35.7492602],[139.4559891,35.749356],[139.4560688,35.7494145],[139.4561393,35.7494493],[139.4562036,35.749458],[139.456337,35.7494455],[139.4564442,35.7494256],[139.4565346,35.7493921],[139.4566235,35.7493386],[139.4567001,35.7492777],[139.4567951,35.7491682],[139.4568626,35.7490886],[139.4569131,35.7489817],[139.4569392,35.748907],[139.4569422,35.7488138],[139.456953,35.7487765],[139.4569622,35.7487454],[139.4569622,35.7487193],[139.4569407,35.748677],[139.456884,35.7486235],[139.4568334,35.7485601],[139.4567951,35.7485165],[139.4567614,35.7485029],[139.4566771,35.7484842],[139.4565668,35.7484432],[139.4564641,35.7484034],[139.4563906,35.7483822],[139.4563109,35.7483685],[139.4562772,35.7483797],[139.4562036,35.7484171],[139.4561209,35.7484506],[139.4560688,35.748483],[139.4559707,35.7485277],[139.4559339,35.748534],[139.4558895,35.7485364],[139.4558604,35.748529],[139.4558267,35.7485203],[139.455796,35.7485203],[139.4557639,35.748529],[139.4557317,35.7485377],[139.4556842,35.7485588],[139.4556597,35.7485812],[139.4556474,35.7486024]]]}},{"type":"Feature","properties":{"name":"向原南公園","distance_m":270,"area_m2":819,"osm_type":"way","osm_id":726612491,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4359293,35.7366135],[139.4365248,35.736532],[139.4365318,35.7364941],[139.4359702,35.7363718],[139.4359293,35.7366135]]]}},{"type":"Feature","properties":{"name":"ハンカチの木公園","distance_m":278,"area_m2":328,"osm_type":"way","osm_id":754224515,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4301443,35.7338879],[139.4302737,35.7338835],[139.4302597,35.7336183],[139.4301318,35.7336491],[139.4301443,35.7338879]]]}},{"type":"Feature","properties":{"name":"ケヤキ広場","distance_m":291,"area_m2":4258,"osm_type":"way","osm_id":568392686,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4909904,35.7057],[139.4908229,35.7047654],[139.4912664,35.704713],[139.4914339,35.7056476],[139.4909904,35.7057]]]}},{"type":"Feature","properties":{"name":"こもれび公園","distance_m":299,"area_m2":101,"osm_type":"way","osm_id":757512355,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5082556,35.709857],[139.5082561,35.7098051],[139.5084498,35.7098065],[139.5084493,35.7098583],[139.5082556,35.709857]]]}},{"type":"Feature","properties":{"name":"いけとおがわ プレイパーク","distance_m":307,"area_m2":4978,"osm_type":"relation","osm_id":8098101,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4947101,35.7065445],[139.4945763,35.7057813],[139.4952471,35.7057015],[139.4953765,35.7064665],[139.4947101,35.7065445]],[[139.4950893,35.705819],[139.4951094,35.7057874],[139.4951631,35.7057743],[139.4952167,35.70577],[139.4952529,35.7058048],[139.4952489,35.7058429],[139.4952167,35.7058571],[139.4951604,35.705856],[139.4951135,35.7058549],[139.495088,35.7058407],[139.4950772,35.7058603],[139.4950196,35.7058701],[139.4949861,35.7058701],[139.4949364,35.705844],[139.4949029,35.70582],[139.4949203,35.7057934],[139.4949485,35.705771],[139.4950088,35.7057558],[139.4950759,35.7057917],[139.4950893,35.705819]]]}},{"type":"Feature","properties":{"name":"上水公園","distance_m":308,"area_m2":19730,"osm_type":"way","osm_id":792340208,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5059775,35.7094834],[139.5058815,35.7086994],[139.5058559,35.7083302],[139.5058523,35.7083008],[139.5057683,35.708308],[139.5044173,35.708411],[139.504473,35.7087899],[139.5044731,35.7089689],[139.5044598,35.7089901],[139.5041879,35.7090139],[139.5042312,35.7093208],[139.5041473,35.7093724],[139.5041645,35.7094737],[139.5042089,35.7097347],[139.5047365,35.7096957],[139.5047224,35.7095896],[139.5059775,35.7094834]]]}},{"type":"Feature","properties":{"name":"向原公園","distance_m":324,"area_m2":1214,"osm_type":"way","osm_id":165192452,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4399088,35.7391889],[139.4399439,35.7388288],[139.4397553,35.7388103],[139.4397604,35.7387533],[139.4402103,35.7387855],[139.4401618,35.7392089],[139.4399088,35.7391889]]]}},{"type":"Feature","properties":{"name":"桜町公園","distance_m":350,"area_m2":921,"osm_type":"way","osm_id":742459128,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5092376,35.7095485],[139.5095374,35.7095478],[139.5095394,35.7092411],[139.509242,35.7092419],[139.5092376,35.7095485]]]}},{"type":"Feature","properties":{"name":"桜町大尽の坂公園","distance_m":399,"area_m2":52,"osm_type":"way","osm_id":742758113,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5108746,35.7097898],[139.5109289,35.7097898],[139.51093,35.7096954],[139.5108746,35.7096959],[139.5108746,35.7097898]]]}},{"type":"Feature","properties":{"name":"幸五公園","distance_m":406,"area_m2":3328,"osm_type":"way","osm_id":943828613,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4193357,35.7277353],[139.4191898,35.7269514],[139.4195889,35.7269375],[139.4197434,35.7278084],[139.4195289,35.7277771],[139.4193357,35.7277353]]]}},{"type":"Feature","properties":{"name":"滝山公園","distance_m":407,"area_m2":24024,"osm_type":"way","osm_id":542874443,"city":"東久留米市","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5097602,35.7465694],[139.5089221,35.745216],[139.510346,35.7446456],[139.5108954,35.7455637],[139.5111656,35.74601],[139.5102752,35.7463629],[139.5097602,35.7465694]]]}},{"type":"Feature","properties":{"name":"桜みらい公園","distance_m":412,"area_m2":485,"osm_type":"way","osm_id":754224514,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4294493,35.7347951],[139.4298489,35.734814],[139.4299348,35.7348602],[139.4295627,35.7349515],[139.4294557,35.7349007],[139.4294493,35.7347951]]]}},{"type":"Feature","properties":{"name":"南町運動広場多目的運動広場","distance_m":422,"area_m2":4686,"osm_type":"way","osm_id":556775131,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5264699,35.7406141],[139.5257566,35.7409729],[139.5254656,35.7405369],[139.5254351,35.7404491],[139.5261406,35.7401456],[139.5264699,35.7406141]]]}},{"type":"Feature","properties":{"name":"上仲原公園","distance_m":429,"area_m2":43090,"osm_type":"way","osm_id":165192451,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4427466,35.7426069],[139.4428341,35.7416082],[139.4429394,35.7404049],[139.4429535,35.7402443],[139.4429599,35.7401797],[139.4446208,35.7402245],[139.4446705,35.7402771],[139.4446645,35.7405096],[139.4445708,35.742414],[139.4445595,35.7426443],[139.4434439,35.7426213],[139.4428881,35.7426098],[139.4427466,35.7426069]]]}},{"type":"Feature","properties":{"name":"清原南公園","distance_m":438,"area_m2":1557,"osm_type":"way","osm_id":777899082,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4480591,35.7404405],[139.4488394,35.7404469],[139.4488403,35.7404346],[139.4487081,35.7403435],[139.4486112,35.7402576],[139.4485363,35.7401809],[139.4484918,35.7401499],[139.448369,35.7400934],[139.4483008,35.7401042],[139.4480591,35.7404405]]]}},{"type":"Feature","properties":{"name":"さくら公園","distance_m":442,"area_m2":525,"osm_type":"way","osm_id":564628995,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5129315,35.7124149],[139.5129346,35.7122069],[139.5131864,35.712211],[139.5131848,35.7124165],[139.5129315,35.7124149]]]}},{"type":"Feature","properties":{"name":"向原中央公園","distance_m":451,"area_m2":13441,"osm_type":"way","osm_id":726614762,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.4381725,35.7391623],[139.438227,35.7391991],[139.4382471,35.7393151],[139.4382469,35.7394344],[139.4382195,35.739566],[139.4370474,35.7394786],[139.4370708,35.7392775],[139.4363421,35.7392288],[139.4362723,35.7398736],[139.4360742,35.7398746],[139.4360746,35.7397974],[139.4359917,35.739796],[139.4359931,35.7396776],[139.4352392,35.7396633],[139.4352386,35.7394249],[139.4351242,35.7392206],[139.4352027,35.7391556],[139.435297,35.7390893],[139.4354047,35.7390274],[139.4354867,35.7390147],[139.4355688,35.7390173],[139.4361418,35.7390492],[139.4381725,35.7391623]]]}},{"type":"Feature","properties":{"name":"南町第6緑地","distance_m":455,"area_m2":566,"osm_type":"way","osm_id":556775132,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5262546,35.7398263],[139.5265128,35.7397042],[139.526403,35.7395524],[139.5261306,35.7396514],[139.5262546,35.7398263]]]}},{"type":"Feature","properties":{"name":"南町第4緑地","distance_m":457,"area_m2":165,"osm_type":"way","osm_id":556775133,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.526216,35.738381],[139.5262871,35.7383397],[139.5264274,35.7384717],[139.5263298,35.738513],[139.526216,35.738381]]]}},{"type":"Feature","properties":{"name":"滝山公園 テニスコート","distance_m":479,"area_m2":3720,"osm_type":"way","osm_id":542874116,"city":"東久留米市","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5104066,35.7463278],[139.5101626,35.7459447],[139.5109323,35.7456247],[139.5111656,35.74601],[139.5104066,35.7463278]]]}},{"type":"Feature","properties":{"name":"田無市民公園","distance_m":485,"area_m2":9142,"osm_type":"way","osm_id":550912804,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5300874,35.7198494],[139.530066,35.7196229],[139.5303516,35.7195984],[139.5307392,35.7195478],[139.5308746,35.7196425],[139.5309149,35.7200857],[139.5309605,35.7203492],[139.5312716,35.7211592],[139.5300311,35.7203394],[139.5300244,35.7202817],[139.5303248,35.7202621],[139.5302819,35.7198407],[139.5300874,35.7198494]]]}},{"type":"Feature","properties":{"name":"本町公園","distance_m":496,"area_m2":2127,"osm_type":"way","osm_id":562641453,"city":"","note":"市外・小平市境界から500m以内"},"geometry":{"type":"Polygon","coordinates":[[[139.5018337,35.7080864],[139.5017524,35.7075213],[139.5021093,35.7074574],[139.5021443,35.7076735],[139.5022019,35.7080299],[139.5018337,35.7080864]]]}}]}
\ No newline at end of file +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"すみれ公園","distance_m":4,"area_m2":1118,"osm_type":"way","osm_id":542686107,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.49887,35.7458891],[139.4987681,35.745741],[139.4984301,35.7459239],[139.4986072,35.7462004],[139.4988244,35.7460828],[139.4987949,35.7459696],[139.49887,35.7458891]]]}},{"type":"Feature","properties":{"name":"若葉緑地","distance_m":4,"area_m2":3395,"osm_type":"way","osm_id":591305877,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4348041,35.7255946],[139.4341234,35.7255259],[139.4341039,35.7250579],[139.4344686,35.7250685],[139.4344816,35.7252139],[139.4349962,35.725235],[139.4350255,35.7252958],[139.4348041,35.7255946]]]}},{"type":"Feature","properties":{"name":"あじさい公園","distance_m":5,"area_m2":2317,"osm_type":"way","osm_id":1521995587,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5066696,35.7418588],[139.5062131,35.7420472],[139.5059587,35.7416542],[139.5064167,35.7414578],[139.5066696,35.7418588]]]}},{"type":"Feature","properties":{"name":"富士見町1丁目第2仲よし広場","distance_m":5,"area_m2":1727,"osm_type":"way","osm_id":965224418,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4551618,35.7387211],[139.4563827,35.739249],[139.4557987,35.7392483],[139.4557816,35.7390298],[139.4551012,35.7389491],[139.4551099,35.7388152],[139.4551618,35.7387211]]]}},{"type":"Feature","properties":{"name":"青梅橋公園","distance_m":22,"area_m2":388,"osm_type":"way","osm_id":754232094,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4324421,35.7321674],[139.4327844,35.7321617],[139.4328439,35.7321892],[139.4328557,35.7321666],[139.4324314,35.7319798],[139.4324421,35.7321674]]]}},{"type":"Feature","properties":{"name":"桜が丘一丁目公園","distance_m":28,"area_m2":3067,"osm_type":"way","osm_id":754232096,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4306727,35.7316731],[139.4314557,35.7316397],[139.4323502,35.7321376],[139.4323444,35.732073],[139.4323392,35.7320163],[139.4323165,35.7319804],[139.430697,35.7312184],[139.4306426,35.7312446],[139.4306727,35.7316731]]]}},{"type":"Feature","properties":{"name":"青梅橋東公園","distance_m":28,"area_m2":148,"osm_type":"way","osm_id":754232095,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4329793,35.7324124],[139.4332489,35.7324111],[139.4329747,35.7323036],[139.4329793,35.7324124]]]}},{"type":"Feature","properties":{"name":"南台公園","distance_m":34,"area_m2":7777,"osm_type":"way","osm_id":591322526,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4586433,35.7420302],[139.4591958,35.7420476],[139.4592366,35.7420302],[139.4593017,35.7410812],[139.4591617,35.7410363],[139.4592431,35.7408433],[139.4588588,35.7406583],[139.4586439,35.740986],[139.4586433,35.7420302]]]}},{"type":"Feature","properties":{"name":"用水北緑地","distance_m":51,"area_m2":2199,"osm_type":"way","osm_id":693736574,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4426016,35.7373346],[139.4426667,35.7367311],[139.4427431,35.736731],[139.4425606,35.7382661],[139.4427828,35.7382803],[139.4427783,35.7383217],[139.4425624,35.7383098],[139.4424377,35.7392776],[139.4424678,35.7393173],[139.4423803,35.7393139],[139.4424744,35.7384284],[139.4423704,35.7384089],[139.4422463,35.7384134],[139.4422664,35.7382484],[139.4425005,35.7382633],[139.4426016,35.7373346]]]}},{"type":"Feature","properties":{"name":"窪東公園","distance_m":56,"area_m2":13725,"osm_type":"way","osm_id":727456979,"city":"","note":"市外・小平市境界から500m以内","name_source":"nearby_node"},"geometry":{"type":"Polygon","coordinates":[[[139.4654475,35.7163836],[139.4661449,35.7151053],[139.4655307,35.7147176],[139.4644605,35.7160374],[139.4654475,35.7163836]]]}},{"type":"Feature","properties":{"name":"富士見町2丁目第2仲よし広場","distance_m":64,"area_m2":76,"osm_type":"way","osm_id":965225385,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4548178,35.7393066],[139.454867,35.73931],[139.4549317,35.7392079],[139.4548302,35.7392088],[139.4548178,35.7393066]]]}},{"type":"Feature","properties":{"name":"萩山公園","distance_m":69,"area_m2":9856,"osm_type":"way","osm_id":1462182280,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.473544,35.743935],[139.4735794,35.7439436],[139.474243,35.7435759],[139.4737767,35.7426251],[139.4728893,35.743005],[139.4729362,35.7431142],[139.4730193,35.7432377],[139.473544,35.743935]]]}},{"type":"Feature","properties":{"name":"コウヤマキ公園","distance_m":76,"area_m2":234,"osm_type":"way","osm_id":817978652,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4935696,35.7084541],[139.4935476,35.708332],[139.4937318,35.7083087],[139.4937538,35.708417],[139.4937284,35.7084376],[139.4935696,35.7084541]]]}},{"type":"Feature","properties":{"name":"萩山四季の森公園","distance_m":76,"area_m2":9260,"osm_type":"way","osm_id":375322224,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4753838,35.7425524],[139.4754466,35.7428113],[139.4754044,35.7430008],[139.474911,35.7431718],[139.4742411,35.7435494],[139.4737718,35.7426025],[139.4742316,35.7424093],[139.4745409,35.7428655],[139.4753838,35.7425524]]]}},{"type":"Feature","properties":{"name":"北町公園","distance_m":77,"area_m2":4613,"osm_type":"way","osm_id":591177446,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4510419,35.721191],[139.4516704,35.721111],[139.4514732,35.7203655],[139.4510357,35.7204256],[139.4510542,35.7205457],[139.4508817,35.7205657],[139.4510419,35.721191]]]}},{"type":"Feature","properties":{"name":"芝久保三丁目ふれあい公園","distance_m":88,"area_m2":2550,"osm_type":"way","osm_id":1362560908,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5191406,35.7293977],[139.5191246,35.729057],[139.5189069,35.7290647],[139.5189106,35.7291702],[139.5187826,35.7291731],[139.5187789,35.7290676],[139.5183128,35.7290842],[139.5183419,35.7292174],[139.5183864,35.7294214],[139.5187654,35.7294077],[139.5187763,35.7294125],[139.518822,35.7294106],[139.5191406,35.7293977]]]}},{"type":"Feature","properties":{"name":"北町パンダ公園","distance_m":94,"area_m2":1675,"osm_type":"manual","osm_id":"hokucho-panda","city":"国分寺市","note":"市外・小平市境界から500m以内(公園の範囲は未取得。位置は国分寺市の公園情報による)","name_source":"manual"},"geometry":{"type":"Point","coordinates":[139.447225,35.721831]}},{"type":"Feature","properties":{"name":"萩山公園","distance_m":105,"area_m2":2063,"osm_type":"way","osm_id":558149589,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4753972,35.7422672],[139.4753234,35.7420898],[139.4752765,35.7419395],[139.4757566,35.7417251],[139.475935,35.74208],[139.4753972,35.7422672]]]}},{"type":"Feature","properties":{"name":"さつき公園","distance_m":113,"area_m2":1974,"osm_type":"way","osm_id":543026519,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5096639,35.7411551],[139.5094266,35.7407812],[139.5098426,35.7406074],[139.5100752,35.74099],[139.5096639,35.7411551]]]}},{"type":"Feature","properties":{"name":"桜町こぶし公園","distance_m":121,"area_m2":152,"osm_type":"way","osm_id":593771700,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5094476,35.7133134],[139.5096078,35.7133184],[139.5096113,35.7132257],[139.5094432,35.7132222],[139.5094476,35.7133134]]]}},{"type":"Feature","properties":{"name":"富士見町1丁目第3仲よし広場","distance_m":184,"area_m2":1041,"osm_type":"way","osm_id":668907354,"city":"東村山市","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4624137,35.7440997],[139.4619222,35.7440867],[139.4619188,35.7440971],[139.4619148,35.7442438],[139.4619138,35.7442969],[139.4624022,35.7443098],[139.4624137,35.7440997]]]}},{"type":"Feature","properties":{"name":"ひょうたん公園","distance_m":214,"area_m2":3515,"osm_type":"way","osm_id":563674138,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4974454,35.7085293],[139.497419,35.708478],[139.4973493,35.7082461],[139.4973206,35.708145],[139.4973091,35.7081114],[139.4972735,35.7079576],[139.4972279,35.707808],[139.4975624,35.7077753],[139.4977517,35.7077645],[139.4978495,35.7085013],[139.4976645,35.7085113],[139.4974454,35.7085293]]]}},{"type":"Feature","properties":{"name":"東大和南公園","distance_m":218,"area_m2":100408,"osm_type":"way","osm_id":165197251,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4209315,35.7317592],[139.4210368,35.7335495],[139.4228427,35.7334823],[139.4251194,35.7333976],[139.4252208,35.7346496],[139.4239703,35.7347091],[139.4222181,35.734803],[139.4222618,35.735362],[139.4224708,35.7353468],[139.4225686,35.7366797],[139.4227799,35.7369273],[139.4214214,35.7372722],[139.4213175,35.7372214],[139.4208196,35.7356856],[139.4207725,35.735498],[139.4207539,35.7353756],[139.4205485,35.7319114],[139.4206145,35.731831],[139.4209315,35.7317592]]]}},{"type":"Feature","properties":{"name":"伊豆殿公園","distance_m":219,"area_m2":3314,"osm_type":"way","osm_id":559748180,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4712554,35.7457789],[139.470588,35.745467],[139.4706381,35.7453434],[139.4706724,35.7451599],[139.471454,35.745248],[139.4712554,35.7457789]]]}},{"type":"Feature","properties":{"name":"上新堀公園","distance_m":229,"area_m2":1400,"osm_type":"way","osm_id":249323019,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.446034,35.7371745],[139.4454807,35.7371991],[139.4454475,35.7372268],[139.4454378,35.7374589],[139.4457612,35.7373604],[139.4458173,35.7373613],[139.4458653,35.737449],[139.4460174,35.737471],[139.4460316,35.7374293],[139.4460628,35.7371958],[139.446034,35.7371745]]]}},{"type":"Feature","properties":{"name":"東村山中央公園","distance_m":253,"area_m2":110590,"osm_type":"relation","osm_id":905357,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4630754,35.7451259],[139.4631504,35.7451587],[139.4631614,35.7452243],[139.4632129,35.745581],[139.4633325,35.7461754],[139.4623581,35.7466046],[139.4626251,35.7470134],[139.4559699,35.7499453],[139.4550786,35.7486398],[139.4555606,35.7484224],[139.4557073,35.7483551],[139.4561157,35.7481626],[139.4565651,35.7481079],[139.4566696,35.748064],[139.4570957,35.7478672],[139.4576711,35.7476035],[139.457848,35.747517],[139.4580643,35.7478251],[139.4594136,35.747169],[139.4591983,35.7468401],[139.4595307,35.7466209],[139.459648,35.7465698],[139.4604244,35.7462363],[139.4612164,35.7459074],[139.4630754,35.7451259]],[[139.4556474,35.7486024],[139.455629,35.7486708],[139.4556505,35.7486844],[139.4556765,35.7487043],[139.4556934,35.7487317],[139.4557041,35.7487553],[139.4557072,35.7487889],[139.4556964,35.7488125],[139.4556811,35.7488287],[139.4556627,35.7488399],[139.4557684,35.7489556],[139.4558619,35.7490302],[139.4559646,35.7492602],[139.4559891,35.749356],[139.4560688,35.7494145],[139.4561393,35.7494493],[139.4562036,35.749458],[139.456337,35.7494455],[139.4564442,35.7494256],[139.4565346,35.7493921],[139.4566235,35.7493386],[139.4567001,35.7492777],[139.4567951,35.7491682],[139.4568626,35.7490886],[139.4569131,35.7489817],[139.4569392,35.748907],[139.4569422,35.7488138],[139.456953,35.7487765],[139.4569622,35.7487454],[139.4569622,35.7487193],[139.4569407,35.748677],[139.456884,35.7486235],[139.4568334,35.7485601],[139.4567951,35.7485165],[139.4567614,35.7485029],[139.4566771,35.7484842],[139.4565668,35.7484432],[139.4564641,35.7484034],[139.4563906,35.7483822],[139.4563109,35.7483685],[139.4562772,35.7483797],[139.4562036,35.7484171],[139.4561209,35.7484506],[139.4560688,35.748483],[139.4559707,35.7485277],[139.4559339,35.748534],[139.4558895,35.7485364],[139.4558604,35.748529],[139.4558267,35.7485203],[139.455796,35.7485203],[139.4557639,35.748529],[139.4557317,35.7485377],[139.4556842,35.7485588],[139.4556597,35.7485812],[139.4556474,35.7486024]]]}},{"type":"Feature","properties":{"name":"向原南公園","distance_m":270,"area_m2":819,"osm_type":"way","osm_id":726612491,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4359293,35.7366135],[139.4365248,35.736532],[139.4365318,35.7364941],[139.4359702,35.7363718],[139.4359293,35.7366135]]]}},{"type":"Feature","properties":{"name":"ハンカチの木公園","distance_m":278,"area_m2":328,"osm_type":"way","osm_id":754224515,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4301443,35.7338879],[139.4302737,35.7338835],[139.4302597,35.7336183],[139.4301318,35.7336491],[139.4301443,35.7338879]]]}},{"type":"Feature","properties":{"name":"ケヤキ広場","distance_m":291,"area_m2":4258,"osm_type":"way","osm_id":568392686,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4909904,35.7057],[139.4908229,35.7047654],[139.4912664,35.704713],[139.4914339,35.7056476],[139.4909904,35.7057]]]}},{"type":"Feature","properties":{"name":"こもれび公園","distance_m":299,"area_m2":101,"osm_type":"way","osm_id":757512355,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5082556,35.709857],[139.5082561,35.7098051],[139.5084498,35.7098065],[139.5084493,35.7098583],[139.5082556,35.709857]]]}},{"type":"Feature","properties":{"name":"いけとおがわ プレイパーク","distance_m":307,"area_m2":4978,"osm_type":"relation","osm_id":8098101,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4947101,35.7065445],[139.4945763,35.7057813],[139.4952471,35.7057015],[139.4953765,35.7064665],[139.4947101,35.7065445]],[[139.4950893,35.705819],[139.4951094,35.7057874],[139.4951631,35.7057743],[139.4952167,35.70577],[139.4952529,35.7058048],[139.4952489,35.7058429],[139.4952167,35.7058571],[139.4951604,35.705856],[139.4951135,35.7058549],[139.495088,35.7058407],[139.4950772,35.7058603],[139.4950196,35.7058701],[139.4949861,35.7058701],[139.4949364,35.705844],[139.4949029,35.70582],[139.4949203,35.7057934],[139.4949485,35.705771],[139.4950088,35.7057558],[139.4950759,35.7057917],[139.4950893,35.705819]]]}},{"type":"Feature","properties":{"name":"上水公園","distance_m":308,"area_m2":19730,"osm_type":"way","osm_id":792340208,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5059775,35.7094834],[139.5058815,35.7086994],[139.5058559,35.7083302],[139.5058523,35.7083008],[139.5057683,35.708308],[139.5044173,35.708411],[139.504473,35.7087899],[139.5044731,35.7089689],[139.5044598,35.7089901],[139.5041879,35.7090139],[139.5042312,35.7093208],[139.5041473,35.7093724],[139.5041645,35.7094737],[139.5042089,35.7097347],[139.5047365,35.7096957],[139.5047224,35.7095896],[139.5059775,35.7094834]]]}},{"type":"Feature","properties":{"name":"向原公園","distance_m":324,"area_m2":1214,"osm_type":"way","osm_id":165192452,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4399088,35.7391889],[139.4399439,35.7388288],[139.4397553,35.7388103],[139.4397604,35.7387533],[139.4402103,35.7387855],[139.4401618,35.7392089],[139.4399088,35.7391889]]]}},{"type":"Feature","properties":{"name":"桜町公園","distance_m":350,"area_m2":921,"osm_type":"way","osm_id":742459128,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5092376,35.7095485],[139.5095374,35.7095478],[139.5095394,35.7092411],[139.509242,35.7092419],[139.5092376,35.7095485]]]}},{"type":"Feature","properties":{"name":"桜町大尽の坂公園","distance_m":399,"area_m2":52,"osm_type":"way","osm_id":742758113,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5108746,35.7097898],[139.5109289,35.7097898],[139.51093,35.7096954],[139.5108746,35.7096959],[139.5108746,35.7097898]]]}},{"type":"Feature","properties":{"name":"幸五公園","distance_m":406,"area_m2":3328,"osm_type":"way","osm_id":943828613,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4193357,35.7277353],[139.4191898,35.7269514],[139.4195889,35.7269375],[139.4197434,35.7278084],[139.4195289,35.7277771],[139.4193357,35.7277353]]]}},{"type":"Feature","properties":{"name":"滝山公園","distance_m":407,"area_m2":24024,"osm_type":"way","osm_id":542874443,"city":"東久留米市","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5097602,35.7465694],[139.5089221,35.745216],[139.510346,35.7446456],[139.5108954,35.7455637],[139.5111656,35.74601],[139.5102752,35.7463629],[139.5097602,35.7465694]]]}},{"type":"Feature","properties":{"name":"桜みらい公園","distance_m":412,"area_m2":485,"osm_type":"way","osm_id":754224514,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4294493,35.7347951],[139.4298489,35.734814],[139.4299348,35.7348602],[139.4295627,35.7349515],[139.4294557,35.7349007],[139.4294493,35.7347951]]]}},{"type":"Feature","properties":{"name":"南町運動広場多目的運動広場","distance_m":422,"area_m2":4686,"osm_type":"way","osm_id":556775131,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5264699,35.7406141],[139.5257566,35.7409729],[139.5254656,35.7405369],[139.5254351,35.7404491],[139.5261406,35.7401456],[139.5264699,35.7406141]]]}},{"type":"Feature","properties":{"name":"上仲原公園","distance_m":429,"area_m2":43090,"osm_type":"way","osm_id":165192451,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4427466,35.7426069],[139.4428341,35.7416082],[139.4429394,35.7404049],[139.4429535,35.7402443],[139.4429599,35.7401797],[139.4446208,35.7402245],[139.4446705,35.7402771],[139.4446645,35.7405096],[139.4445708,35.742414],[139.4445595,35.7426443],[139.4434439,35.7426213],[139.4428881,35.7426098],[139.4427466,35.7426069]]]}},{"type":"Feature","properties":{"name":"清原南公園","distance_m":438,"area_m2":1557,"osm_type":"way","osm_id":777899082,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4480591,35.7404405],[139.4488394,35.7404469],[139.4488403,35.7404346],[139.4487081,35.7403435],[139.4486112,35.7402576],[139.4485363,35.7401809],[139.4484918,35.7401499],[139.448369,35.7400934],[139.4483008,35.7401042],[139.4480591,35.7404405]]]}},{"type":"Feature","properties":{"name":"さくら公園","distance_m":442,"area_m2":525,"osm_type":"way","osm_id":564628995,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5129315,35.7124149],[139.5129346,35.7122069],[139.5131864,35.712211],[139.5131848,35.7124165],[139.5129315,35.7124149]]]}},{"type":"Feature","properties":{"name":"向原中央公園","distance_m":451,"area_m2":13441,"osm_type":"way","osm_id":726614762,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.4381725,35.7391623],[139.438227,35.7391991],[139.4382471,35.7393151],[139.4382469,35.7394344],[139.4382195,35.739566],[139.4370474,35.7394786],[139.4370708,35.7392775],[139.4363421,35.7392288],[139.4362723,35.7398736],[139.4360742,35.7398746],[139.4360746,35.7397974],[139.4359917,35.739796],[139.4359931,35.7396776],[139.4352392,35.7396633],[139.4352386,35.7394249],[139.4351242,35.7392206],[139.4352027,35.7391556],[139.435297,35.7390893],[139.4354047,35.7390274],[139.4354867,35.7390147],[139.4355688,35.7390173],[139.4361418,35.7390492],[139.4381725,35.7391623]]]}},{"type":"Feature","properties":{"name":"南町第6緑地","distance_m":455,"area_m2":566,"osm_type":"way","osm_id":556775132,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5262546,35.7398263],[139.5265128,35.7397042],[139.526403,35.7395524],[139.5261306,35.7396514],[139.5262546,35.7398263]]]}},{"type":"Feature","properties":{"name":"南町第4緑地","distance_m":457,"area_m2":165,"osm_type":"way","osm_id":556775133,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.526216,35.738381],[139.5262871,35.7383397],[139.5264274,35.7384717],[139.5263298,35.738513],[139.526216,35.738381]]]}},{"type":"Feature","properties":{"name":"滝山公園 テニスコート","distance_m":479,"area_m2":3720,"osm_type":"way","osm_id":542874116,"city":"東久留米市","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5104066,35.7463278],[139.5101626,35.7459447],[139.5109323,35.7456247],[139.5111656,35.74601],[139.5104066,35.7463278]]]}},{"type":"Feature","properties":{"name":"田無市民公園","distance_m":485,"area_m2":9142,"osm_type":"way","osm_id":550912804,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5300874,35.7198494],[139.530066,35.7196229],[139.5303516,35.7195984],[139.5307392,35.7195478],[139.5308746,35.7196425],[139.5309149,35.7200857],[139.5309605,35.7203492],[139.5312716,35.7211592],[139.5300311,35.7203394],[139.5300244,35.7202817],[139.5303248,35.7202621],[139.5302819,35.7198407],[139.5300874,35.7198494]]]}},{"type":"Feature","properties":{"name":"本町公園","distance_m":496,"area_m2":2127,"osm_type":"way","osm_id":562641453,"city":"","note":"市外・小平市境界から500m以内","name_source":"osm"},"geometry":{"type":"Polygon","coordinates":[[[139.5018337,35.7080864],[139.5017524,35.7075213],[139.5021093,35.7074574],[139.5021443,35.7076735],[139.5022019,35.7080299],[139.5018337,35.7080864]]]}}]} diff --git a/src/data/park-icons.ts b/src/data/park-icons.ts new file mode 100644 index 0000000..620df65 --- /dev/null +++ b/src/data/park-icons.ts @@ -0,0 +1,15 @@ +// 遊具アイコン(SVGRepo の素材をインライン化) +export type ParkIcon = { title: string; svg: string }; +export const PARK_ICONS: Record<string, ParkIcon> = { + airplane: { title: "ヒコーキジャングル", svg: "<svg viewBox=\"0 0 511.999 511.999\" aria-hidden=\"true\" fill=\"currentColor\"><g>\n\t<g>\n\t\t<path d=\"M492.474,359.413L380.416,247.356c12.062-9.959,11.958-9.813,12.861-10.718c19.26-19.259,27.146-45.418,23.582-70.739\n\t\t\tl11.785-11.786l52.041,52.039c6.511,6.511,17.067,6.511,23.578,0c6.511-6.51,6.511-17.067,0-23.578l-54.069-54.068\n\t\t\tc8.855-18.589,5.609-41.549-9.76-56.919c-15.714-15.714-38.763-18.485-56.94-9.782L329.447,7.759c-6.51-6.51-17.067-6.51-23.578,0\n\t\t\tc-6.511,6.51-6.511,17.067,0,23.578l52.04,52.041l-11.787,11.786c-25.318-3.561-51.47,4.314-70.737,23.582\n\t\t\tc-0.894,0.894-0.757,0.799-10.717,12.862c-0.993-0.994-109.805-109.806-112.059-112.061C126.547-6.513,84.361-6.519,58.295,19.549\n\t\t\tc-26.065,26.063-26.067,68.247,0,94.313c5.315,5.315,115.771,115.772,121.066,121.067l-55.682,67.439\n\t\t\tc-31.857-15.372-70.047-9.028-95.196,16.121L4.906,342.068c-6.511,6.511-6.511,17.067,0,23.578\n\t\t\tc106.734,106.732,37.325,37.326,141.471,141.47c6.511,6.511,17.067,6.511,23.578,0l23.578-23.578\n\t\t\tc25.114-25.113,31.512-63.294,16.122-95.195l67.439-55.682c20.182,20.182,103.593,103.594,121.065,121.066\n\t\t\tc26.003,26.003,68.312,26.005,94.314,0C518.477,427.724,518.477,385.416,492.474,359.413z M393.277,95.167\n\t\t\tc6.517-6.517,17.062-6.517,23.578,0c6.501,6.501,6.501,17.078,0,23.579l-12.966,12.966c-3.064-4.615-6.607-8.96-10.613-12.966\n\t\t\ts-8.352-7.549-12.967-10.613L393.277,95.167z M81.873,43.127c13-13.001,34.155-13.002,47.158-0.001L81.873,90.284\n\t\t\tC68.84,77.25,68.841,56.162,81.873,43.127z M105.452,113.862l47.158-47.158l23.578,23.578l-47.158,47.158L105.452,113.862z\n\t\t\t M152.608,161.019l47.158-47.157l43.575,43.576l-42.654,51.66L152.608,161.019z M62.672,376.257l-22.399-22.399l11.789-11.791\n\t\t\tc13.101-13.1,32.219-17.515,49.282-12.648L62.672,376.257z M169.956,459.961l-11.79,11.789l-22.399-22.399l46.838-38.671\n\t\t\tC187.472,427.743,183.056,446.859,169.956,459.961z M112.082,425.664l-25.724-25.724c8.147-9.867,204.411-247.571,213.132-258.134\n\t\t\tc19.574-19.021,50.827-18.863,70.209,0.518c19.379,19.379,19.541,50.632,0.519,70.209\n\t\t\tC359.671,221.241,121.995,417.479,112.082,425.664z M351.004,359.415l-48.078-48.079l51.66-42.654l43.575,43.575L351.004,359.415z\n\t\t\t M374.582,382.994l47.158-47.158l23.579,23.579l-47.158,47.157L374.582,382.994z M468.896,430.15\n\t\t\tc-13.001,12.998-34.155,13.001-47.158,0l47.158-47.157C481.896,395.995,481.896,417.149,468.896,430.15z\"/>\n\t</g>\n</g>\n<g>\n\t<g>\n\t\t<path d=\"M322.541,189.481c-6.51-6.51-17.067-6.51-23.578,0l-70.736,70.736c-6.511,6.51-6.511,17.067,0,23.578\n\t\t\tc3.256,3.256,7.523,4.884,11.79,4.884c4.267,0,8.534-1.628,11.789-4.884l70.736-70.736\n\t\t\tC329.053,206.549,329.053,195.992,322.541,189.481z\"/>\n\t</g>\n</g></svg>" }, + climbing: { title: "ジャングルジム", svg: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" fill=\"currentColor\"><defs><style>.climb-cls-1{fill:none;stroke:currentColor;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"0.55\" x2=\"1.5\" y2=\"23.45\"/><line class=\"climb-cls-1\" x1=\"12\" y1=\"0.55\" x2=\"12\" y2=\"23.45\"/><line class=\"climb-cls-1\" x1=\"22.5\" y1=\"6.27\" x2=\"22.5\" y2=\"23.45\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"2.45\" x2=\"12\" y2=\"2.45\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"6.27\" x2=\"12\" y2=\"6.27\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"10.09\" x2=\"12\" y2=\"10.09\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"13.91\" x2=\"12\" y2=\"13.91\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"17.73\" x2=\"12\" y2=\"17.73\"/><line class=\"climb-cls-1\" x1=\"1.5\" y1=\"21.55\" x2=\"12\" y2=\"21.55\"/><line class=\"climb-cls-1\" x1=\"12\" y1=\"8.18\" x2=\"22.5\" y2=\"8.18\"/><line class=\"climb-cls-1\" x1=\"12\" y1=\"12\" x2=\"22.5\" y2=\"12\"/><line class=\"climb-cls-1\" x1=\"12\" y1=\"15.82\" x2=\"22.5\" y2=\"15.82\"/><line class=\"climb-cls-1\" x1=\"12\" y1=\"19.64\" x2=\"22.5\" y2=\"19.64\"/></svg>" }, + swing: { title: "ブランコ", svg: "<svg viewBox=\"0 0 430 430\" aria-hidden=\"true\" fill=\"currentColor\"><path d=\"M412,410h-16V54.09C396,31.984,378.016,14,355.91,14h-67.432v-4c0-5.523-4.478-10-10-10s-10,4.477-10,10v4H161.521v-4 c0-5.523-4.477-10-10-10s-10,4.477-10,10v4H74.09C51.984,14,34,31.984,34,54.09V410H18c-5.523,0-10,4.477-10,10s4.477,10,10,10h52 c5.523,0,10-4.477,10-10s-4.477-10-10-10H54V54.09C54,43.012,63.012,34,74.09,34h67.432v314c0,5.523,4.477,10,10,10h126.958 c5.522,0,10-4.477,10-10V34h67.432C366.988,34,376,43.012,376,54.09V410h-16c-5.522,0-10,4.477-10,10s4.478,10,10,10h52 c5.522,0,10-4.477,10-10S417.523,410,412,410z M161.521,338v-13.383h106.958V338H161.521z M268.479,304.617H161.521V34h106.958 V304.617z\"/></svg>" }, + teeter: { title: "シーソー", svg: "<svg viewBox=\"0 0 493.927 493.927\" aria-hidden=\"true\" fill=\"currentColor\"><g>\n\t<path d=\"M38.758,234.499c16.794-4.117,27.073-21.056,22.956-37.851c-4.108-16.801-21.048-27.074-37.842-22.965\n\t\tC7.072,177.801-3.209,194.74,0.907,211.534C5.025,228.337,21.964,238.607,38.758,234.499z\"/>\n\t<path d=\"M400.077,146.024c16.803-4.109,27.073-21.057,22.957-37.842c-4.11-16.802-21.049-27.074-37.835-22.965\n\t\tc-16.809,4.117-27.073,21.048-22.964,37.85C366.351,139.861,383.291,150.141,400.077,146.024z\"/>\n\t<path d=\"M493.565,256.893c-1.646-6.725-8.503-10.825-15.138-9.188l-19.46,4.77c0.327-1.818,0.205-3.71-0.098-5.618\n\t\tc-0.978-4.075-18.383-76.988-19.817-80.91c-2.593-11.021-16.6-18.929-29.381-15.823c-12.457,3.057-20.307,14.942-18.773,27.366\n\t\tl-38.96,37.369c-6.651,6.384-6.872,16.949-0.489,23.609c1.06,1.1,2.365,1.736,3.62,2.478c-5.25,2.005-8.421,7.484-7.044,13.084\n\t\tl5.91,24.155l-84.064,20.576c-5.333-5.698-12.865-9.31-21.285-9.31c-13.41,0-24.58,9.074-28.018,21.384l-79.826,19.549\n\t\tl-5.919-24.163c-1.36-5.528-6.563-8.951-12.064-8.365c0.725-1.199,1.435-2.421,1.851-3.831c2.584-8.853-2.495-18.122-11.34-20.714\n\t\tl-51.831-15.146c-4.387-11.732-16.786-18.677-29.3-15.596c-12.627,3.09-21.528,16.532-18.749,27.571\n\t\tc0.538,3.994,19.809,80.942,19.809,80.942c0.611,1.826,1.37,3.563,2.504,5.014l-19.445,4.762\n\t\tc-6.716,1.646-10.833,8.421-9.185,15.138c1.654,6.767,8.486,10.825,15.138,9.187l64.491-15.79l-3.146,19.916\n\t\tc-1.435,9.114,4.776,17.657,13.884,19.1c9.179,1.434,17.673-4.827,19.1-13.882l5.331-33.742l102.953-25.207\n\t\tc2.202,3.082,4.974,5.698,8.169,7.728l-24.961,39.448c-3.408,5.396-3.62,12.211-0.54,17.804c3.074,5.585,8.952,9.058,15.327,9.058\n\t\th51.538c6.376,0,12.253-3.473,15.327-9.058c3.081-5.593,2.869-12.408-0.539-17.804l-24.961-39.448\n\t\tc7.076-4.493,12.008-11.927,13.215-20.601l107.331-26.273l20.307,27.456c5.486,7.409,15.938,8.967,23.355,3.498\n\t\tc7.411-5.486,8.975-15.946,3.497-23.357l-11.992-16.206l64.483-15.791C491.096,270.384,495.211,263.609,493.565,256.893z\n\t\t M399.824,215.202l6.375,26.013l-28.377,6.954c-2.666,0.653-5.07,1.948-7.061,3.71l-0.789-3.227\n\t\tc-0.563-2.308-1.932-4.118-3.58-5.593c3.139-0.563,6.196-1.729,8.667-4.101L399.824,215.202z M108.589,306.043\n\t\tc1.647,0,3.245-0.311,4.784-0.775c-0.701,1.988-1.043,4.124-0.496,6.334l0.79,3.219c-2.576-0.636-5.307-0.676-7.972-0.023\n\t\tl-28.386,6.946l-6.368-26.014C74.839,296.87,104.856,306.043,108.589,306.043z\"/>\n</g></svg>" }, + spring: { title: "スプリング遊具", svg: "<svg viewBox=\"0 0 512 512\" aria-hidden=\"true\" fill=\"currentColor\"><rect x=\"99.667\" y=\"143.442\" style=\"fill:#FFE97C;\" width=\"307.853\" height=\"106.786\"/>\n<g>\n\t<polygon style=\"fill:#FFF6CC;\" points=\"99.669,238.684 99.669,154.986 10.199,173.265 10.199,220.405 \t\"/>\n\t<path style=\"fill:#FFF6CC;\" d=\"M132.379,94.377h-32.71l25.013,49.064h56.761l0,0C181.442,116.344,159.476,94.377,132.379,94.377z\"\n\t\t/>\n\t<path style=\"fill:#FFF6CC;\" d=\"M132.379,299.292h-32.71l25.013-49.064h56.761l0,0C181.442,277.326,159.476,299.292,132.379,299.292\n\t\tz\"/>\n</g>\n<g>\n\t<polygon style=\"fill:#FF9ECA;\" points=\"501.801,196.834 407.521,152.864 407.521,240.805 \t\"/>\n\t<path style=\"fill:#FF9ECA;\" d=\"M297.38,170.379h-87.571c-14.877,0-26.937-12.061-26.937-26.937l0,0h141.446l0,0\n\t\tC324.317,158.318,312.258,170.379,297.38,170.379z\"/>\n</g>\n<path style=\"fill:#FFE97C;\" d=\"M301.697,422.433h-103.9c-7.97,0-14.431-6.461-14.431-14.431l0,0c0-7.97,6.461-14.431,14.431-14.431\n\th103.9c7.97,0,14.431,6.461,14.431,14.431l0,0C316.127,415.971,309.666,422.433,301.697,422.433z\"/>\n<g>\n\t<path style=\"fill:#4D1E1E;\" d=\"M176.632,186.155h-5.773c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199\n\t\th5.773c5.633,0,10.199-4.566,10.199-10.199C186.831,190.721,182.265,186.155,176.632,186.155z\"/>\n\t<path style=\"fill:#4D1E1E;\" d=\"M190.746,133.242c-4.851-27.833-29.168-49.064-58.367-49.064H99.67\n\t\tc-3.548,0-6.843,1.845-8.697,4.871s-2.001,6.799-0.39,9.961l17.452,34.233H99.67c-5.633,0-10.199,4.566-10.199,10.199v3.219\n\t\tL8.158,163.272C3.41,164.242,0,168.418,0,173.265v47.14c0,4.846,3.41,9.023,8.158,9.993l81.312,16.611v3.219\n\t\tc0,5.633,4.566,10.199,10.199,10.199h8.365L90.583,294.66c-1.611,3.162-1.465,6.934,0.39,9.961\n\t\tc1.854,3.026,5.148,4.871,8.697,4.871h32.709c29.199,0,53.516-21.232,58.367-49.064h52.284c0.056,0.027,0.107,0.058,0.164,0.084\n\t\tl9.186,4.083l-34.814,5.247c-6.176,0.931-10.66,5.94-10.904,12.181c-0.244,6.242,3.835,11.585,9.92,12.997\n\t\tc0.058,0.013,0.116,0.027,0.174,0.039l25.064,5.351l-25.077,4.551c-0.05,0.009-0.101,0.019-0.151,0.029\n\t\tc-6.091,1.2-10.346,6.375-10.346,12.583c0,6.208,4.253,11.383,10.345,12.585c0.05,0.01,0.102,0.019,0.152,0.029l25.098,4.555\n\t\tl-25.862,5.526c-0.057,0.012-0.115,0.025-0.172,0.039c-6.014,1.395-10.096,6.679-9.924,12.852\n\t\tc0.172,6.172,4.541,11.221,10.623,12.279l43.458,7.559l-17.067,10.38h-45.099c-13.581,0-24.63,11.049-24.63,24.629\n\t\tc0,13.581,11.049,24.63,24.63,24.63h103.9c13.581,0,24.63-11.049,24.63-24.63c0-13.58-11.049-24.629-24.63-24.629h-19.548\n\t\tl5.636-3.428c4.611-2.804,6.943-8.042,5.938-13.345c-1.004-5.303-5.088-9.326-10.404-10.249l-24.81-4.314l25.126-5.369\n\t\tc0.057-0.012,0.115-0.026,0.172-0.039c5.947-1.379,10.029-6.605,9.927-12.708c-0.102-6.103-4.356-11.191-10.345-12.371\n\t\tc-0.05-0.01-0.1-0.019-0.15-0.029l-21.766-3.951l21.766-3.95c0.05-0.009,0.101-0.019,0.15-0.029\n\t\tc5.99-1.18,10.243-6.267,10.345-12.371c0.102-6.103-3.981-11.329-9.927-12.708c-0.058-0.013-0.116-0.027-0.174-0.039l-21.866-4.667\n\t\tl19.313-2.911c5.689-0.857,10.029-5.264,10.799-10.965c0.472-3.494-0.482-6.871-2.499-9.503h118.139\n\t\tc5.633,0,10.199-4.566,10.199-10.199v-2.925l88.391-41.224c3.592-1.676,5.888-5.28,5.888-9.244s-2.296-7.568-5.888-9.244\n\t\tl-88.391-41.224v-2.925c0-5.633-4.566-10.199-10.199-10.199h-39.827V99.767h2.307c5.633,0,10.199-4.566,10.199-10.199\n\t\tc0-5.633-4.566-10.199-10.199-10.199h-26.937c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h4.232\n\t\tv33.476H190.746z M305.928,408.002c0,2.334-1.898,4.232-4.232,4.232H197.797c-2.334,0-4.232-1.898-4.232-4.232\n\t\tc0-2.334,1.898-4.231,4.232-4.231h103.9C304.03,403.771,305.928,405.669,305.928,408.002z M109.868,206.553h28.282\n\t\tc5.633,0,10.199-4.566,10.199-10.199c0-5.633-4.566-10.199-10.199-10.199h-28.282v-32.514h64.241\n\t\tc4.445,15.531,18.761,26.937,35.7,26.937h87.571c16.94,0,31.256-11.405,35.7-26.937h64.241v86.387H109.868V206.553z\n\t\t M310.644,153.641c-3.063,3.974-7.87,6.539-13.263,6.539H209.81c-5.394,0-10.2-2.565-13.263-6.539H310.644z M116.316,104.577\n\t\th16.062c17.903,0,33.017,12.167,37.508,28.666H130.93L116.316,104.577z M89.469,167.479v18.675H20.398v-4.563L89.469,167.479z\n\t\t M20.398,212.078v-5.525h69.071v19.637L20.398,212.078z M132.378,289.092h-16.062l14.614-28.666h38.955\n\t\tC165.394,276.927,150.28,289.092,132.378,289.092z M417.721,224.795v-55.92l59.951,27.96L417.721,224.795z\"/>\n</g></svg>" }, + slide: { title: "滑り台", svg: "<svg viewBox=\"0 0 32 32\" aria-hidden=\"true\" fill=\"currentColor\"><style type=\"text/css\">\n\t.slide-bentblocks_een{fill:#0B1719;}\n</style>\n<path class=\"slide-bentblocks_een\" d=\"M12,4H4v20h2v-2h4v2h2v-8.114L20,28h8L12,4z M6,6.801L6.792,8H6V6.801z M6,10h2.113l1.321,2H6V10z\n\t M10,20H6v-2h4V20z M10,16H6v-2h4V16z M7.868,6h3.062l13.333,20h-3.187L7.868,6z\"/></svg>" }, + flyingfox: { title: "ロープウェイ", svg: "<svg viewBox=\"0 0 512 512\" aria-hidden=\"true\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M46.244 18l170.172 72.932 7.586 18.982L242.18 155.4l26.775 11.487-4.412 49.513c3.896-2.256 7.913-4.446 12.102-6.49l.685-.334.733-.215a38.637 38.637 0 0 1 5.28-1.15l3.018-33.858 21.488 9.216 64.54-25.793L494 209.895v-19.583L352.756 129.78c10.44-5.566 17.607-16.574 17.607-29.145 0-18.12-14.88-33-33-33-8.92 0-17.05 3.61-23.01 9.433 0-.145.01-.288.01-.433 0-18.12-14.88-33-33-33-18.12 0-33 14.88-33 33 0 3.096.443 6.093 1.254 8.94L91.937 18H46.245zm235.12 43.635c8.39 0 15 6.61 15 15s-6.61 15-15 15c-8.392 0-15-6.61-15-15s6.608-15 15-15zm56 24c8.39 0 15 6.61 15 15s-6.61 15-15 15c-8.392 0-15-6.61-15-15s6.608-15 15-15zm-32.99 14.566c0 .146-.01.29-.01.435 0 3.096.442 6.093 1.253 8.94l-8.86-3.796a33.383 33.383 0 0 0 7.618-5.58zm-55.595 23.212l77.7 33.328-18.388 7.348-51.965-22.29-7.348-18.386zm-88.813 44.465c-1.564.03-3.078.278-4.53.736-5.804 1.835-10.49 6.994-13.12 15.508-2.627 8.515-2.697 19.943.974 31.56 3.672 11.617 10.297 20.93 17.34 26.386 7.045 5.457 13.845 6.983 19.65 5.15 5.804-1.836 10.49-6.993 13.118-15.507 2.63-8.515 2.7-19.943-.972-31.56-3.672-11.617-10.296-20.927-17.34-26.384-5.283-4.093-10.428-5.977-15.12-5.89zm128.265 57.992c-1.4.004-2.946.266-4.543.68-16.685 8.293-31.973 20.153-49.28 29.423-17.458 9.35-37.674 15.72-61.853 10.865-11.962-1.176-16.464 4.17-17.71 9.943-1.218 5.632.82 12.564 11.155 16.76 55.347 5.065 101.237-31.265 126.685-52.144.002 0 0-.002.002-.003 4.028-3.38 5.42-6.192 5.8-7.913.38-1.72.105-2.63-.76-3.83-1.296-1.8-4.77-3.796-9.498-3.78zm-115.675 40.968c.095.01.18.01.277.02l-.883-.137c.204.043.404.077.607.118zm104.746 9.178c-5.806 4.074-12.11 8.176-18.862 12.086 4.013 9.28 7.354 19.894 9.945 31.99 1.61.113 3.294.26 4.955.398l3.963-44.474zm-34.82 20.457c-20.36 9.566-43.922 16.344-69.917 15.54 2.902 4.44 6.194 8.82 9.725 13.038 5.8 6.925 12.3 13.454 18.59 19.218 7.647-1.047 19.705-4.422 30-9.143 6.385-2.927 12.267-6.335 16.405-9.57a29.504 29.504 0 0 0 3.274-2.98c-2.216-10.03-4.95-18.662-8.078-26.102zm17.345 42.033c-.493.404-.95.832-1.457 1.228-5.738 4.487-12.654 8.39-19.985 11.752-6.59 3.022-13.37 5.63-19.96 7.563 5.682 4.404 9.88 7.312 11.093 8.052 8.19 5.004 20.968 8.01 34.627 9.54 3.935-12.394.912-24.61-4.317-38.136zm19.342.512c4.085 12.243 6.466 25.26 3.312 38.824 2.596.06 5.167.077 7.684.05 21.71-.245 39.418-2.925 39.418-2.925l14.576-2.213-4.693 13.976c-7.496 22.323-10.164 38.087-17.156 59.038l55.614 24.933 9.418-18.973-39.594-16.97 4.678-8.667c3.575-6.625 9.348-20.202 13.79-33.983 3.962-12.288 6.61-24.973 6.827-32.623-.047-.03-.026-.033-.076-.062-1.7-1.004-4.682-2.357-8.45-3.71-7.535-2.71-18.256-5.546-30.214-8.087-17.38-3.692-37.464-6.792-55.133-8.607zm32.142 55.933c-5.626.428-12.035.78-18.986.905l-26.097 43.653 35.194 11.67 1.342-3.64a265.75 265.75 0 0 0 4.83-14.364l-14.076-5.15 17.793-33.073z\"/></svg>" }, + sand: { title: "砂場", svg: "<svg viewBox=\"0 0 100 100\" aria-hidden=\"true\" fill=\"currentColor\"><g stroke-width=\"0\"/>\n\n<g stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\" stroke-width=\"2\"> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g> <g> <path d=\"M92.3,8.2C92.2,8,92,7.8,91.8,7.7C76,0.6,57.2,4.1,45,16.3L25.8,35.4c-0.4,0.4-0.4,1,0,1.4l12.6,12.6l-4.8,4.8 c-1.5-1.4-4-1.3-5.5,0.1L4.9,77.7c-1.6,1.6-1.6,4.1,0,5.7l11.8,11.8c0.8,0.8,1.8,1.2,2.8,1.2s2.1-0.4,2.8-1.2l23.3-23.3 c1.5-1.5,1.5-3.9,0.1-5.5l4.8-4.8l12.6,12.6c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3L83.7,55C95.9,42.8,99.4,24,92.3,8.2z M44.3,64.9l-9.3-9.2l5.5-5.5c0,0,0,0,0,0s0,0,0,0l2.3-2.3c0.8-0.8,2.1-0.8,2.8,0l6.4,6.4c0.8,0.8,0.8,2,0,2.8L44.3,64.9z M44.2,70.4L20.8,93.7c-0.8,0.8-2.1,0.8-2.8,0L6.3,82c-0.8-0.8-0.8-2,0-2.8l23.3-23.3c0.4-0.4,0.9-0.6,1.4-0.6s1,0.2,1.4,0.6 l0.5,0.5l0,0l10.7,10.7c0,0,0,0,0,0l0.5,0.5C44.9,68.4,44.9,69.6,44.2,70.4z M82.3,53.6L63.9,72L52,60.1l1.6-1.6 c1.6-1.6,1.6-4.1,0-5.7l-6.4-6.4c-1.5-1.5-4.1-1.5-5.7,0L39.9,48L28,36.1l18.4-18.4C57.9,6.1,75.7,2.8,90.7,9.3 C97.2,24.3,93.9,42.1,82.3,53.6z\"/> <path d=\"M85.7,13c-0.5,0.2-0.7,0.8-0.5,1.3c5.1,11.4,2.5,25-6.4,33.9c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3 s0.5-0.1,0.7-0.3c9.5-9.5,12.2-24,6.8-36.2C86.8,13,86.2,12.8,85.7,13z\"/> <path d=\"M13.9,80.7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L13.9,80.7z\"/> <path d=\"M17.5,77.2c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L17.5,77.2z\"/> <path d=\"M21,73.6c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L21,73.6z\"/> </g> </g> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> </g>\n\n<g> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g> <g> <path d=\"M92.3,8.2C92.2,8,92,7.8,91.8,7.7C76,0.6,57.2,4.1,45,16.3L25.8,35.4c-0.4,0.4-0.4,1,0,1.4l12.6,12.6l-4.8,4.8 c-1.5-1.4-4-1.3-5.5,0.1L4.9,77.7c-1.6,1.6-1.6,4.1,0,5.7l11.8,11.8c0.8,0.8,1.8,1.2,2.8,1.2s2.1-0.4,2.8-1.2l23.3-23.3 c1.5-1.5,1.5-3.9,0.1-5.5l4.8-4.8l12.6,12.6c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3L83.7,55C95.9,42.8,99.4,24,92.3,8.2z M44.3,64.9l-9.3-9.2l5.5-5.5c0,0,0,0,0,0s0,0,0,0l2.3-2.3c0.8-0.8,2.1-0.8,2.8,0l6.4,6.4c0.8,0.8,0.8,2,0,2.8L44.3,64.9z M44.2,70.4L20.8,93.7c-0.8,0.8-2.1,0.8-2.8,0L6.3,82c-0.8-0.8-0.8-2,0-2.8l23.3-23.3c0.4-0.4,0.9-0.6,1.4-0.6s1,0.2,1.4,0.6 l0.5,0.5l0,0l10.7,10.7c0,0,0,0,0,0l0.5,0.5C44.9,68.4,44.9,69.6,44.2,70.4z M82.3,53.6L63.9,72L52,60.1l1.6-1.6 c1.6-1.6,1.6-4.1,0-5.7l-6.4-6.4c-1.5-1.5-4.1-1.5-5.7,0L39.9,48L28,36.1l18.4-18.4C57.9,6.1,75.7,2.8,90.7,9.3 C97.2,24.3,93.9,42.1,82.3,53.6z\"/> <path d=\"M85.7,13c-0.5,0.2-0.7,0.8-0.5,1.3c5.1,11.4,2.5,25-6.4,33.9c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3 s0.5-0.1,0.7-0.3c9.5-9.5,12.2-24,6.8-36.2C86.8,13,86.2,12.8,85.7,13z\"/> <path d=\"M13.9,80.7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L13.9,80.7z\"/> <path d=\"M17.5,77.2c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L17.5,77.2z\"/> <path d=\"M21,73.6c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4 L21,73.6z\"/> </g> </g> <g/> <g/> <g/> <g/> <g/> <g/> <g/> <g/> </g></svg>" }, + playground: { title: "複合遊具", svg: "<svg viewBox=\"0 0 512.001 512.001\" aria-hidden=\"true\" fill=\"currentColor\"><g>\n\t<g>\n\t\t<path d=\"M501.335,170.587h-352v-21.333h10.667c4.181,0,7.979-2.453,9.728-6.251c1.728-3.819,1.067-8.299-1.685-11.435\n\t\t\tL93.377,46.235c-4.053-4.651-12.011-4.651-16.064,0L2.647,131.569c-2.752,3.157-3.435,7.616-1.685,11.435\n\t\t\tc1.728,3.797,5.525,6.251,9.707,6.251h10.667v309.333c0,5.888,4.779,10.667,10.667,10.667s10.667-4.779,10.667-10.667v-10.667\n\t\t\th85.333v10.667c0,5.888,4.779,10.667,10.667,10.667s10.667-4.779,10.667-10.667V191.921h64V300.55\n\t\t\tc-12.395,4.416-21.333,16.149-21.333,30.037c0,17.643,14.357,32,32,32s32-14.357,32-32c0-13.888-8.939-25.621-21.333-30.037\n\t\t\tV191.921h85.333v87.296c-12.395,4.416-21.333,16.149-21.333,30.037c0,17.643,14.357,32,32,32c17.643,0,32-14.357,32-32\n\t\t\tc0-13.888-8.939-25.621-21.333-30.037v-87.296h64v266.667c0,5.888,4.779,10.667,10.667,10.667c5.888,0,10.667-4.779,10.667-10.667\n\t\t\tV191.921h42.667v23.573c-6.4,2.667-10.667,7.531-10.667,13.76c0,6.229,4.267,11.093,10.667,13.76v31.147\n\t\t\tc-6.4,2.667-10.667,7.531-10.667,13.76s4.267,11.093,10.667,13.76v31.147c-6.4,2.667-10.667,7.531-10.667,13.76\n\t\t\tc0,6.229,4.267,11.093,10.667,13.76v31.147c-6.4,2.667-10.667,7.531-10.667,13.76c0,6.229,4.267,11.093,10.667,13.76v18.24\n\t\t\tc0,5.888,4.779,10.667,10.667,10.667c5.888,0,10.667-4.779,10.667-10.667v-18.24c6.4-2.667,10.667-7.531,10.667-13.76\n\t\t\tc0-6.229-4.267-11.093-10.667-13.76v-31.147c6.4-2.667,10.667-7.531,10.667-13.76c0-6.229-4.267-11.093-10.667-13.76v-31.147\n\t\t\tc6.4-2.667,10.667-7.531,10.667-13.76c0-6.229-4.267-11.093-10.667-13.76v-31.147c6.4-2.667,10.667-7.531,10.667-13.76\n\t\t\ts-4.267-11.093-10.667-13.76v-23.573h10.667c5.888,0,10.667-4.779,10.667-10.667S507.223,170.587,501.335,170.587z\n\t\t\t M128.001,426.587H42.668v-42.667h85.333V426.587z M128.001,362.587H42.668v-42.667h85.333V362.587z M128.001,298.587H42.668\n\t\t\tv-42.667h85.333V298.587z M128.001,234.587H42.668v-85.333h85.333V234.587z M34.177,127.921l51.157-58.475l51.157,58.475H34.177z\n\t\t\t M224.001,341.254c-5.888,0-10.667-4.779-10.667-10.667s4.779-10.667,10.667-10.667s10.667,4.779,10.667,10.667\n\t\t\tS229.889,341.254,224.001,341.254z M330.668,319.921c-5.888,0-10.667-4.779-10.667-10.667s4.779-10.667,10.667-10.667\n\t\t\ts10.667,4.779,10.667,10.667S336.556,319.921,330.668,319.921z\"/>\n\t</g>\n</g></svg>" }, + picnic: { title: "ピクニック・広場", svg: "<svg viewBox=\"0 0 512 512\" aria-hidden=\"true\"><g>\n\t<rect x=\"10.199\" y=\"228.645\" style=\"fill:#FF7731;\" width=\"491.602\" height=\"62.593\"/>\n\t<path style=\"fill:#FF7731;\" d=\"M482.422,291.243H29.578v85.334c0,66.844,54.187,121.031,121.031,121.031h210.784\n\t\tc66.843-0.001,121.03-54.188,121.03-121.031V291.243z\"/>\n</g>\n<rect x=\"361.613\" y=\"56.441\" style=\"fill:#BDCED4;\" width=\"50.068\" height=\"50.905\"/>\n<path style=\"fill:#FF2E48;\" d=\"M455.075,209.154c0-23.966-19.428-43.395-43.395-43.395v-58.416H361.61v58.416\n\tc-23.966,0-43.395,19.428-43.395,43.395v19.495h136.86V209.154z\"/>\n<rect x=\"224.607\" y=\"14.39\" style=\"fill:#FF7731;\" width=\"62.786\" height=\"276.847\"/>\n<rect x=\"58.135\" y=\"173.273\" style=\"fill:#BDCED4;\" width=\"138.709\" height=\"55.382\"/>\n<path d=\"M501.801,218.451h-36.526v-9.296c0-26.065-18.704-47.844-43.396-52.62v-49.191V56.438c0-5.633-4.567-10.199-10.199-10.199\n\th-50.071c-5.632,0-10.199,4.566-10.199,10.199v50.905v49.191c-24.69,4.776-43.394,26.555-43.394,52.62v9.296H297.59V14.391\n\tc0-5.633-4.567-10.199-10.199-10.199h-62.783c-5.632,0-10.199,4.566-10.199,10.199v204.06h-7.365v-45.177\n\tc0-5.633-4.567-10.199-10.199-10.199H58.135c-5.632,0-10.199,4.566-10.199,10.199v45.177H10.199C4.567,218.451,0,223.017,0,228.65\n\tv62.595c0,5.633,4.567,10.199,10.199,10.199h9.179v75.134c0,72.36,58.87,131.23,131.23,131.23h210.783\n\tc72.36,0,131.23-58.87,131.23-131.23v-75.134h9.179c5.632,0,10.199-4.566,10.199-10.199V228.65\n\tC512,223.017,507.433,218.451,501.801,218.451z M371.809,66.638h29.673v30.507h-29.673V66.638z M328.414,209.154\n\tc0-18.303,14.892-33.195,33.195-33.195c5.632,0,10.199-4.566,10.199-10.199v-48.217h29.673v48.217\n\tc0,5.633,4.567,10.199,10.199,10.199c18.305,0,33.196,14.892,33.196,33.195v9.296H328.414V209.154z M297.59,281.044v-42.196h194.011\n\tv42.196H297.59z M234.808,24.59h42.385v256.454h-42.385L234.808,24.59L234.808,24.59z M68.335,183.471h118.311v34.978H68.335\n\tV183.471z M20.398,281.044v-42.196H214.41v42.196H20.398z M472.223,301.443v75.134c0,61.113-49.719,110.832-110.832,110.832H150.609\n\tC89.496,487.408,39.777,437.69,39.777,376.577v-75.134H472.223z\"/>\n<path d=\"M386.55,343.599h12.239c5.632,0,10.199-4.566,10.199-10.199c0-5.633-4.567-10.199-10.199-10.199H386.55\n\tc-5.632,0-10.199,4.566-10.199,10.199C376.351,339.033,380.918,343.599,386.55,343.599z\"/>\n<path d=\"M62.215,333.4c0,5.633,4.567,10.199,10.199,10.199h269.259c5.632,0,10.199-4.566,10.199-10.199\n\tc0-5.633-4.567-10.199-10.199-10.199H72.414C66.782,323.201,62.215,327.767,62.215,333.4z\"/>\n<path d=\"M398.789,371.137H72.414c-5.632,0-10.199,4.566-10.199,10.199s4.567,10.199,10.199,10.199h326.375\n\tc5.632,0,10.199-4.566,10.199-10.199S404.421,371.137,398.789,371.137z\"/>\n<path d=\"M398.789,419.073H94.853c-5.632,0-10.199,4.566-10.199,10.199s4.567,10.199,10.199,10.199h303.936\n\tc5.632,0,10.199-4.566,10.199-10.199S404.421,419.073,398.789,419.073z\"/></svg>" }, + ball: { title: "ボール遊び", svg: "<svg viewBox=\"0 0 64 64\" aria-hidden=\"true\" fill=\"currentColor\"><path d=\"M61.934 31.992c.021-.713.209-10.904-5.822-17.538c-.268-.593-1.539-2.983-5.641-5.904a41.959 41.959 0 0 0-5.775-3.763l-.008-.004C44.432 4.646 39.43 2 33.359 2c-.461 0-.917.027-1.368.058V2.05c-4.629-.101-9.227 1.09-11.998 2.341c-2.458 1.11-5.187 2.971-5.384 3.115C11.205 9.41 4.75 17.051 4.239 21.1c-2.063 2.637-3.787 14.482.004 21.697c2.658 10.027 12.664 15.045 13.46 15.43c.484.309 5.937 3.68 12.636 3.68c.281 0 1.98.094 2.586.094c7.241 0 17.971-5.104 20.217-9.102c6.171-4.514 9.37-16.147 8.792-20.907M17.758 47.055c-2.869-4.641-4.504-10.705-4.854-12.098c.908-1.361 5.387-7.965 7.939-9.952c1.445.266 7.479 1.374 13.17 2.404c.715 1.853 3.852 10.029 4.75 13.185c-.99 1.174-4.879 5.702-8.708 9.248c-4.065.019-10.979-2.326-12.297-2.787M53.824 14.58c-.012.45-.119 2.05-.885 3.887c-1.521-.777-5.344-2.441-10.584-2.722c-.793-1.171-3.777-5.254-8.49-8.086c.645-1.262 1.543-2.801 2.068-3.27c.17-.048.434-.092.836-.092c2.527 0 6.893 1.655 7.273 1.802c.403.213 8.251 4.439 9.782 8.481M11.773 34.012c-3.423-.584-5.458-1.648-6.066-2.008c-1.273-4.617-.248-9.607-.09-10.322c1.256-2.246 4.832-7.971 7.191-9.058c2.445-.499 5.494.121 6.736.424c-.117 1.615-.342 6.127.326 10.862c-2.706 2.178-6.989 8.447-8.097 10.102M31.685 3.53c.768.057 1.895.225 2.667.454c-.77 1.024-1.559 2.542-1.932 3.292c-1.57.257-7.533 1.397-12.211 4.43c-.943-.25-3.791-.917-6.488-.687c.668-1.293 1.666-2.249 1.773-2.347c.371-.266 7.513-5.263 16.191-5.155v.013m19.096 38.093c-1.17-.048-5.678-.305-10.621-1.466c-.947-3.302-4.074-11.444-4.789-13.296a556.586 556.586 0 0 1 6.928-9.654c5.688.312 9.682 2.387 10.455 2.82c3.295 5.299 4.018 10.711 4.117 11.615c-1.75 5.446-5.211 9.113-6.09 9.981M3.655 28.519c.084 1.266.287 2.599.654 3.917a11.738 11.738 0 0 0-.682 2.651a33.039 33.039 0 0 1 .028-6.568m9.644 23.359c1.508-1.453 3.367-2.867 4.088-3.401c1.63.574 8.324 2.837 12.591 2.837c.727.975 3.104 4.028 6.018 6.362c-1.814 1.775-4.434 2.613-4.897 2.752c-8.127.218-16.042-4.35-17.8-8.55m21.463 8.538c.922-.537 1.883-1.244 2.678-2.139c1.297-.179 6.863-1.137 11.893-4.832c.332.036.879.08 1.49.063c-3.018 2.957-10.382 6.26-16.061 6.908m15.424-8.376c1.807-4.708 1.73-8.258 1.641-9.392c.992-.972 4.396-4.599 6.285-10.113c1.018.17 1.68.429 1.994.574c.109.4.291 1.324.188 2.725c-.77 5.043-3.428 12.6-8.084 15.941c-.468.239-1.292.291-2.024.265\" fill=\"currentColor\"></path></svg>" }, +}; diff --git a/src/data/parks-rows.ts b/src/data/parks-rows.ts new file mode 100644 index 0000000..39d7ae0 --- /dev/null +++ b/src/data/parks-rows.ts @@ -0,0 +1,141 @@ +import parksData from "@/data/kodaira-parks.json"; +import osmData from "@/data/parks-geojson"; +import nearbyData from "@/data/kodaira-nearby-parks.json"; +import facilitiesData from "@/data/kodaira-facilities.json"; +import nearbyFacilitiesData from "@/data/kodaira-nearby-facilities.json"; + +export type Park = Record<string, string>; + +function nearbyCentroid(feature: any): [number, number] { + const g = feature.geometry; + if (g.type === "Point") return [g.coordinates[1], g.coordinates[0]]; + const ring = g.type === "Polygon" ? g.coordinates[0] : g.coordinates[0][0]; + if (!ring?.length) return [0, 0]; + let lat = 0; + let lon = 0; + for (const [x, y] of ring) { + lon += x; + lat += y; + } + return [lat / ring.length, lon / ring.length]; +} + +function mapsQueryForNearby(name: string, city: string): string { + return name ? (city && !name.includes(city) ? `${city}${name}` : name) : ""; +} + +// 「小平市小川町2-1325」→「小川町」。公共施設の住所から町名を取り出す。 +function townFromAddress(address: string): string { + const match = /^小平市(.+?)(?:\d|$)/.exec(address || ""); + return match ? match[1].trim() : ""; +} + +// 市境500m以内の市外公園。小平市の公園と同じ表に「市外」バッジ付きで並べる。 +const nearbyRows: Park[] = ((nearbyData as any).features ?? []).map((f: any) => { + const p = f.properties || {}; + const [lat, lon] = nearbyCentroid(f); + const area = Math.round(Number(p.area_m2) || 0); + const name = String(p.name || "").trim(); + const mapsQuery = name + ? mapsQueryForNearby(name, String(p.city || "")) + : `${lat},${lon}`; + return { + "公園名": name, + "住所": p.city ? `${p.city}(市外)` : "市外", + "町名": "市外", + "面積㎡": area ? String(area) : "", + "500㎡以上": "", + "種別": "市外", + "南西部指定管理": "", + "トイレ": "", + "水飲み場": "", + "ベンチ・椅子": "", + "テーブル": "", + "パーゴラ": "", + "東屋": "", + "防災用具": "", + "ボール遊び": "", + "遊具": "", + "駐車場": "", + "その他設備": "", + "緯度": String(lat), + "経度": String(lon), + "備考": `市外・小平市境から約${Math.round(Number(p.distance_m) || 0)}m${ + f.geometry?.type === "Point" ? "・範囲未取得" : "" + }`, + "設置者": "市外", + "状態": "", + "地図キー": p.key || `${p.osm_type || ""}/${p.osm_id || ""}`, + "地図URL": `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(mapsQuery)}`, + }; +}); + +// 市内・市外の公共施設を公園一覧と同じ行形式にする。 +function facilityRow(f: any, nearby: boolean): Park { + const name = String(f.name || "").trim(); + const type = String(f.type || ""); + const city = String(f.city || "").trim(); + const address = String(f.address || city || (nearby ? "市外" : "")).trim(); + const hours = f.hours ? `開館 ${f.hours}` : ""; + const closed = f.closed ? `休館 ${f.closed}` : ""; + const note = [type, hours, closed, f.note].filter(Boolean).join("。"); + const mapsQuery = nearby ? mapsQueryForNearby(name, city) : `小平市${name}`; + const toilet = f.toilet === "○" ? "○" : ""; + const water = f.water === "○" ? "○" : ""; + return { + "公園名": name, + "住所": address, + "町名": nearby ? "市外" : townFromAddress(address), + "面積㎡": "", + "500㎡以上": "", + "種別": type, + "南西部指定管理": "", + "トイレ": toilet, + "水飲み場": "", + "給水": water, + "ベンチ・椅子": "", + "テーブル": "", + "パーゴラ": "", + "東屋": "", + "防災用具": "", + "ボール遊び": "", + "遊具": "", + "駐車場": "", + "その他設備": "", + "緯度": f.lat != null ? String(f.lat) : "", + "経度": f.lon != null ? String(f.lon) : "", + "備考": note, + "設置者": nearby ? "市外" : "小平市", + "状態": "", + "地図URL": `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(mapsQuery)}`, + "公式URL": String(f.source || ""), + "施設": "1", + }; +} + +const facilityRows: Park[] = [ + ...(facilitiesData as any[]).map((f) => facilityRow(f, false)), + ...(nearbyFacilitiesData as any[]).map((f) => facilityRow(f, true)), +]; + +export const parks: Park[] = [ + ...(parksData as Park[]), + ...nearbyRows, + ...facilityRows, +]; + +export const total = parks.length; + +export const towns = [...new Set(parks.map((p) => p["町名"]).filter(Boolean))].sort( + (a, b) => a.localeCompare(b, "ja"), +); + +// OpenStreetMapにポリゴンがあり、地図から位置を表示できる公園名。 +export const mappedNames = new Set<string>([ + ...(osmData as any).features + .map((f: any) => f.properties?.our_name) + .filter(Boolean), + ...((nearbyData as any).features ?? []) + .map((f: any) => String(f.properties?.name || "").trim()) + .filter(Boolean), +]); diff --git a/src/styles/custom.css b/src/styles/custom.css index 1ea4aa7..ba1f7ed 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -173,6 +173,68 @@ box-shadow: none; } +/* 公園ページの折りたたみ(説明・凡例) */ +.sl-markdown-content details.page-help, +.sl-markdown-content details.park-map-help { + margin: 0.75rem 0; + border: 1px solid var(--sl-color-gray-5); + border-radius: 10px; + background: var(--sl-color-gray-6); + overflow: hidden; +} +.sl-markdown-content details.page-help > summary, +.sl-markdown-content details.park-map-help > summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.65rem 0.9rem; + background: transparent; + color: var(--sl-color-text); + font-size: 0.92rem; + font-weight: 600; + list-style: none; +} +.sl-markdown-content details.page-help > summary::-webkit-details-marker, +.sl-markdown-content details.park-map-help > summary::-webkit-details-marker { + display: none; +} +.sl-markdown-content details.page-help > summary::after, +.sl-markdown-content details.park-map-help > summary::after { + content: "▾"; + flex: none; + color: var(--sl-color-gray-3); + transition: transform 0.15s ease; +} +.sl-markdown-content details.page-help[open] > summary::after, +.sl-markdown-content details.park-map-help[open] > summary::after { + transform: rotate(180deg); +} +.sl-markdown-content details.page-help > summary:hover, +.sl-markdown-content details.park-map-help > summary:hover { + background: var(--sl-color-gray-5); +} +.sl-markdown-content details.page-help[open] > summary, +.sl-markdown-content details.park-map-help[open] > summary { + border-bottom: 1px solid var(--sl-color-gray-5); +} +.sl-markdown-content details.page-help > p, +.sl-markdown-content details.page-help > ul, +.sl-markdown-content details.page-help > ol, +.sl-markdown-content details.park-map-help > p { + margin-left: 0.9rem; + margin-right: 0.9rem; +} +.sl-markdown-content details.page-help > p:first-of-type, +.sl-markdown-content details.park-map-help > p:first-of-type { + margin-top: 0.65rem; +} +.sl-markdown-content details.page-help > p:last-child, +.sl-markdown-content details.page-help > ul:last-child, +.sl-markdown-content details.park-map-help > p:last-child { + margin-bottom: 0.75rem; +} + /* ── アーカイブの表(保存日・更新日の列) ── */ .archive-table table th:nth-child(2), .archive-table table td:nth-child(2), |
