aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/ParkMap.astro
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-25 12:24:24 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-25 12:24:24 +0900
commita01062f43d36112c00d5c007ac9d488c14d8fb0d (patch)
treeb99ddac9928e02b9cd07d372fa42842a348bfb61 /src/components/ParkMap.astro
parenta71727538f65ab4f10f5239550be82590f9d47d1 (diff)
parks: アイコン・フィルタ・編集画面を更新し、公開を一時停止
- 遊具アイコンをSVGRepo素材に差し替え、施設種別をアイコン表示 - フィルタに種類・遊具・ピクニックを追加し、適用時の地図移動を停止 - 現在地ボタンを追跡トグルに変更し、初回以外は地図を動かさない - 編集画面を市内公園・周辺公園・公共施設の4タブに拡張 - 公園ページをドラフト化し、サイドバーとPWAの公開を停止
Diffstat (limited to 'src/components/ParkMap.astro')
-rw-r--r--src/components/ParkMap.astro446
1 files changed, 362 insertions, 84 deletions
diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro
index 1c75bee..e4f226b 100644
--- a/src/components/ParkMap.astro
+++ b/src/components/ParkMap.astro
@@ -10,22 +10,6 @@
role="region"
aria-label="小平市の公園の位置(OpenStreetMap)"
></div>
- <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">
- 公園の範囲を赤で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園と公共施設は別レイヤーで表示し、右上のレイヤ切替でON/OFFできます。トイレと水飲み場のアイコンは、地図を拡大すると表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © <a
- href="https://www.openstreetmap.org/copyright"
- target="_blank"
- rel="noopener">OpenStreetMap</a
- > contributors
- </p>
<p class="park-map-locate-status" data-locate-status hidden></p>
</div>
@@ -94,6 +78,10 @@
background: #8e24aa;
border-radius: 50%;
}
+ .swatch-facility-nearby {
+ background: #fb8c00;
+ border-radius: 50%;
+ }
.swatch-boundary {
height: 0;
border-top: 4px solid #6a1b9a;
@@ -106,18 +94,46 @@
font-size: 0.8rem;
}
:global(.park-locate) {
- width: 34px;
- height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ padding: 0;
border: 0;
+ border-radius: 50%;
background: #fff;
color: #1e88e5;
- font-size: 20px;
- line-height: 1;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
cursor: pointer;
}
:global(.park-locate:hover) {
background: #f0f7ff;
}
+ :global(.park-locate.is-tracking) {
+ background: #1e88e5;
+ color: #fff;
+ animation: park-locate-pulse 1.8s infinite;
+ }
+ :global(.park-locate.is-tracking:hover) {
+ background: #1565c0;
+ }
+ @keyframes park-locate-pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(30, 136, 229, 0.45);
+ }
+ 70% {
+ box-shadow: 0 0 0 9px rgba(30, 136, 229, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(30, 136, 229, 0);
+ }
+ }
+ :global(.park-locate svg) {
+ display: block;
+ width: 20px;
+ height: 20px;
+ }
.park-map-note {
margin-top: 0.4rem;
color: var(--sl-color-gray-2);
@@ -218,6 +234,122 @@
:global(.park-fac svg) {
display: block;
}
+ :global(.park-facility-marker) {
+ background: none;
+ border: 0;
+ }
+ :global(.park-facility-emoji) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 22px;
+ height: 22px;
+ border: 2px solid #8e24aa;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ font-size: 13px;
+ line-height: 1;
+ }
+ :global(.park-facility-marker.is-nearby .park-facility-emoji) {
+ border-color: #fb8c00;
+ }
+ :global(.park-play-markers),
+ :global(.park-play-list) {
+ pointer-events: none;
+ }
+ :global(.park-play-list) {
+ display: inline-flex;
+ gap: 2px;
+ transform: translate(-50%, -50%) translateY(-24px);
+ }
+ :global(.park-play) {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: 1px solid currentColor;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 1;
+ }
+ :global(.park-play-plane) {
+ color: #c62828;
+ }
+ :global(.park-play-swing) {
+ color: #ef6c00;
+ }
+ :global(.park-play-seesaw) {
+ color: #6d4c41;
+ }
+ :global(.park-play-spring) {
+ color: #d81b60;
+ }
+ :global(.park-play-slide) {
+ color: #2e7d32;
+ }
+ :global(.park-play-jungle) {
+ color: #6a1b9a;
+ }
+ :global(.park-play-sand) {
+ color: #3e2723;
+ }
+ :global(.park-play-sand svg *) {
+ vector-effect: non-scaling-stroke;
+ }
+ :global(.park-play-complex) {
+ color: #0277bd;
+ }
+ :global(.park-play-ball) {
+ color: #1b5e20;
+ }
+ :global(.park-play-rope) {
+ color: #00838f;
+ }
+ :global(.park-locate-marker) {
+ border: 0;
+ background: none;
+ }
+ :global(.park-locate-arrow) {
+ display: block;
+ transform-origin: 50% 50%;
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
+ }
+ .park-map-help {
+ margin-top: 0.4rem;
+ }
+ .park-map-help summary {
+ cursor: pointer;
+ color: var(--sl-color-text-accent);
+ font-size: 0.85rem;
+ }
+ .park-play-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 0.8rem;
+ margin: 0.5rem 0 0;
+ color: var(--sl-color-gray-2);
+ font-size: 0.78rem;
+ }
+ .park-play-legend-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ }
+ :global(.park-play svg) {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
+ :global(.park-play-swing svg) {
+ width: 70%;
+ height: 70%;
+ }
/* StarlightのMarkdown余白ルールがLeafletの各ペインに margin-top:1rem を
付けて地図を下へずらすため、地図内のペインは余白なしにする。 */
.park-map :global(.leaflet-pane) {
@@ -252,6 +384,8 @@
import boundary from "@/data/kodaira-boundary.json";
import nearbyData from "@/data/kodaira-nearby-parks.json";
import facilities from "@/data/kodaira-facilities.json";
+ import nearbyFacilities from "@/data/kodaira-nearby-facilities.json";
+ import { PARK_ICONS } from "@/data/park-icons";
import parks from "@/data/kodaira-parks.json";
const MATCHED_STYLE = {
@@ -285,6 +419,13 @@
fillColor: "#42a5f5",
fillOpacity: 0.25,
};
+ const NEARBY_POINT_STYLE = {
+ radius: 7,
+ color: "#0d47a1",
+ weight: 2,
+ fillColor: "#42a5f5",
+ fillOpacity: 0.85,
+ };
const NEARBY_SELECTED_STYLE = {
color: "#0d47a1",
weight: 3,
@@ -365,7 +506,9 @@
if (name) nearbyInfoByName.set(name, p);
});
const nearbyMapsQuery = (name: string, city = "") =>
- name ? (city ? `${city}${name}` : name) : "";
+ name ? (city && !name.includes(city) ? `${city}${name}` : name) : "";
+ const nearbyBaseStyle = (lyr: any) =>
+ lyr._isPoint ? NEARBY_POINT_STYLE : NEARBY_STYLE;
const parkDataByName = new Map<string, any>();
(parks as any[]).forEach((p) => parkDataByName.set(p["公園名"], p));
const ownerByName = new Map<string, string>();
@@ -536,12 +679,17 @@
// 市外の近い公園(500m以内)は別レイヤーにする。
// 小平市内の公園と同じく、クリックで一覧を絞り込み、左下に情報を表示する。
const nearbyLayer = L.geoJSON(nearbyData as any, {
- style: NEARBY_STYLE,
+ // 範囲が取得できない公園は点(丸)で表示する。
+ pointToLayer: (_feature: any, latlng: any) =>
+ L.circleMarker(latlng, NEARBY_POINT_STYLE),
+ style: (feature: any) =>
+ feature?.geometry?.type === "Point" ? NEARBY_POINT_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 || ""}`;
+ lyr._isPoint = feature.geometry?.type === "Point";
if (name) nearbyEntries.push({ name, layer: lyr, city, key });
lyr.on("click", (ev: any) => {
if (ev?.originalEvent) L.DomEvent.stopPropagation(ev.originalEvent);
@@ -552,35 +700,51 @@
},
}).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);
- });
+ const facilityMarkers: { name: string; layer: any }[] = [];
+ const FACILITY_ICON: Record<string, string> = {
+ 地域センター: "📍",
+ 公民館: "🏛",
+ 児童館: "🧒",
+ 図書館: "📚",
+ 市役所支所: "🏢",
+ 文化施設: "🎭",
+ };
+ const setFacilityOpacity = (layer: any, show: boolean) => {
+ if (typeof layer.setOpacity === "function") {
+ layer.setOpacity(show ? 1 : 0.2);
+ } else if (typeof layer.setStyle === "function") {
+ layer.setStyle({
+ opacity: show ? 1 : 0.15,
+ fillOpacity: show ? 0.85 : 0.1,
+ });
+ }
+ };
+ const addFacilities = (list: any[], nearby: boolean) => {
+ list.forEach((f) => {
+ if (f.lat == null || f.lon == null) return;
+ const icon = FACILITY_ICON[f.type] || "";
+ const marker = L.marker([f.lat, f.lon], {
+ icon: L.divIcon({
+ className: `park-facility-marker${nearby ? " is-nearby" : ""}`,
+ html: `<span class="park-facility-emoji">${icon || "🏢"}</span>`,
+ iconSize: [22, 22],
+ iconAnchor: [11, 11],
+ }),
+ })
+ .bindPopup(
+ `<strong>${icon} ${f.name}</strong>${nearby ? "(市外)" : ""}<br>${f.type}<br>${f.hours || ""}${f.closed ? `<br>休: ${f.closed}` : ""}<br>トイレ: ${f.toilet || "未確認"} / 給水: ${f.water || "未確認"}`,
+ )
+ .addTo(publicFacilityLayer);
+ facilityMarkers.push({ name: String(f.name || ""), layer: marker });
+ });
+ };
+ addFacilities(facilities as any[], false);
+ addFacilities(nearbyFacilities as any[], true);
publicFacilityLayer.addTo(map);
- L.control
- .layers(
- null,
- {
- "市外の近い公園(500m以内)": nearbyLayer,
- "公共施設": publicFacilityLayer,
- },
- { collapsed: false, position: "topright" },
- )
- .addTo(map);
-
// 拡大時だけ表示する設備アイコン(トイレ・水飲み場)
// 一覧と一致した公園のポリゴン内にだけ表示する。
const facilityLayer = L.layerGroup();
@@ -612,12 +776,76 @@
}).addTo(facilityLayer);
});
+ // 拡大時だけ表示する遊具アイコン。
+ // 公園ポリゴンの中心に、遊具の種類を絵アイコンで表示する。
+ const PLAY_DEFS = [
+ { tokens: ["ヒコーキ", "飛行機"], key: "airplane", title: PARK_ICONS.airplane.title, cls: "park-play-plane", icon: PARK_ICONS.airplane.svg },
+ { tokens: ["ジャングル"], key: "climbing", title: PARK_ICONS.climbing.title, cls: "park-play-jungle", icon: PARK_ICONS.climbing.svg },
+ { tokens: ["ブランコ"], key: "swing", title: PARK_ICONS.swing.title, cls: "park-play-swing", icon: PARK_ICONS.swing.svg },
+ { tokens: ["シーソー"], key: "seesaw", title: PARK_ICONS.teeter.title, cls: "park-play-seesaw", icon: PARK_ICONS.teeter.svg },
+ { tokens: ["スプリング"], key: "spring", title: PARK_ICONS.spring.title, cls: "park-play-spring", icon: PARK_ICONS.spring.svg },
+ { tokens: ["滑り台", "滑台"], key: "slide", title: PARK_ICONS.slide.title, cls: "park-play-slide", icon: PARK_ICONS.slide.svg },
+ { tokens: ["ロープウェイ"], key: "flyingfox", title: PARK_ICONS.flyingfox.title, cls: "park-play-rope", icon: PARK_ICONS.flyingfox.svg },
+ { tokens: ["砂場"], key: "sand", title: PARK_ICONS.sand.title, cls: "park-play-sand", icon: PARK_ICONS.sand.svg },
+ { tokens: ["複合遊具"], key: "playground", title: PARK_ICONS.playground.title, cls: "park-play-complex", icon: PARK_ICONS.playground.svg },
+ { tokens: [], key: "ball", title: PARK_ICONS.ball.title, cls: "park-play-ball", icon: PARK_ICONS.ball.svg },
+ ];
+ const playLegend = document.querySelector<HTMLElement>("[data-play-legend]");
+ if (playLegend) {
+ playLegend.innerHTML = PLAY_DEFS.map(
+ (def) =>
+ `<span class="park-play-legend-item"><span class="park-play ${def.cls}" aria-hidden="true">${def.icon}</span>${def.title}</span>`,
+ ).join("");
+ }
+ const playLayer = L.layerGroup();
+ layersByName.forEach((lyr, name) => {
+ const p = parkDataByName.get(name);
+ if (!p) return;
+ const text = (p["遊具"] || "").trim();
+ const ballText = (p["ボール遊び"] || "").trim();
+ const hasBall = Boolean(ballText && ballText !== "禁止");
+ if (!text && !hasBall) return;
+ const seen = new Set<string>();
+ const icons: string[] = [];
+ for (const def of PLAY_DEFS) {
+ const matched =
+ def.key === "ball"
+ ? hasBall
+ : def.tokens.some((token) => text.includes(token));
+ if (!matched) continue;
+ if (seen.has(def.key)) continue;
+ // ヒコーキジャングルはジャングルジムの一種なので、飛があればジは出さない。
+ if (def.key === "climbing" && seen.has("airplane")) continue;
+ seen.add(def.key);
+ icons.push(
+ `<span class="park-play ${def.cls}" title="${def.title}" aria-label="${def.title}">${def.icon}</span>`,
+ );
+ }
+ if (!icons.length) return;
+ const center = lyr.getBounds().getCenter();
+ L.marker([center.lat, center.lng], {
+ icon: L.divIcon({
+ className: "park-play-markers",
+ html: `<span class="park-play-list">${icons.join("")}</span>`,
+ iconSize: null,
+ iconAnchor: [0, 0],
+ }),
+ riseOnHover: false,
+ interactive: false,
+ }).addTo(playLayer);
+ });
+
const updateFacilities = () => {
if (map.getZoom() >= 16) {
if (!map.hasLayer(facilityLayer)) facilityLayer.addTo(map);
} else if (map.hasLayer(facilityLayer)) {
map.removeLayer(facilityLayer);
}
+ if (map.getZoom() >= 17) {
+ if (!map.hasLayer(playLayer)) playLayer.addTo(map);
+ } else if (map.hasLayer(playLayer)) {
+ map.removeLayer(playLayer);
+ }
};
map.on("zoomend", updateFacilities);
updateFacilities();
@@ -646,7 +874,8 @@
if (key) {
const nearbyHit = nearbyEntries.find((entry) => entry.key === key);
if (nearbyHit) {
- map.fitBounds(nearbyHit.layer.getBounds(), { padding: [40, 40], maxZoom: 17 });
+ const group = L.featureGroup([nearbyHit.layer]);
+ map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 17 });
showSelected(
name,
name,
@@ -702,7 +931,6 @@
const names = new Set<string>(detail.names || []);
const active = Boolean(detail.active);
const fromMap = Boolean(detail.fromMap);
- const visible: any[] = [];
if (!fromMap && selectedBar) selectedBar.hidden = true;
@@ -718,7 +946,12 @@
}
});
nearbyEntries.forEach(({ name, layer: lyr }) => {
- lyr.setStyle(name && names.has(name) ? NEARBY_SELECTED_STYLE : NEARBY_STYLE);
+ lyr.setStyle(
+ name && names.has(name) ? NEARBY_SELECTED_STYLE : nearbyBaseStyle(lyr),
+ );
+ });
+ facilityMarkers.forEach(({ layer }) => {
+ setFacilityOpacity(layer, true);
});
return;
}
@@ -731,7 +964,6 @@
}
if (name && names.has(name)) {
lyr.setStyle(MATCHED_STYLE);
- visible.push(lyr);
} else {
lyr.setStyle(DIM_STYLE);
}
@@ -739,26 +971,23 @@
nearbyEntries.forEach(({ name, layer: lyr }) => {
if (!active) {
- lyr.setStyle(NEARBY_STYLE);
+ lyr.setStyle(nearbyBaseStyle(lyr));
return;
}
if (name && names.has(name)) {
- lyr.setStyle(NEARBY_STYLE);
- visible.push(lyr);
+ lyr.setStyle(nearbyBaseStyle(lyr));
} else {
lyr.setStyle(NEARBY_DIM_STYLE);
}
});
- if (active && visible.length && visible.length < 30) {
- const bounds = L.featureGroup(visible).getBounds();
- if (bounds.isValid()) {
- map.fitBounds(bounds, { padding: [40, 40], maxZoom: 16 });
- }
- }
+ // 公共施設のマーカーも絞り込みに合わせて表示する。
+ facilityMarkers.forEach(({ name, layer }) => {
+ setFacilityOpacity(layer, !active || names.has(name));
+ });
});
- // 現在地表示(スマホでは自動で取得を試みる)
+ // 現在地表示(ボタン操作時のみ取得)
const statusEl = document.querySelector<HTMLElement>("[data-locate-status]");
const setStatus = (msg: string) => {
if (!statusEl) return;
@@ -769,30 +998,72 @@
let locationMarker: any = null;
let accuracyCircle: any = null;
let watchId: number | null = null;
+ let tracking = false;
+ let firstFix = true;
+ let locateButton: HTMLButtonElement | null = null;
+
+ const LOCATE_ICON =
+ '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M12 2.2 20 21.5 12 16.8 4 21.5Z" fill="#1e88e5" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/></svg></span>';
+ const TRACKING_ICON =
+ '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="3.5" fill="currentColor"/><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>';
+
+ const updateLocateButton = () => {
+ if (!locateButton) return;
+ locateButton.classList.toggle("is-tracking", tracking);
+ locateButton.setAttribute("aria-pressed", tracking ? "true" : "false");
+ const label = tracking ? "現在地の追跡を停止" : "現在地を表示";
+ locateButton.title = label;
+ locateButton.setAttribute("aria-label", label);
+ locateButton.innerHTML = tracking ? TRACKING_ICON : LOCATE_ICON;
+ };
+
+ const stopTracking = () => {
+ if (watchId !== null) {
+ navigator.geolocation.clearWatch(watchId);
+ watchId = null;
+ }
+ tracking = false;
+ updateLocateButton();
+ };
- const locate = (center = false) => {
+ const locate = () => {
+ if (tracking) {
+ stopTracking();
+ setStatus("現在地の追跡を停止しました。");
+ return;
+ }
if (!navigator.geolocation) {
setStatus("このブラウザでは現在地を取得できません。");
return;
}
setStatus("現在地を取得しています…");
+ firstFix = true;
const onSuccess = (pos: GeolocationPosition) => {
const { latitude, longitude, accuracy } = pos.coords;
if (!locationMarker) {
- locationMarker = L.circleMarker([latitude, longitude], {
- radius: 7,
- color: "#ffffff",
- weight: 2,
- fillColor: "#1e88e5",
- fillOpacity: 1,
- }).bindPopup("現在地", { autoPan: false });
- locationMarker.on("add", () => {
- locationMarker.getElement()?.setAttribute("data-locate", "true");
+ const icon = L.divIcon({
+ className: "park-locate-marker",
+ html: '<span class="park-locate-arrow"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path d="M12 2.2 20 21.5 12 16.8 4 21.5Z" fill="#1e88e5" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/></svg></span>',
+ iconSize: [24, 24],
+ iconAnchor: [12, 12],
});
- locationMarker.addTo(map);
+ locationMarker = L.marker([latitude, longitude], {
+ icon,
+ interactive: false,
+ keyboard: false,
+ }).addTo(map);
} else {
locationMarker.setLatLng([latitude, longitude]);
}
+ const heading =
+ 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)`;
if (!accuracyCircle) {
accuracyCircle = L.circle([latitude, longitude], {
radius: Math.min(accuracy, 1000),
@@ -807,13 +1078,21 @@
.setLatLng([latitude, longitude])
.setRadius(Math.min(accuracy, 1000));
}
- setStatus(`現在地を表示しています(精度 約${Math.round(accuracy)}m)。`);
- if (center) map.setView([latitude, longitude], Math.max(map.getZoom(), 15));
+ if (firstFix) {
+ map.setView([latitude, longitude], Math.max(map.getZoom(), 15));
+ firstFix = false;
+ }
+ setStatus(
+ `現在地を追跡中です(精度 約${Math.round(accuracy)}m)。もう一度押すと停止します。`,
+ );
};
const onError = () => {
setStatus("現在地を取得できませんでした。位置情報の利用を許可してください。");
+ stopTracking();
};
if (watchId !== null) navigator.geolocation.clearWatch(watchId);
+ tracking = true;
+ updateLocateButton();
watchId = navigator.geolocation.watchPosition(onSuccess, onError, {
enableHighAccuracy: true,
timeout: 10000,
@@ -823,23 +1102,22 @@
const LocateControl = L.Control.extend({
onAdd: () => {
- const btn = L.DomUtil.create("button", "leaflet-bar park-locate");
+ const btn = L.DomUtil.create("button", "park-locate") as HTMLButtonElement;
btn.setAttribute("type", "button");
- btn.title = "現在地を表示";
- btn.setAttribute("aria-label", "現在地を表示");
- btn.innerHTML = '<span aria-hidden="true">◎</span>';
+ locateButton = btn;
L.DomEvent.disableClickPropagation(btn);
- btn.addEventListener("click", () => locate(true));
+ btn.addEventListener("click", () => locate());
+ updateLocateButton();
return btn;
},
});
new LocateControl({ position: "topright" }).addTo(map);
- if (window.matchMedia("(max-width: 640px)").matches) {
- locate(true);
- }
+ // 位置情報の許可確認は、ボタン操作(ユーザー操作)のときだけ出す。
+ // ページ読み込み時に自動で watchPosition を呼ぶと、ブラウザによっては
+ // 許可ダイアログが出ずに失敗することがあるため。
window.addEventListener("pagehide", () => {
- if (watchId !== null) navigator.geolocation.clearWatch(watchId);
+ stopTracking();
});
// レイアウト確定後にサイズを再計算し、タイルを再配置する。