From a01062f43d36112c00d5c007ac9d488c14d8fb0d Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:24:24 +0900 Subject: parks: アイコン・フィルタ・編集画面を更新し、公開を一時停止 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 遊具アイコンをSVGRepo素材に差し替え、施設種別をアイコン表示 - フィルタに種類・遊具・ピクニックを追加し、適用時の地図移動を停止 - 現在地ボタンを追跡トグルに変更し、初回以外は地図を動かさない - 編集画面を市内公園・周辺公園・公共施設の4タブに拡張 - 公園ページをドラフト化し、サイドバーとPWAの公開を停止 --- astro.config.mjs | 6 +- package.json | 2 + public/kodaira-data/parks/LICENSE.txt | 9 + public/manifest.webmanifest | 6 +- public/sw.js | 13 +- scripts/make-park-qgis-kit.py | 65 ++++- scripts/park-editor.html | 254 ++++++++++++++---- scripts/park-editor.mjs | 106 ++++++-- scripts/update-nearby-facilities.py | 179 +++++++++++++ scripts/update-nearby-parks.py | 243 ++++++++++++++++- src/components/FacilitiesTable.astro | 70 ++++- src/components/ParkMap.astro | 446 ++++++++++++++++++++++++++------ src/components/ParkMapHelp.astro | 111 ++++++++ src/components/ParksFilters.astro | 184 +++++++++++++ src/components/ParksTable.astro | 260 ++++++++++--------- src/content/docs/kodaira-data/parks.mdx | 90 ++++--- src/data/kodaira-nearby-facilities.json | 306 ++++++++++++++++++++++ src/data/kodaira-nearby-parks.json | 2 +- src/data/park-icons.ts | 15 ++ src/data/parks-rows.ts | 141 ++++++++++ src/styles/custom.css | 62 +++++ 21 files changed, 2229 insertions(+), 341 deletions(-) create mode 100644 scripts/update-nearby-facilities.py create mode 100644 src/components/ParkMapHelp.astro create mode 100644 src/components/ParksFilters.astro create mode 100644 src/data/kodaira-nearby-facilities.json create mode 100644 src/data/park-icons.ts create mode 100644 src/data/parks-rows.ts 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 @@
-- 公園の範囲を赤で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園と公共施設は別レイヤーで表示し、右上のレイヤ切替でON/OFFできます。トイレと水飲み場のアイコンは、地図を拡大すると表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © OpenStreetMap contributors -
@@ -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+ 公園の範囲を赤、市外の近い公園を青、公共施設を紫(市外はオレンジ)で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園のうち、範囲が取得できないものは丸で表示しています。トイレと水飲み場のアイコンは拡大すると表示されます。さらに拡大すると、遊具アイコン(上の凡例)が表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © OpenStreetMap contributors +
++ 情報には誤りが含まれる場合があります。最新の情報は各施設の公式ページでご確認ください。 +
+ + 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設備の情報は、市の資料などに基づきます。設備で絞ると、この情報がある公園だけが対象になります。
-条件に合う公園が見つかりませんでした。
+条件に合う公園・施設が見つかりませんでした。
| 住所 | @@ -293,13 +255,14 @@ const total = parks.length;|
|---|---|
| + | {mappedNames.has(park["公園名"]) ? ( + ) : park["公式URL"] ? ( + {park["公園名"]} ) : ( park["公園名"] )} @@ -344,7 +315,7 @@ const total = parks.length; title={TYPE_LABEL[park["種別"]]} aria-label={TYPE_LABEL[park["種別"]]} > - {park["種別"]} + ) : ( 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; |