diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ParkMap.astro | 358 | ||||
| -rw-r--r-- | src/components/ParkMapHelp.astro | 7 | ||||
| -rw-r--r-- | src/components/ParksFilters.astro | 7 | ||||
| -rw-r--r-- | src/components/ParksTable.astro | 81 |
4 files changed, 399 insertions, 54 deletions
diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro index 8e7f35c..b7ac130 100644 --- a/src/components/ParkMap.astro +++ b/src/components/ParkMap.astro @@ -119,6 +119,62 @@ font-size: 0.78rem; cursor: pointer; } + :global(.park-popup-row) { + display: flex; + gap: 0.35rem; + align-items: flex-start; + font-size: 0.78rem; + margin-top: 0.1rem; + } + :global(.park-popup-ico) { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 16px; + height: 16px; + border: 1px solid #c8e6c9; + border-radius: 4px; + background: #e8f5e9; + color: #2e7d32; + font-size: 10px; + font-weight: 700; + line-height: 1; + } + :global(.park-popup-ico svg) { + display: block; + width: 12px; + height: 12px; + } + :global(.park-popup-ico-water) { + border-color: #90caf9; + background: #e3f2fd; + color: #0277bd; + } + :global(.park-popup-ico-bench) { + border-color: #d7ccc8; + background: #efebe9; + color: #6d4c41; + } + :global(.park-popup-ico-pond) { + border-color: #81d4fa; + background: #e1f5fe; + color: #0277bd; + } + :global(.park-popup-meta) { + margin-top: 0.1rem; + color: #777; + font-size: 0.7rem; + } + :global(.park-popup-links a::after) { + content: none !important; + } + :global(.leaflet-popup-content) { + max-height: 50vh; + overflow: auto; + } + :global(.park-popup) { font-size: 0.82rem; line-height: 1.5; @@ -209,7 +265,9 @@ animation: park-selected-pulse 1.2s ease-in-out infinite; } :global(.leaflet-overlay-pane path.park-blink) { - animation: park-blink-pulse 2.6s ease-in-out infinite; + fill: #ffd54f; + stroke: #ff6d00; + animation: park-blink-pulse 2s ease-in-out infinite; } @keyframes park-selected-pulse { 0%, @@ -225,12 +283,12 @@ @keyframes park-blink-pulse { 0%, 100% { - fill-opacity: 0.6; + fill-opacity: 0.85; stroke-opacity: 1; } 50% { - fill-opacity: 0.08; - stroke-opacity: 0.3; + fill-opacity: 0.35; + stroke-opacity: 0.7; } } @media (prefers-reduced-motion: reduce) { @@ -428,8 +486,11 @@ border-radius: 4px; } .swatch-fac-water { - background: #0277bd; + box-sizing: border-box; + border: 1px solid #0277bd; border-radius: 50%; + background: #fff; + color: #0277bd; } :global(.park-fac-markers), :global(.park-fac-list) { @@ -458,9 +519,10 @@ border-radius: 5px; } :global(.park-fac-water) { - background: #0277bd; - border: 1px solid #01579b; + border: 1px solid #0277bd; border-radius: 50%; + background: #fff; + color: #0277bd; font-size: 12px; } :global(.park-fac-clock) { @@ -531,6 +593,12 @@ font-weight: 700; line-height: 1; } + /* アイコンのクリックで公園を選択できるようにする(地図のドラッグは邪魔しない)。 */ + :global(.park-fac-markers .park-fac), + :global(.park-play-markers .park-play) { + pointer-events: auto; + cursor: pointer; + } :global(.park-play-plane) { color: #c62828; } @@ -558,6 +626,12 @@ :global(.park-play-ball) { color: #1b5e20; } + :global(.park-play-special) { + color: #f9a825; + } + :global(.park-play-pond) { + color: #0277bd; + } :global(.park-play-rope) { color: #00838f; } @@ -971,24 +1045,86 @@ value.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch] as string, ); + // 水飲み場アイコン(線画の水滴。ほかの遊具アイコンと同じ線画スタイル)。 + const WATER_DROP_ICON = + '<svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5c3.5 4.2 6 7.4 6 10.3a6 6 0 0 1-12 0c0-2.9 2.5-6.1 6-10.3z"/></svg>'; const buildPopup = (ourName: string, osmName: string, owner = "", mapsQuery = "") => { - const rec = parkDataByName.get(ourName); + // 市外の近隣公園は kodaira-parks.json にないので、近隣データの属性を使う。 + const rec = parkDataByName.get(ourName) || nearbyInfoByName.get(ourName); const name = escapePopup(ourName || osmName || ""); const addr = escapePopup(rec?.["住所"] || ""); - const areaRaw = Number(rec?.["面積㎡"]); + // 近隣データは面積㎡が空で area_m2 に入っていることがある。 + const areaRaw = Number( + String(rec?.["面積㎡"] || "").trim() || rec?.["area_m2"] || 0, + ); const area = Number.isFinite(areaRaw) && areaRaw > 0 ? `${areaRaw.toLocaleString("ja-JP")}㎡` : ""; const type = escapePopup(rec?.["種別"] || ""); - const facs = [ - rec?.["トイレ"] ? "トイレ" : "", - rec?.["水飲み場"] ? "水飲み" : "", - rec?.["遊具"] ? "遊具" : "", - rec?.["シェルター等"] ? "東屋等" : "", - ] - .filter(Boolean) - .join("・"); + // 詳細行の項目名はアイコンで示し、文字量を抑える。 + const DISASTER_KEEP = /かまど|カマド|防災収納|レスキュー|防災器具|避難/; + const disaster = String(rec?.["防災用具"] || "") + .split(/[、,]/) + .map((s) => s.trim()) + .filter((s) => s && !s.includes("防火水槽") && !s.includes("消火栓")) + .filter((s) => DISASTER_KEEP.test(s)) + .join("、"); + const waterRaw = String(rec?.["水飲み場"] || "").trim(); + // 水飲み場は数量ではなく有無だけを示す。 + const water = waterRaw ? (/[??]/.test(waterRaw) ? "あり?" : "あり") : "なし"; + // ボール遊びは、試合形式の可否を括弧書きで添える。 + const ballText = String(rec?.["ボール遊び"] || "").trim(); + const matchText = String(rec?.["試合形式のボール遊び"] || "").trim(); + const ball = + ballText && matchText ? `${ballText}(試合形式${matchText})` : ballText; + // 項目名は1文字の文字アイコンで示す(水・ベンチ・池は絵アイコン)。 + const rowIcons: Record<string, string> = { + 遊具: "遊", + トイレ: "便", + 水飲み場: WATER_DROP_ICON, + "ベンチ・椅子": PARK_ICONS.bench.svg, + テーブル: "台", + "シェルター等": "屋", + 草地広場: "草", + 多目的広場: "広", + 築山: "山", + じゃぶじゃぶ池: PARK_ICONS.pond.svg, + ボール遊び: "球", + ペット: "犬", + 時計: "時", + 防災用具: "災", + }; + const rows = [ + ["遊具", rec?.["遊具"]], + ["トイレ", rec?.["トイレ"]], + ["水飲み場", water], + ["ベンチ・椅子", rec?.["ベンチ・椅子"]], + ["テーブル", rec?.["テーブル"]], + ["シェルター等", rec?.["シェルター等"]], + ["草地広場", rec?.["草地広場"]], + ["多目的広場", rec?.["多目的広場"]], + ["築山", rec?.["築山"]], + ["じゃぶじゃぶ池", rec?.["じゃぶじゃぶ池"]], + ["ボール遊び", ball], + ["ペット", rec?.["ペット"]], + ["時計", rec?.["時計"]], + ["防災用具", disaster], + ].filter(([, value]) => value && String(value).trim()); + const detailHtml = rows + .map(([label, value]) => { + const labelText = String(label); + const icoClass = + labelText === "水飲み場" + ? "park-popup-ico park-popup-ico-water" + : labelText === "ベンチ・椅子" + ? "park-popup-ico park-popup-ico-bench" + : labelText === "じゃぶじゃぶ池" + ? "park-popup-ico park-popup-ico-pond" + : "park-popup-ico"; + return `<div class="park-popup-row" title="${escapePopup(labelText)}"><span class="${icoClass}" role="img" aria-label="${escapePopup(labelText)}">${rowIcons[labelText] || ""}</span><span class="park-popup-val">${escapePopup(String(value))}</span></div>`; + }) + .join(""); const query = mapsQuery || (ourName ? `小平市${ourName}` : ""); const dest = rec?.["緯度"] && rec?.["経度"] @@ -998,7 +1134,7 @@ rec?.["地図URL"] || `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; const route = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(dest)}&travelmode=walking`; - return `<div class="park-popup"><strong>${name}</strong>${owner ? `<span class="park-popup-owner">${escapePopup(owner)}</span>` : ""}${addr || area || type ? `<div class="park-popup-meta">${[addr, area, type].filter(Boolean).join(" ・ ")}</div>` : ""}${facs ? `<div class="park-popup-fac">${facs}</div>` : ""}<div class="park-popup-links"><a href="${maps}" target="_blank" rel="noopener">🗺Gマップ</a><a href="${route}" target="_blank" rel="noopener">🚶経路</a></div></div>`; + return `<div class="park-popup"><strong>${name}</strong>${owner ? `<span class="park-popup-owner">${escapePopup(owner)}</span>` : ""}${addr || area || type ? `<div class="park-popup-meta">${[addr, area, type].filter(Boolean).join(" ・ ")}</div>` : ""}${detailHtml}<div class="park-popup-links"><a href="${maps}" target="_blank" rel="noopener">🌏GMap</a><a href="${route}" target="_blank" rel="noopener">🚶経路</a></div></div>`; }; const filterName = (name: string, scroll = false) => { @@ -1194,7 +1330,7 @@ lyr._isPoint = feature.geometry?.type === "Point"; if (name) nearbyEntries.push({ name, layer: lyr, city, key }); if (name) { - lyr.bindPopup(buildPopup(name, name, "", nearbyMapsQuery(name, city)), { + lyr.bindPopup(buildPopup(name, name, city ? `市外・${city}` : "", nearbyMapsQuery(name, city)), { maxWidth: 300, }); lyr.on("popupopen", () => { @@ -1219,6 +1355,43 @@ }, }).addTo(map); + // 遊具・設備アイコンをクリックしたときも、その公園を選択する。 + const openParkPopup = (name: string) => { + let layer: any = layersByName.get(name); + if (layer && typeof layer.getLayers === "function") { + layer = layer.getLayers()[0] || layer; + } + if (!layer) { + const entry = nearbyEntries.find((e) => e.name === name); + layer = entry?.layer || null; + } + if (!layer) return; + if (typeof layer.openPopup === "function") { + const latlng = layer.getBounds + ? layer.getBounds().getCenter() + : layer.getLatLng?.(); + layer.openPopup(latlng); + } + document.dispatchEvent( + new CustomEvent("park:select-row", { detail: { name } }), + ); + }; + // アイコンのマーカーに公園名を持たせ、クリックでその公園を選択できるようにする。 + // 地図側のクリック処理がフキダシを閉じてしまうので、伝播は止める。 + const markIconOwner = (marker: any, name: string) => { + const bind = () => { + const el = marker.getElement?.(); + if (!el || el.getAttribute("data-park") === name) return; + el.setAttribute("data-park", name); + L.DomEvent.on(el, "click", (e: any) => { + L.DomEvent.stopPropagation(e); + openParkPopup(name); + }); + }; + if (marker.getElement?.()) bind(); + else marker.on("add", bind); + }; + // 地域センター・公民館・図書館・市役所支所・文化施設。 // 市外の近隣施設はオレンジで表示する。 const publicFacilityLayer = L.layerGroup(); @@ -1307,7 +1480,7 @@ } if ((p["水飲み場"] || "").trim()) { icons.push( - '<span class="park-fac park-fac-water" title="水飲み場あり" aria-label="水飲み場あり">🚰</span>', + '<span class="park-fac park-fac-water" title="水飲み場あり" aria-label="水飲み場あり"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5c3.5 4.2 6 7.4 6 10.3a6 6 0 0 1-12 0c0-2.9 2.5-6.1 6-10.3z"/></svg></span>', ); } const clock = (p["時計"] || "").trim(); @@ -1344,6 +1517,7 @@ riseOnHover: false, interactive: false, }).addTo(facilityLayer); + markIconOwner(marker, name); const list = facilityMarkersByName.get(name) || []; list.push(marker); facilityMarkersByName.set(name, list); @@ -1351,17 +1525,73 @@ // 拡大時だけ表示する遊具アイコン。 // 公園ポリゴンの中心に、遊具の種類を絵アイコンで表示する。 + // スイング遊具・健康遊具の判定用(一覧表の facTokens と同じ名前で判定する) + const PLAY_SPRING_TOKENS = [ + "スプリング", + "スイング", + "リトルポップ", + "リンクミニ", + "ルッキンロッキン", + "リーフィー", + "チューリー", + "ロッキンパッピー", + "ロッキンパピー", + "ビー", + "ハッピー", + "ハニー", + "クップル", + "ウゴウゴ", + ]; + const PLAY_HEALTH_TOKENS = [ + "フィットコンビ", + "レグオーバー", + "アームウォーカー", + "ホップオーバー", + "ジャンプ・ステップ", + "立位体前屈", + "懸垂", + "垂直飛び", + "閉眼片足立ち", + "腕歩行", + "リラックスボード", + "パネルラダー", + "棒とびこし", + "脚のばし", + "ふみいたストレッチ", + ]; + const PLAY_STANDARD_TOKENS = [ + "ブランコ", + "ぶらんこ", + "滑台", + "滑り台", + "すべり台", + "砂場", + "鉄棒", + "複合遊具", + "ジャングル", + "ヒコーキ", + "飛行機", + "シーソー", + // 次の遊具は「特殊遊具」扱いにしない(別の分類・設備のため) + "ロープスイング", + "ロープウェイ", + "バスケ", + ...PLAY_SPRING_TOKENS, + ...PLAY_HEALTH_TOKENS, + ]; 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: PLAY_SPRING_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 }, + { tokens: [], key: "pond", title: PARK_ICONS.pond.title, cls: "park-play-pond", icon: PARK_ICONS.pond.svg }, + { tokens: [], key: "special", title: PARK_ICONS.special.title, cls: "park-play-special", icon: PARK_ICONS.special.svg }, ]; const playLegend = document.querySelector<HTMLElement>("[data-play-legend]"); if (playLegend) { @@ -1377,14 +1607,35 @@ const text = (p["遊具"] || "").trim(); const ballText = (p["ボール遊び"] || "").trim(); const hasBall = Boolean(ballText && ballText !== "禁止"); - if (!text && !hasBall) return; + const hasPond = Boolean((p["じゃぶじゃぶ池"] || "").trim()); + if (!text && !hasBall && !hasPond) return; + const segments = text + .split(/[、,]/) + .map((s) => s.trim()) + .filter(Boolean); + // ロープスイングはスイング遊具ではないので spring には含めない。 + const isSpring = segments.some( + (seg) => + !seg.includes("ロープスイング") && + PLAY_SPRING_TOKENS.some((token) => seg.includes(token)), + ); + // 標準的な遊具名を含まない区画は「特殊遊具」とみなす(一覧表と同じ判定)。 + const isSpecial = segments.some( + (seg) => !PLAY_STANDARD_TOKENS.some((token) => seg.includes(token)), + ); 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)); + : def.key === "pond" + ? hasPond + : def.key === "spring" + ? isSpring + : def.key === "special" + ? isSpecial + : def.tokens.some((token) => text.includes(token)); if (!matched) continue; if (seen.has(def.key)) continue; // ヒコーキジャングルはジャングルジムの一種なので、飛があればジは出さない。 @@ -1406,30 +1657,58 @@ riseOnHover: false, interactive: false, }).addTo(playLayer); + markIconOwner(marker, name); const list = playMarkersByName.get(name) || []; list.push(marker); playMarkersByName.set(name, list); }); - // 公園名のラベル(高ズーム時のみ) + // 公園名のラベル(高ズーム時のみ)。 + // 起動時に全件のマーカーを作ると重いので、画面内の公園だけを都度生成する。 const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch] as string, ); - const parkNameLayer = L.layerGroup(); - layersByName.forEach((lyr, name) => { + const parkCenters = Array.from(layersByName, ([name, lyr]) => { const center = centerOfLayer(lyr); - L.marker([center.lat, center.lng], { - icon: L.divIcon({ - className: "park-name-marker", - html: `<span class="park-name-label">${escapeHtml(name)}</span>`, - iconSize: null, - iconAnchor: [0, 0], - }), - interactive: false, - keyboard: false, - }).addTo(parkNameLayer); + return { name, lat: center.lat, lng: center.lng }; }); + const parkNameLayer = L.layerGroup(); + const nameMarkers = new Map<string, any>(); + const updateParkNames = () => { + if (map.getZoom() < 17) { + if (nameMarkers.size) { + nameMarkers.forEach((marker) => parkNameLayer.removeLayer(marker)); + nameMarkers.clear(); + } + if (map.hasLayer(parkNameLayer)) map.removeLayer(parkNameLayer); + return; + } + if (!map.hasLayer(parkNameLayer)) parkNameLayer.addTo(map); + const bounds = map.getBounds().pad(0.1); + const visible = new Set<string>(); + for (const p of parkCenters) { + if (!bounds.contains([p.lat, p.lng])) continue; + visible.add(p.name); + if (nameMarkers.has(p.name)) continue; + const marker = L.marker([p.lat, p.lng], { + icon: L.divIcon({ + className: "park-name-marker", + html: `<span class="park-name-label">${escapeHtml(p.name)}</span>`, + iconSize: null, + iconAnchor: [0, 0], + }), + interactive: false, + keyboard: false, + }).addTo(parkNameLayer); + nameMarkers.set(p.name, marker); + } + nameMarkers.forEach((marker, name) => { + if (visible.has(name)) return; + parkNameLayer.removeLayer(marker); + nameMarkers.delete(name); + }); + }; // フィルタ中は、該当しない公園の設備・遊具アイコンを隠す const applyIconFilter = (names: Set<string> | null) => { @@ -1459,11 +1738,6 @@ } else if (map.hasLayer(playLayer)) { map.removeLayer(playLayer); } - if (map.getZoom() >= 17) { - if (!map.hasLayer(parkNameLayer)) parkNameLayer.addTo(map); - } else if (map.hasLayer(parkNameLayer)) { - map.removeLayer(parkNameLayer); - } // 公共施設の名前はもう一段ズームしたときだけ表示する(切替時のみ実行) const showFacilityLabels = map.getZoom() >= 15; if (showFacilityLabels !== facilityLabelsShown) { @@ -1476,6 +1750,8 @@ }; map.on("zoomend", updateFacilities); updateFacilities(); + map.on("moveend", updateParkNames); + updateParkNames(); // フィルタがないときは、すべての公園を点滅させる allLayers.forEach((lyr) => { diff --git a/src/components/ParkMapHelp.astro b/src/components/ParkMapHelp.astro index e45b87f..4402567 100644 --- a/src/components/ParkMapHelp.astro +++ b/src/components/ParkMapHelp.astro @@ -14,7 +14,7 @@ <span><i class="swatch swatch-facility-nearby"></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">🚰</i>水飲み場(拡大時)</span> + <span><i class="swatch-fac swatch-fac-water"><svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5c3.5 4.2 6 7.4 6 10.3a6 6 0 0 1-12 0c0-2.9 2.5-6.1 6-10.3z"/></svg></i>水飲み場(拡大時)</span> <span><i class="swatch-fac swatch-fac-clock"><svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 7v5l3 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></i>時計(拡大時)</span> <span><i class="swatch-fac swatch-fac-hydrant">消</i>消火栓(拡大時)</span> <span><i class="swatch-fac swatch-fac-tank">槽</i>防火水槽(拡大時)</span> @@ -96,8 +96,11 @@ border-radius: 4px; } .swatch-fac-water { - background: #0277bd; + box-sizing: border-box; + border: 1px solid #0277bd; border-radius: 50%; + background: #fff; + color: #0277bd; } .swatch-fac-clock { background: #f9a825; diff --git a/src/components/ParksFilters.astro b/src/components/ParksFilters.astro index 4890741..5675eb9 100644 --- a/src/components/ParksFilters.astro +++ b/src/components/ParksFilters.astro @@ -59,7 +59,7 @@ const facilityTypes = [ <label><input type="checkbox" data-fac-filter value="water" />水飲み</label> <label> <input type="checkbox" data-fac-filter value="picnic" /> - <span class="filter-icon" set:html={PARK_ICONS.picnic.svg} />緑地広場 + <span class="filter-icon filter-icon-grass" set:html={PARK_ICONS.grass.svg} />草地広場 </label> <label><input type="checkbox" data-fac-filter value="clock" />⏰時計</label> <label> @@ -97,7 +97,7 @@ const facilityTypes = [ <label><input type="checkbox" data-fac-filter value="basket" />🏀バスケゴール</label> <label><input type="checkbox" data-fac-filter value="shelter" />⛱東屋/シェルター</label> <label><input type="checkbox" data-fac-filter value="health" />💪健康遊具</label> - <label><input type="checkbox" data-fac-filter value="special" />✨特殊遊具</label> + <label><input type="checkbox" data-fac-filter value="special" /><span class="filter-icon" set:html={PARK_ICONS.special.svg} />特殊遊具</label> </fieldset> <label class="parks-fire-toggle" title="消火栓・防火水槽を地図に表示する"> <input type="checkbox" data-show-fire /> @@ -189,6 +189,9 @@ const facilityTypes = [ height: 18px; flex: none; } + .filter-icon-grass { + color: #2e7d32; + } .filter-icon :global(svg) { display: block; width: 100%; diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro index 4918b2c..f85fea3 100644 --- a/src/components/ParksTable.astro +++ b/src/components/ParksTable.astro @@ -106,7 +106,7 @@ function facilityBadges(p: Park): Badge[] { // スイング遊具・健康遊具の判定用 const SPRING_TOKENS = [ "スプリング", "スイング", "リトルポップ", "リンクミニ", "ルッキンロッキン", - "リーフィー", "チューリー", "ロッキンパッピー", "ビー", "ハッピー", + "リーフィー", "チューリー", "ロッキンパッピー", "ロッキンパピー", "ビー", "ハッピー", "ハニー", "クップル", "ウゴウゴ", ]; const HEALTH_TOKENS = [ @@ -188,17 +188,27 @@ const DETAIL_FIELDS: { key: string; icon: string }[] = [ { key: "多目的広場", icon: "🏟" }, { key: "築山", icon: "⛰" }, { key: "じゃぶじゃぶ池", icon: "💦" }, - { key: "防災用具", icon: "🧯" }, + { key: "防災用具", icon: "🛟" }, { key: "遊具", icon: "🎠" }, { key: "時計", icon: "⏰" }, { key: "駐車場", icon: "🅿" }, { key: "その他設備", icon: "🔧" }, { key: "備考", icon: "" }, ]; +function disasterText(value: string): string { + return value + .split(/[、,]/) + .map((s) => s.trim()) + .filter((s) => s && !s.includes("防火水槽") && !s.includes("消火栓")) + .filter((s) => /かまど|カマド|防災収納|レスキュー|防災器具|避難/.test(s)) + .join("、"); +} function detailText(p: Park): string { const parts: string[] = []; for (const { key, icon } of DETAIL_FIELDS) { - const value = p[key]; + const raw = p[key]; + if (!raw) continue; + const value = key === "防災用具" ? disasterText(raw) : raw; if (!value) continue; parts.push(icon ? `${icon} ${value}` : value); } @@ -705,7 +715,7 @@ function isUnconfirmed(p: Park): boolean { content: "🚻"; } .parks .fac-water::before { - content: "🚰"; + content: "💧"; } .parks .fac-toy::before { content: "🎠"; @@ -729,14 +739,67 @@ function isUnconfirmed(p: Park): boolean { opacity: 0.85; } - /* スマートフォンでも表形式を維持し、横スクロールで見られるようにする。 */ - @media (max-width: 640px) { + /* スマートフォンでは表をカード状に組み替え、横スクロールなしで読めるようにする。 */ + @media (max-width: 700px) { .parks table { - font-size: 0.75rem; + display: block; + width: 100%; + min-width: 0; + font-size: 0.8rem; + } + .parks thead { + display: none; + } + .parks tbody, + .parks tbody tr, + .parks td { + display: block; + } + .parks tbody tr { + display: grid; + grid-template-columns: 1fr auto auto; + grid-template-areas: + "name name name" + "addr area type" + "fac fac fac"; + gap: 0.15rem 0.5rem; + padding: 0.45rem 0.55rem; + border-bottom: 1px solid var(--sl-color-hairline, #ddd); + } + .parks tbody tr:nth-child(even) td { + background: transparent; + } + .parks tbody tr:nth-child(even) { + background: color-mix(in srgb, var(--sl-color-text, #333) 5%, transparent); } - .parks th, .parks td { - padding: 0.3rem 0.4rem; + border: 0; + padding: 0; + white-space: normal; + overflow-wrap: anywhere; + } + .parks .is-name { + grid-area: name; + } + .parks .is-address { + grid-area: addr; + min-width: 0; + } + .parks .is-num { + grid-area: area; + text-align: left; + } + .parks .is-type { + grid-area: type; + text-align: left; + } + .parks .is-facility { + grid-area: fac; + min-width: 0; + max-width: none; + } + .parks tbody tr.is-selected td:first-child { + box-shadow: none; } } </style> |
