aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ParkMap.astro358
-rw-r--r--src/components/ParkMapHelp.astro7
-rw-r--r--src/components/ParksFilters.astro7
-rw-r--r--src/components/ParksTable.astro81
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) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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>