diff options
| -rw-r--r-- | src/components/ParkMap.astro | 177 |
1 files 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 @@ '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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) => + `絞り込み <span aria-hidden="true">${open ? "▴" : "▾"}</span>`; + 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<HTMLElement>(".parks-controls"); + const disclaimer = document.querySelector<HTMLElement>(".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 @@ '<span class="park-map-selected-name"></span>' + '<span class="park-map-selected-owner" hidden></span>' + '<span class="park-map-selected-note" hidden>一覧にない緑地・公園</span>' + - '<a data-park-maps-link href="#" target="_blank" rel="noopener">Googleマップ</a>' + + '<a data-park-maps-link href="#" target="_blank" rel="noopener">現在地から経路</a>' + '<a data-park-table-link href="#">一覧で見る</a>'; selectedBar = box; selectedNameEl = box.querySelector<HTMLElement>(".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<HTMLElement>(".park-locate-arrow"); - if (arrow) arrow.style.transform = `rotate(${heading}deg)`; + ) { + const arrow = locationMarker + .getElement() + ?.querySelector<HTMLElement>(".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, }); }; |
