aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/ParkMap.astro
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/ParkMap.astro')
-rw-r--r--src/components/ParkMap.astro261
1 files changed, 231 insertions, 30 deletions
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()) {