aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/FacilitiesTable.astro186
-rw-r--r--src/components/ParkMap.astro261
-rw-r--r--src/components/ParksTable.astro114
-rw-r--r--src/components/PwaRegister.astro19
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 @@
'&copy; <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>