From 56c5d10d394cc5de288a353ab018adb4d134e2d5 Mon Sep 17 00:00:00 2001
From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:01:36 +0900
Subject: parks: 全画面の絞り込み・経路リンク・現在地追従を改善
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
全画面時に地図内へ絞り込みパネルを表示し、1本指で地図を動かせるようにした。現在地マーカーを最前面にし、追従を改善。地図のリンクを現在地からの経路探索に変更した。
---
src/components/ParkMap.astro | 177 ++++++++++++++++++++++++++++++++++++++-----
1 file changed, 160 insertions(+), 17 deletions(-)
diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro
index 47dae97..a185fe3 100644
--- a/src/components/ParkMap.astro
+++ b/src/components/ParkMap.astro
@@ -45,6 +45,45 @@
:global(body.park-map-maximized) {
overflow: hidden;
}
+ :global(.park-map-filter-panel) {
+ position: absolute;
+ top: 10px;
+ left: 55px;
+ right: 55px;
+ z-index: 1000;
+ max-height: 62vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: var(--sl-color-bg, #fff);
+ color: var(--sl-color-text, #1e293b);
+ border: 1px solid var(--sl-color-gray-5, #cbd5e1);
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
+ }
+ :global(.park-map-filter-toggle) {
+ flex: none;
+ padding: 0.4rem 0.6rem;
+ border: 0;
+ border-bottom: 1px solid var(--sl-color-gray-5, #cbd5e1);
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ font-size: 0.85rem;
+ font-weight: 700;
+ text-align: left;
+ cursor: pointer;
+ }
+ :global(.park-map-filter-body) {
+ overflow: auto;
+ padding: 0.5rem;
+ }
+ :global(.park-map-filter-body .parks-controls) {
+ margin: 0;
+ }
+ :global(.park-map-filter-body .parks-disclaimer) {
+ margin: 0.3rem 0 0;
+ }
:global(.park-map.park-map-touch.leaflet-container) {
touch-action: none;
}
@@ -532,6 +571,13 @@
el.addEventListener(
"touchstart",
(e: TouchEvent) => {
+ if (
+ document.fullscreenElement === el ||
+ el.classList.contains("is-maximized")
+ ) {
+ tracking = false;
+ return;
+ }
if (e.touches.length !== 1) {
tracking = false;
return;
@@ -550,6 +596,12 @@
el.addEventListener(
"touchmove",
(e: TouchEvent) => {
+ if (
+ document.fullscreenElement === el ||
+ el.classList.contains("is-maximized")
+ ) {
+ return;
+ }
if (!tracking || e.touches.length !== 1) return;
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
@@ -580,6 +632,65 @@
'© OpenStreetMap contributors',
}).addTo(map);
+ // 全画面のときだけ、地図内にフィルターのパネルを出す
+ const filterPanel = document.createElement("div");
+ filterPanel.className = "park-map-filter-panel";
+ filterPanel.hidden = true;
+ const filterToggle = document.createElement("button");
+ filterToggle.type = "button";
+ filterToggle.className = "park-map-filter-toggle";
+ const filterToggleLabel = (open: boolean) =>
+ `絞り込み ${open ? "▴" : "▾"}`;
+ filterToggle.innerHTML = filterToggleLabel(false);
+ const filterBody = document.createElement("div");
+ filterBody.className = "park-map-filter-body";
+ filterBody.hidden = true;
+ filterPanel.append(filterToggle, filterBody);
+ L.DomEvent.disableClickPropagation(filterPanel);
+ L.DomEvent.disableScrollPropagation(filterPanel);
+ filterToggle.addEventListener("click", () => {
+ filterBody.hidden = !filterBody.hidden;
+ filterToggle.innerHTML = filterToggleLabel(!filterBody.hidden);
+ });
+
+ let filterHome: {
+ parent: Node;
+ next: Node | null;
+ disclaimerParent: Node | null;
+ disclaimerNext: Node | null;
+ } | null = null;
+
+ const setFiltersInMap = (on: boolean) => {
+ const controls = document.querySelector(".parks-controls");
+ const disclaimer = document.querySelector(".parks-disclaimer");
+ if (on) {
+ if (!controls || filterHome) return;
+ filterHome = {
+ parent: controls.parentNode as Node,
+ next: controls.nextSibling,
+ disclaimerParent: disclaimer?.parentNode ?? null,
+ disclaimerNext: disclaimer?.nextSibling ?? null,
+ };
+ filterBody.append(controls);
+ if (disclaimer) filterBody.append(disclaimer);
+ map.getContainer().appendChild(filterPanel);
+ filterPanel.hidden = false;
+ filterBody.hidden = false;
+ filterToggle.innerHTML = filterToggleLabel(true);
+ } else if (filterHome && controls) {
+ filterHome.parent.insertBefore(controls, filterHome.next);
+ if (disclaimer && filterHome.disclaimerParent) {
+ filterHome.disclaimerParent.insertBefore(
+ disclaimer,
+ filterHome.disclaimerNext,
+ );
+ }
+ filterPanel.remove();
+ filterPanel.hidden = true;
+ filterHome = null;
+ }
+ };
+
const FullscreenControl = L.Control.extend({
onAdd: () => {
const container = L.DomUtil.create("div", "leaflet-bar park-map-fullscreen");
@@ -598,6 +709,12 @@
button.innerHTML = active ? "✕" : "⛶";
button.title = active ? "全画面を終了" : "全画面表示";
button.setAttribute("aria-label", button.title);
+ setFiltersInMap(active);
+ // 全画面中は1本指でも地図を動かせるようにする(PCのドラッグと同じ)。
+ if (isTouch) {
+ if (active) map.dragging.enable();
+ else map.dragging.disable();
+ }
window.setTimeout(() => map.invalidateSize(), 60);
};
const enterCssMax = () => {
@@ -696,9 +813,8 @@
selectedNameEl.textContent = ourName;
selectedNameEl.title =
osmName && osmName !== ourName ? `OSM: ${osmName}` : "";
- // 表と同じく、公園名で検索する。名前がない場合だけ住所・座標を使う。
+ // 経路検索(現在地からのルート)を開く。
const rec = parkDataByName.get(ourName);
- const fixed = rec?.["地図URL"];
const name = (ourName || "").trim();
const query = mapsQuery || (name
? `小平市${name}`
@@ -707,8 +823,11 @@
: rec?.["緯度"] && rec?.["経度"]
? `${rec["緯度"]},${rec["経度"]}`
: "小平市");
- selectedMapsLink.href =
- fixed || `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
+ const destination =
+ rec?.["緯度"] && rec?.["経度"]
+ ? `${rec["緯度"]},${rec["経度"]}`
+ : query;
+ selectedMapsLink.href = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(destination)}`;
selectedTableLink.dataset.parkRow = encodeURIComponent(ourName);
selectedTableLink.hidden = !listed;
if (selectedNoteEl) selectedNoteEl.hidden = listed;
@@ -727,7 +846,7 @@
'' +
'' +
'一覧にない緑地・公園' +
- 'Googleマップ' +
+ '現在地から経路' +
'一覧で見る';
selectedBar = box;
selectedNameEl = box.querySelector(".park-map-selected-name");
@@ -782,7 +901,10 @@
});
lyr.on("click", (ev: any) => {
if (ev?.originalEvent) L.DomEvent.stopPropagation(ev.originalEvent);
- filterName(ourName, false);
+ highlightOnly(ourName);
+ document.dispatchEvent(
+ new CustomEvent("park:select-row", { detail: { name: ourName } }),
+ );
showSelected(ourName, name, true, ownerByName.get(ourName) || "");
});
} else {
@@ -1264,6 +1386,7 @@
let watchId: number | null = null;
let tracking = false;
let firstFix = true;
+ let userMoved = false;
let locateButton: HTMLButtonElement | null = null;
const LOCATE_ICON =
@@ -1290,6 +1413,16 @@
updateLocateButton();
};
+ // ユーザーが地図を動かしたら、追従を止める(勝手に戻らないように)。
+ map.on("dragstart", () => {
+ if (!tracking) return;
+ userMoved = true;
+ stopTracking();
+ setStatus(
+ "地図を動かしたため、現在地の追従を停止しました。もう一度ボタンを押すと再開します。",
+ );
+ });
+
const locate = () => {
if (tracking) {
stopTracking();
@@ -1315,19 +1448,20 @@
icon,
interactive: false,
keyboard: false,
+ zIndexOffset: 1000,
}).addTo(map);
} else {
locationMarker.setLatLng([latitude, longitude]);
}
- const heading =
+ if (
typeof pos.coords.heading === "number" &&
Number.isFinite(pos.coords.heading)
- ? pos.coords.heading
- : 0;
- const arrow = locationMarker
- .getElement()
- ?.querySelector(".park-locate-arrow");
- if (arrow) arrow.style.transform = `rotate(${heading}deg)`;
+ ) {
+ const arrow = locationMarker
+ .getElement()
+ ?.querySelector(".park-locate-arrow");
+ if (arrow) arrow.style.transform = `rotate(${pos.coords.heading}deg)`;
+ }
if (!accuracyCircle) {
accuracyCircle = L.circle([latitude, longitude], {
radius: Math.min(accuracy, 1000),
@@ -1345,22 +1479,31 @@
if (firstFix) {
map.setView([latitude, longitude], Math.max(map.getZoom(), 15));
firstFix = false;
+ } else if (!userMoved) {
+ map.panTo([latitude, longitude], { animate: true, duration: 0.4 });
}
setStatus(
`現在地を追跡中です(精度 約${Math.round(accuracy)}m)。もう一度押すと停止します。`,
);
};
- const onError = () => {
- setStatus("現在地を取得できませんでした。位置情報の利用を許可してください。");
+ const onError = (err: GeolocationPositionError) => {
+ const message =
+ err.code === err.PERMISSION_DENIED
+ ? "位置情報の利用が許可されていません。ブラウザの設定で許可してください。"
+ : err.code === err.POSITION_UNAVAILABLE
+ ? "現在地を取得できませんでした(位置情報が利用できません)。"
+ : "現在地の取得がタイムアウトしました。";
+ setStatus(message);
stopTracking();
};
if (watchId !== null) navigator.geolocation.clearWatch(watchId);
+ userMoved = false;
tracking = true;
updateLocateButton();
watchId = navigator.geolocation.watchPosition(onSuccess, onError, {
enableHighAccuracy: true,
- timeout: 10000,
- maximumAge: 30000,
+ timeout: 15000,
+ maximumAge: 5000,
});
};
--
cgit v1.3.1