aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--astro.config.mjs6
-rw-r--r--package.json2
-rw-r--r--public/kodaira-data/parks/LICENSE.txt9
-rw-r--r--public/manifest.webmanifest6
-rw-r--r--public/sw.js13
-rw-r--r--scripts/make-park-qgis-kit.py65
-rw-r--r--scripts/park-editor.html254
-rw-r--r--scripts/park-editor.mjs106
-rw-r--r--scripts/update-nearby-facilities.py179
-rw-r--r--scripts/update-nearby-parks.py243
-rw-r--r--src/components/FacilitiesTable.astro70
-rw-r--r--src/components/ParkMap.astro446
-rw-r--r--src/components/ParkMapHelp.astro111
-rw-r--r--src/components/ParksFilters.astro184
-rw-r--r--src/components/ParksTable.astro260
-rw-r--r--src/content/docs/kodaira-data/parks.mdx90
-rw-r--r--src/data/kodaira-nearby-facilities.json306
-rw-r--r--src/data/kodaira-nearby-parks.json2
-rw-r--r--src/data/park-icons.ts15
-rw-r--r--src/data/parks-rows.ts141
-rw-r--r--src/styles/custom.css62
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),