diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-24 12:29:24 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-24 12:29:24 +0900 |
| commit | a71727538f65ab4f10f5239550be82590f9d47d1 (patch) | |
| tree | 4608d37faead4f253c3311982a400a8fb88fba9e /src/components | |
| parent | c7efe674b34a2949b76d62bb71a972427965af92 (diff) | |
parks: 公園一覧と地図、公共施設の情報を更新
- 近隣公園のクリックを市内公園と同じ挙動にし、Googleマップを公園名で開く
- 公共施設のトイレを○にし、ルネこだいらを追加
- 未取得だった施設の座標を追加し、再ジオコーディング時の上書きを防止
- 手入力ポリゴン、近隣公園、PWA、ライセンス表示を追加
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/FacilitiesTable.astro | 186 | ||||
| -rw-r--r-- | src/components/ParkMap.astro | 261 | ||||
| -rw-r--r-- | src/components/ParksTable.astro | 114 | ||||
| -rw-r--r-- | src/components/PwaRegister.astro | 19 |
4 files changed, 535 insertions, 45 deletions
diff --git a/src/components/FacilitiesTable.astro b/src/components/FacilitiesTable.astro new file mode 100644 index 0000000..2974581 --- /dev/null +++ b/src/components/FacilitiesTable.astro @@ -0,0 +1,186 @@ +--- +// 地域センター・公民館・図書館・市役所支所・文化施設の一覧。 +// トイレは市の公共施設として基本的に利用できる。給水は公式ページに明記が少ないため「未確認」が多い。 +import facilitiesData from "@/data/kodaira-facilities.json"; + +type Facility = { + name: string; + type: string; + address: string; + hours: string; + closed: string; + toilet: string; + water: string; + note: string; + source: string; +}; + +const facilities = facilitiesData as Facility[]; +const types = ["地域センター", "公民館", "図書館", "市役所支所", "文化施設"]; +const mapsUrl = (name: string) => + `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`小平市${name}`)}`; +--- + +<div class="facilities" data-fac-root> + <div class="fac-controls"> + <label> + 種別 + <select data-fac-type> + <option value="">すべて</option> + {types.map((type) => <option value={type}>{type}</option>)} + </select> + </label> + <label> + キーワード + <input type="search" data-fac-search placeholder="施設名・住所・備考" /> + </label> + <label><input type="checkbox" data-fac-toilet /> トイレ○</label> + <label><input type="checkbox" data-fac-water /> 給水○</label> + <span class="fac-count" data-fac-count>{facilities.length}件</span> + </div> + + <div class="fac-table-wrap"> + <table> + <thead> + <tr> + <th scope="col">施設名</th> + <th scope="col">種別</th> + <th scope="col">住所</th> + <th scope="col">開館時間</th> + <th scope="col">休館日</th> + <th scope="col">トイレ</th> + <th scope="col">給水</th> + <th scope="col">備考</th> + </tr> + </thead> + <tbody> + {facilities.map((f) => ( + <tr + data-fac-row + data-type={f.type} + data-toilet={f.toilet || ""} + data-water={f.water || ""} + data-search={`${f.name} ${f.type} ${f.address} ${f.note}`.toLowerCase()} + > + <td> + {f.source ? ( + <a href={f.source} target="_blank" rel="noopener">{f.name}</a> + ) : ( + f.name + )} + </td> + <td>{f.type}</td> + <td> + <a href={mapsUrl(f.name)} target="_blank" rel="noopener"> + {f.address || "—"} + </a> + </td> + <td>{f.hours || "未確認"}</td> + <td>{f.closed || "未確認"}</td> + <td + class:list={[ + "fac-status", + f.toilet === "○" ? "is-yes" : f.toilet === "×" ? "is-no" : "", + ]} + >{f.toilet || "未確認"}</td> + <td + class:list={[ + "fac-status", + f.water === "○" ? "is-yes" : f.water === "×" ? "is-no" : "", + ]} + >{f.water || "未確認"}</td> + <td>{f.note}</td> + </tr> + ))} + </tbody> + </table> + </div> + + <p class="fac-note"> + トイレは市の公共施設として基本的に利用できます(○)。給水は公式ページに明記が少ないため「未確認」が多いです。最新の情報は各施設の公式ページで確認してください。地図の位置は住所・名称からの概略です。 + </p> +</div> + +<script> + const root = document.querySelector<HTMLElement>("[data-fac-root]"); + if (root) { + const rows = Array.from(root.querySelectorAll<HTMLTableRowElement>("[data-fac-row]")); + const search = root.querySelector<HTMLInputElement>("[data-fac-search]"); + const type = root.querySelector<HTMLSelectElement>("[data-fac-type]"); + const toilet = root.querySelector<HTMLInputElement>("[data-fac-toilet]"); + const water = root.querySelector<HTMLInputElement>("[data-fac-water]"); + const count = root.querySelector<HTMLElement>("[data-fac-count]"); + const apply = () => { + const q = (search?.value || "").trim().toLowerCase(); + const t = type?.value || ""; + const needToilet = Boolean(toilet?.checked); + const needWater = Boolean(water?.checked); + let shown = 0; + rows.forEach((row) => { + const okType = !t || row.dataset.type === t; + const okText = !q || (row.dataset.search || "").includes(q); + const okToilet = !needToilet || row.dataset.toilet === "○"; + const okWater = !needWater || row.dataset.water === "○"; + const ok = okType && okText && okToilet && okWater; + row.hidden = !ok; + if (ok) shown += 1; + }); + if (count) count.textContent = `${shown} / ${rows.length}件`; + }; + search?.addEventListener("input", apply); + type?.addEventListener("change", apply); + toilet?.addEventListener("change", apply); + water?.addEventListener("change", apply); + apply(); + } +</script> + +<style> + .fac-controls { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; + margin: 0.75rem 0; + font-size: 0.9em; + } + .fac-controls label { + display: flex; + gap: 0.35rem; + align-items: center; + } + .fac-controls input, + .fac-controls select { + padding: 0.3rem 0.5rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: 4px; + background: var(--sl-color-bg); + color: var(--sl-color-text); + } + .fac-count { + color: var(--sl-color-gray-2); + font-size: 0.85em; + } + .fac-table-wrap { + overflow-x: auto; + } + .fac-table-wrap table { + font-size: 0.85em; + } + .fac-status { + text-align: center; + white-space: nowrap; + } + .fac-status.is-yes { + color: #1b5e20; + font-weight: 700; + } + .fac-status.is-no { + color: #b71c1c; + } + .fac-note { + margin-top: 0.5rem; + color: var(--sl-color-gray-2); + font-size: 0.82em; + } +</style> diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro index 8537af2..1c75bee 100644 --- a/src/components/ParkMap.astro +++ b/src/components/ParkMap.astro @@ -13,12 +13,14 @@ <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"> - 公園の範囲を赤で表示しています。地図を拡大すると見やすくなります。トイレと水飲み場のアイコンは、地図を拡大すると表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © <a + 公園の範囲を赤で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園と公共施設は別レイヤーで表示し、右上のレイヤ切替でON/OFFできます。トイレと水飲み場のアイコンは、地図を拡大すると表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a @@ -39,6 +41,26 @@ border: 1px solid var(--sl-color-gray-5); border-radius: 8px; } + .park-map:fullscreen, + .park-map:-webkit-full-screen { + height: 100vh !important; + width: 100vw !important; + border: 0; + border-radius: 0; + } + :global(.park-map-fullscreen button) { + width: 30px; + height: 30px; + border: 0; + background: #fff; + color: #1e293b; + font-size: 18px; + line-height: 1; + cursor: pointer; + } + :global(.park-map-fullscreen button:hover) { + background: #f1f5f9; + } .park-map-legend { display: flex; flex-wrap: wrap; @@ -65,6 +87,13 @@ .swatch-unmatched { background: #ffcdd2; } + .swatch-nearby { + background: #42a5f5; + } + .swatch-facility { + background: #8e24aa; + border-radius: 50%; + } .swatch-boundary { height: 0; border-top: 4px solid #6a1b9a; @@ -155,6 +184,10 @@ background: #0277bd; border-radius: 50%; } + :global(.park-fac-markers), + :global(.park-fac-list) { + pointer-events: none; + } :global(.park-fac-list) { display: inline-flex; gap: 2px; @@ -198,9 +231,12 @@ max-height: none !important; margin: 0 !important; } - /* Starlightの画像用 margin がタイルに付くと行が16pxずれるため、0にする。 */ + /* Starlightの画像用スタイル(margin / border / border-radius)が + タイルに付くと、行ずれやタイルごとの枠が出るため打ち消す。 */ .park-map :global(.leaflet-tile) { margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; } @media (max-width: 640px) { .park-map { @@ -214,6 +250,8 @@ import "leaflet/dist/leaflet.css"; import geojson from "@/data/parks-geojson"; import boundary from "@/data/kodaira-boundary.json"; + import nearbyData from "@/data/kodaira-nearby-parks.json"; + import facilities from "@/data/kodaira-facilities.json"; import parks from "@/data/kodaira-parks.json"; const MATCHED_STYLE = { @@ -241,13 +279,32 @@ fillColor: "#d32f2f", fillOpacity: 0.6, }; + const NEARBY_STYLE = { + color: "#1565c0", + weight: 1.5, + fillColor: "#42a5f5", + fillOpacity: 0.25, + }; + const NEARBY_SELECTED_STYLE = { + color: "#0d47a1", + weight: 3, + fillColor: "#1565c0", + fillOpacity: 0.55, + }; + const NEARBY_DIM_STYLE = { + color: "#90caf9", + weight: 0.8, + fillColor: "#bbdefb", + fillOpacity: 0.05, + opacity: 0.3, + }; const el = document.getElementById("park-map"); if (el && !el.dataset.initialized) { el.dataset.initialized = "1"; const map = L.map(el, { - scrollWheelZoom: false, + scrollWheelZoom: true, zoomSnap: 1, zoomDelta: 1, zoomAnimation: false, @@ -264,8 +321,51 @@ '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); + const FullscreenControl = L.Control.extend({ + onAdd: () => { + const container = L.DomUtil.create("div", "leaflet-bar park-map-fullscreen"); + const button = L.DomUtil.create("button", "", container); + button.type = "button"; + button.title = "全画面表示"; + button.setAttribute("aria-label", "全画面表示"); + button.innerHTML = "⛶"; + L.DomEvent.disableClickPropagation(container); + L.DomEvent.on(button, "click", (ev: Event) => { + L.DomEvent.stop(ev); + const target = map.getContainer(); + if (!document.fullscreenElement) { + target.requestFullscreen?.().catch(() => {}); + } else { + document.exitFullscreen?.().catch(() => {}); + } + }); + const update = () => { + const active = document.fullscreenElement === map.getContainer(); + button.innerHTML = active ? "✕" : "⛶"; + button.title = active ? "全画面を終了" : "全画面表示"; + button.setAttribute("aria-label", active ? "全画面を終了" : "全画面表示"); + map.invalidateSize(); + }; + document.addEventListener("fullscreenchange", update); + return container; + }, + }); + if (document.fullscreenEnabled) { + new FullscreenControl({ position: "topright" }).addTo(map); + } + const layersByName = new Map<string, any>(); const allLayers: any[] = []; + type NearbyEntry = { name: string; layer: any; city: string; key: string }; + const nearbyEntries: NearbyEntry[] = []; + const nearbyInfoByName = new Map<string, any>(); + ((nearbyData as any).features ?? []).forEach((f: any) => { + const p = f.properties || {}; + const name = String(p.name || "").trim(); + if (name) nearbyInfoByName.set(name, p); + }); + const nearbyMapsQuery = (name: string, city = "") => + name ? (city ? `${city}${name}` : name) : ""; const parkDataByName = new Map<string, any>(); (parks as any[]).forEach((p) => parkDataByName.set(p["公園名"], p)); const ownerByName = new Map<string, string>(); @@ -303,22 +403,24 @@ osmName: string, listed = true, owner = "", + mapsQuery = "", ) => { if (!selectedBar || !selectedNameEl || !selectedMapsLink || !selectedTableLink) return; selectedNameEl.textContent = ourName; selectedNameEl.title = osmName && osmName !== ourName ? `OSM: ${osmName}` : ""; - // 名前が「緑地(OSM …)」のような場合は名前検索が使えないため、 - // 座標があれば緯度経度で開く。 + // 表と同じく、公園名で検索する。名前がない場合だけ住所・座標を使う。 const rec = parkDataByName.get(ourName); - const lat = rec?.["緯度"]; - const lng = rec?.["経度"]; - selectedMapsLink.href = - lat && lng - ? `https://www.google.com/maps/search/?api=1&query=${lat},${lng}` - : "https://www.google.com/maps/search/?api=1&query=" + - encodeURIComponent(`小平市${ourName}`); + const name = (ourName || "").trim(); + const query = mapsQuery || (name + ? `小平市${name}` + : rec?.["住所"] + ? `小平市${rec["住所"]}` + : rec?.["緯度"] && rec?.["経度"] + ? `${rec["緯度"]},${rec["経度"]}` + : "小平市"); + selectedMapsLink.href = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; selectedTableLink.dataset.parkRow = encodeURIComponent(ourName); selectedTableLink.hidden = !listed; if (selectedNoteEl) selectedNoteEl.hidden = listed; @@ -359,6 +461,7 @@ new SelectedControl({ position: "bottomleft" }).addTo(map); const layer = L.geoJSON(geojson as any, { + bubblingMouseEvents: false, style: (feature: any) => feature?.properties?.matched ? MATCHED_STYLE : UNMATCHED_STYLE, onEachFeature: (feature: any, lyr: any) => { @@ -375,15 +478,13 @@ const path = lyr.getElement?.(); if (path) path.setAttribute("data-park", ourName); }); - lyr.on("click", () => { + lyr.on("click", (ev: any) => { + if (ev?.originalEvent) L.DomEvent.stopPropagation(ev.originalEvent); filterName(ourName, false); showSelected(ourName, name, true, ownerByName.get(ourName) || ""); }); - } else if (p.name) { - // 一覧にないOSM上の公園・緑地。名前があればバーに表示する。 - lyr.on("click", () => showSelected(name, name, false)); } else { - // 名前のない緑地はクリックに反応させない。 + // 一覧にない公園・緑地は表示だけ。クリックには反応させない。 lyr.on("add", () => { const path = lyr.getElement?.(); if (path) path.classList.remove("leaflet-interactive"); @@ -432,6 +533,54 @@ interactive: false, }).addTo(map); + // 市外の近い公園(500m以内)は別レイヤーにする。 + // 小平市内の公園と同じく、クリックで一覧を絞り込み、左下に情報を表示する。 + const nearbyLayer = L.geoJSON(nearbyData as any, { + 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 || ""}`; + if (name) nearbyEntries.push({ name, layer: lyr, city, key }); + lyr.on("click", (ev: any) => { + if (ev?.originalEvent) L.DomEvent.stopPropagation(ev.originalEvent); + if (!name) return; + filterName(name, false); + showSelected(name, name, true, city || "市外", nearbyMapsQuery(name, city)); + }); + }, + }).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); + }); + publicFacilityLayer.addTo(map); + + L.control + .layers( + null, + { + "市外の近い公園(500m以内)": nearbyLayer, + "公共施設": publicFacilityLayer, + }, + { collapsed: false, position: "topright" }, + ) + .addTo(map); + // 拡大時だけ表示する設備アイコン(トイレ・水飲み場) // 一覧と一致した公園のポリゴン内にだけ表示する。 const facilityLayer = L.layerGroup(); @@ -459,6 +608,7 @@ iconAnchor: [0, 0], }), riseOnHover: false, + interactive: false, }).addTo(facilityLayer); }); @@ -492,24 +642,59 @@ }; fitAll(); - const focusPark = (name: string) => { + const focusPark = (name: string, key = "") => { + if (key) { + const nearbyHit = nearbyEntries.find((entry) => entry.key === key); + if (nearbyHit) { + map.fitBounds(nearbyHit.layer.getBounds(), { padding: [40, 40], maxZoom: 17 }); + showSelected( + name, + name, + true, + nearbyHit.city || "市外", + nearbyMapsQuery(name, nearbyHit.city), + ); + return true; + } + } const target = layersByName.get(name); - if (!target) return false; - map.fitBounds(target.getBounds(), { padding: [40, 40], maxZoom: 17 }); - showSelected( - name, - target._osmName || name, - true, - ownerByName.get(name) || "", - ); - return true; + if (target) { + map.fitBounds(target.getBounds(), { padding: [40, 40], maxZoom: 17 }); + showSelected( + name, + target._osmName || name, + true, + ownerByName.get(name) || "", + ); + return true; + } + const nearbyTargets = nearbyEntries + .filter((entry) => entry.name === name) + .map((entry) => entry.layer); + if (nearbyTargets.length) { + const group = L.featureGroup(nearbyTargets); + map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 17 }); + const info = nearbyInfoByName.get(name); + const city = String(info?.city || "").trim(); + showSelected(name, name, true, city || "市外", nearbyMapsQuery(name, city)); + return true; + } + return false; }; document.addEventListener("park:focus", (e: Event) => { - const name = (e as CustomEvent).detail?.name; - if (!name || !layersByName.has(name)) return; + const detail = (e as CustomEvent).detail || {}; + const name = detail.name; + if (!name) return; + const key = detail.key || ""; + if ( + !key && + !layersByName.has(name) && + !nearbyEntries.some((entry) => entry.name === name) + ) + return; el.scrollIntoView({ behavior: "smooth", block: "center" }); - focusPark(name); + focusPark(name, key); }); document.addEventListener("park:filter", (e: Event) => { @@ -532,6 +717,9 @@ lyr.setStyle(name ? MATCHED_STYLE : UNMATCHED_STYLE); } }); + nearbyEntries.forEach(({ name, layer: lyr }) => { + lyr.setStyle(name && names.has(name) ? NEARBY_SELECTED_STYLE : NEARBY_STYLE); + }); return; } @@ -549,6 +737,19 @@ } }); + nearbyEntries.forEach(({ name, layer: lyr }) => { + if (!active) { + lyr.setStyle(NEARBY_STYLE); + return; + } + if (name && names.has(name)) { + lyr.setStyle(NEARBY_STYLE); + visible.push(lyr); + } else { + lyr.setStyle(NEARBY_DIM_STYLE); + } + }); + if (active && visible.length && visible.length < 30) { const bounds = L.featureGroup(visible).getBounds(); if (bounds.isValid()) { diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro index 6f8fdbd..bea8ff2 100644 --- a/src/components/ParksTable.astro +++ b/src/components/ParksTable.astro @@ -5,16 +5,74 @@ // 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 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>; -const parks = parksData as Park[]; + +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) +// 市外の近隣公園も地図に表示しているので、同じように地図で見られる。 +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), -); +]); const EMPTY = "—"; @@ -28,6 +86,7 @@ const TYPE_LABEL: Record<string, string> = { 緑地: "緑地", 団地内公園: "団地内公園", 児童遊園: "児童遊園", + 市外: "市外の近隣公園", }; const TYPE_CLASS: Record<string, string> = { 街区: "parks-type--block", @@ -38,6 +97,7 @@ const TYPE_CLASS: Record<string, string> = { 緑地: "parks-type--green", 団地内公園: "parks-type--danchi", 児童遊園: "parks-type--jidou", + 市外: "parks-type--green", }; // 市立以外の公園に付けるバッジ(設置者が空欄なら市立として扱う)。 @@ -143,16 +203,17 @@ function sizeTitle(area: number): string { return "目安: 10,000㎡以上(広め)"; } -// 座標があればその位置を開く。ない場合は公園名で検索する(地番は地図側で正しく解決されないため)。 -// 公園名だけだと別の自治体の同名公園に飛ぶので「小平市」を必ず付ける。 +// 公園名で検索する。公園名だけだと別の自治体の同名公園に飛ぶので「小平市」を必ず付ける。 +// 公園名がない場合だけ住所、それもなければ座標を使う。 function mapsUrl(p: Park): string { - const lat = p["緯度"]; - const lng = p["経度"]; - if (lat && lng) { - return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`; - } - const query = encodeURIComponent(`小平市${p["公園名"]}`); - return `https://www.google.com/maps/search/?api=1&query=${query}`; + if (p["地図URL"]) return p["地図URL"]; + const name = (p["公園名"] || "").trim(); + const query = name + ? `小平市${name}` + : p["住所"] + ? `小平市${p["住所"]}` + : `${p["緯度"]},${p["経度"]}`; + return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; } // 検索対象(公園名・住所・町名・種別・設備・備考)。 @@ -164,6 +225,7 @@ function searchIndex(p: Park): string { p["種別"], TYPE_LABEL[p["種別"]], p["設置者"], + p["状態"], p["トイレ"], p["水飲み場"], p["ボール遊び"], @@ -243,11 +305,15 @@ const total = parks.length; type="button" class="park-name-link" data-focus-park={park["公園名"]} + data-focus-key={park["地図キー"] || ""} title="地図でこの公園の位置を表示" >{park["公園名"]}</button> ) : ( park["公園名"] )} + {park["状態"] === "閉鎖" ? ( + <span class="parks-status" title="閉鎖中">閉鎖</span> + ) : null} {park["設置者"] && park["設置者"] !== "小平市" ? ( <span class:list={["parks-owner", ownerClass(park["設置者"])]} @@ -440,6 +506,20 @@ const total = parks.length; .parks-owner--unknown { color: #757575; } + .parks-status { + display: inline-block; + margin-left: 0.35rem; + padding: 0.02rem 0.35rem; + border: 1px solid #ef9a9a; + border-radius: 999px; + background: #ffebee; + color: #b71c1c; + font-size: 0.7rem; + font-weight: 700; + line-height: 1.5; + vertical-align: 0.08em; + white-space: nowrap; + } .park-name-link { padding: 0; border: 0; @@ -607,7 +687,11 @@ const total = parks.length; btn.addEventListener("click", () => { const name = btn.dataset.focusPark; if (!name) return; - document.dispatchEvent(new CustomEvent("park:focus", { detail: { name } })); + document.dispatchEvent( + new CustomEvent("park:focus", { + detail: { name, key: btn.dataset.focusKey || "" }, + }), + ); }); }); diff --git a/src/components/PwaRegister.astro b/src/components/PwaRegister.astro new file mode 100644 index 0000000..82359a4 --- /dev/null +++ b/src/components/PwaRegister.astro @@ -0,0 +1,19 @@ +<script> + if ("serviceWorker" in navigator) { + if (import.meta.env.PROD) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js").catch(() => { + // PWA登録に失敗しても、通常のWebページとして動作させる。 + }); + }); + } else { + // 開発時はService WorkerのキャッシュがHMRを妨げるため、既存の登録を解除する。 + navigator.serviceWorker.getRegistrations().then((registrations) => { + registrations.forEach((registration) => registration.unregister()); + }); + if ("caches" in window) { + caches.keys().then((keys) => keys.forEach((key) => caches.delete(key))); + } + } + } +</script> |
