--- // 小平市の公園・周辺の公園・公共施設の一覧(/kodaira-data/parks/)を表示する。 // データは src/data/parks-rows.ts で組み立てる。 // DOM を抑えるため、設備は1セルにまとめ、アイコンは CSS の擬似要素で描く。 // 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 import { parks, mappedNames, type Park } from "@/data/parks-rows"; import { PARK_ICONS } from "@/data/park-icons"; const EMPTY = "—"; // 種別の表示(色付きの丸+区分名。正式名は title / aria-label で補う)。 const TYPE_LABEL: Record = { 街区: "街区公園", 近隣: "近隣公園", 運動: "運動公園", 広域: "広域公園", 広場: "広場", 緑地: "緑地", 団地内公園: "団地内公園", 児童遊園: "児童遊園", 市外: "市外の近隣公園", 地域センター: "地域センター", 公民館: "公民館", 児童館: "児童館", 図書館: "図書館", 市役所支所: "市役所支所", 文化施設: "文化施設", }; const TYPE_CLASS: Record = { 街区: "parks-type--block", 近隣: "parks-type--neighbor", 運動: "parks-type--sports", 広域: "parks-type--wide", 広場: "parks-type--open", 緑地: "parks-type--green", 団地内公園: "parks-type--danchi", 児童遊園: "parks-type--jidou", 市外: "parks-type--green", 地域センター: "parks-type--facility", 公民館: "parks-type--facility", 児童館: "parks-type--facility", 図書館: "parks-type--facility", 市役所支所: "parks-type--facility", 文化施設: "parks-type--facility", }; // 種別は列幅を小さくするため、漢字1文字の記号で表示する。 const TYPE_MARK: Record = { 街区: "街", 近隣: "近", 運動: "運", 広域: "広", 広場: "場", 緑地: "緑", 団地内公園: "団", 児童遊園: "児", 市外: "外", 地域センター: "📍", 公民館: "🏛", 児童館: "🧒", 図書館: "📚", 市役所支所: "🏢", 文化施設: "🎭", }; // 市立以外の公園に付けるバッジ(設置者が空欄なら市立として扱う)。 function ownerLabel(owner: string): string { if (owner.includes("都営")) return "都営"; if (owner === "東京都") return "都立"; if (owner.includes("UR")) return "UR"; if (owner.includes("民間")) return "民間"; return owner; } function ownerClass(owner: string): string { if (owner.includes("都営")) return "parks-owner--toei"; if (owner === "東京都") return "parks-owner--tokyo"; if (owner === "未確認") return "parks-owner--unknown"; return "parks-owner--other"; } // スイング遊具・健康遊具の判定用 const SPRING_TOKENS = [ "スプリング", "スイング", "リトルポップ", "リンクミニ", "ルッキンロッキン", "リーフィー", "チューリー", "ロッキンパッピー", "ロッキンパピー", "ビー", "ハッピー", "ハニー", "クップル", "ウゴウゴ", ]; const HEALTH_TOKENS = [ "フィットコンビ", "レグオーバー", "アームウォーカー", "ホップオーバー", "ジャンプ・ステップ", "立位体前屈", "懸垂", "垂直飛び", "閉眼片足立ち", "腕歩行", "リラックスボード", "パネルラダー", "棒とびこし", "脚のばし", "ふみいたストレッチ", ]; const STANDARD_PLAY_TOKENS = [ "ブランコ", "ぶらんこ", "滑台", "滑り台", "すべり台", "砂場", "鉄棒", "複合遊具", "ジャングル", "ヒコーキ", "飛行機", "シーソー", ...SPRING_TOKENS, ...HEALTH_TOKENS, ]; function playSegments(play: string): string[] { return play.split(/[、,]/).map((s) => s.trim()).filter(Boolean); } function hasSpecialToy(play: string): boolean { return playSegments(play).some( (segment) => !STANDARD_PLAY_TOKENS.some((token) => segment.includes(token)), ); } // 設備フィルタ用のトークン(data-fac)。 function facTokens(p: Park): string { const tokens: string[] = []; if (p["トイレ"]) tokens.push("toilet"); if (p["水飲み場"]) tokens.push("water"); if (p["給水"]) tokens.push("water"); if (p["遊具"]) tokens.push("toy"); const play = p["遊具"] || ""; const segments = playSegments(play); if (play.includes("砂場")) tokens.push("sand"); if (play.includes("ブランコ")) tokens.push("swing"); if (play.includes("シーソー")) tokens.push("seesaw"); if ( segments.some( (segment) => !segment.includes("ロープスイング") && SPRING_TOKENS.some((token) => segment.includes(token)), ) ) { tokens.push("spring"); } if (play.includes("ジャングル")) tokens.push("jungle"); if (play.includes("ヒコーキ") || play.includes("飛行機")) tokens.push("plane"); if (play.includes("滑り台") || play.includes("滑台")) tokens.push("slide"); if (play.includes("複合遊具")) tokens.push("complex"); if (play.includes("バスケ")) tokens.push("basket"); // シェルター等の欄には、シェルター・東屋・パーゴラ・GTシェードが混在している。 // 「東屋/シェルター」と「パーゴラ」を別々に絞れるよう、キーワードで分ける。 const shelterText = (p["シェルター等"] || "").trim(); if (/東屋|シェルター|シェード/.test(shelterText)) tokens.push("shelter"); if (shelterText.includes("パーゴラ")) tokens.push("pergola"); if (HEALTH_TOKENS.some((token) => play.includes(token))) tokens.push("health"); if (hasSpecialToy(play)) tokens.push("special"); if (play.includes("幼児")) tokens.push("toddler"); const clock = (p["時計"] || "").trim(); if (clock === "○" || clock === "あり" || clock === "有" || clock === "1") tokens.push("clock"); if (p["南西部指定管理"]) tokens.push("cm"); // 草地広場がある公園だけを「草地広場」で絞り込む。 if (p["草地広場"]) tokens.push("picnic"); // 野外卓・縁台がある公園。 if ((p["野外卓"] || "").trim()) tokens.push("table"); // 利用ルール(ボール遊び禁止の表示なし・犬禁止の表示なし)。 // 「禁止」と記録されていない公園を対象にする(未記録も含む)。 // 公共施設と市外の公園は利用ルールを調べていないので対象外。 if (p["施設"] !== "1" && p["種別"] !== "市外") { if (!/禁止/.test(p["ボール遊び"] || "")) tokens.push("ballok"); if (!/禁止/.test(p["ペット"] || "")) tokens.push("dogok"); } return tokens.join(" "); } // 一覧の施設・備考は、フキダシと同じ「アイコン+値」の行で表示する。 const FACILITY_KEYS = [ "ベンチ・椅子", "野外卓", "シェルター等", "草地広場", "多目的広場", "築山", "じゃぶじゃぶ池", "防災用具", "遊具", "時計", "駐車場", "その他設備", "備考", ]; type FacilityRow = { icon: string; cls: string; value: string; banned?: boolean }; const textOf = (value: unknown) => String(value ?? "").trim(); const presenceText = (label: string, value: unknown) => { const v = textOf(value); if (!v) return ""; return /^[○〇◯あり有1]+$/.test(v) ? `${label}あり` : v; }; const waterText = (value: string) => { const text = textOf(value); if (!text) return ""; if (/[??]/.test(text)) return "水飲み場?"; let total = 0; for (const part of text.split(/[、,]/)) { const nums = [...part.matchAll(/[x×](\d+)/g)].map((m) => Number(m[1])); if (nums.length) total += nums.reduce((a, b) => a * b, 1); } return total > 0 ? `水飲み場x${total}` : "水飲み場"; }; 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 facilityRows(p: Park): FacilityRow[] { const rows: FacilityRow[] = []; // 遊具もフキダシと同じく「遊」の文字アイコンを付ける。 if (textOf(p["遊具"])) { rows.push({ icon: '遊', cls: "", value: textOf(p["遊具"]) }); } if (textOf(p["トイレ"])) { rows.push({ icon: 'WC', cls: "is-toilet", value: textOf(p["トイレ"]), }); } const water = textOf(p["水飲み場"]); if (water) { rows.push({ icon: PARK_ICONS.water.svg, cls: "is-water", value: waterText(water) }); } if (textOf(p["給水"])) { rows.push({ icon: PARK_ICONS.water.svg, cls: "is-water", value: textOf(p["給水"]) }); } if (textOf(p["ベンチ・椅子"])) { rows.push({ icon: PARK_ICONS.bench.svg, cls: "is-bench", value: textOf(p["ベンチ・椅子"]) }); } if (textOf(p["野外卓"])) { rows.push({ icon: PARK_ICONS.picnic.svg, cls: "", value: textOf(p["野外卓"]) }); } if (textOf(p["シェルター等"])) { rows.push({ icon: PARK_ICONS.shelter.svg, cls: "", value: textOf(p["シェルター等"]) }); } if (textOf(p["草地広場"])) { rows.push({ icon: PARK_ICONS.grass.svg, cls: "is-grass", value: presenceText("草地広場", p["草地広場"]) }); } if (textOf(p["多目的広場"])) { rows.push({ icon: '広', cls: "", value: presenceText("多目的広場", p["多目的広場"]) }); } if (textOf(p["築山"])) { rows.push({ icon: PARK_ICONS.mountain.svg, cls: "", value: presenceText("築山", p["築山"]) }); } if (textOf(p["じゃぶじゃぶ池"])) { rows.push({ icon: PARK_ICONS.pond.svg, cls: "is-pond", value: presenceText("じゃぶじゃぶ池", p["じゃぶじゃぶ池"]) }); } const ballText = textOf(p["ボール遊び"]); const matchText = textOf(p["試合形式のボール遊び"]); const ballBanned = /禁止/.test(ballText); const ballValue = ballBanned ? "ボール遊び禁止" : /禁止/.test(matchText) ? "試合形式禁止" : ballText; if (ballValue) { rows.push({ icon: PARK_ICONS.ball.svg, cls: "is-ball", value: ballValue, banned: ballBanned }); } const pet = textOf(p["ペット"]); if (pet) { rows.push({ icon: PARK_ICONS.dog.svg, cls: "is-pet", value: pet, banned: /禁止/.test(pet) }); } if (textOf(p["時計"])) { rows.push({ icon: PARK_ICONS.clock.svg, cls: "is-clock", value: presenceText("時計", p["時計"]) }); } const disaster = disasterText(textOf(p["防災用具"])); if (disaster) { rows.push({ icon: '災', cls: "", value: disaster }); } if (textOf(p["駐車場"])) { rows.push({ icon: 'P', cls: "", value: textOf(p["駐車場"]) }); } if (textOf(p["その他設備"])) { rows.push({ icon: '他', cls: "", value: textOf(p["その他設備"]) }); } if (textOf(p["備考"])) { rows.push({ icon: "", cls: "", value: textOf(p["備考"]) }); } if (p["南西部指定管理"]) { rows.push({ icon: "", cls: "", value: "指定管理" }); } return rows; } // 面積(数値) function areaNumber(value: string): number { const n = Number(value); return Number.isFinite(n) ? n : 0; } function areaText(value: string): string { const n = areaNumber(value); return n ? n.toLocaleString("ja-JP") : value; } // 面積の目安(小・中・大)。しきい値はページの注記に合わせる。 function sizeLabel(area: number): string { if (area < 1000) return "小"; if (area < 10000) return "中"; return "大"; } function sizeTitle(area: number): string { if (area < 1000) return "目安: 1,000㎡未満(小さめ)"; if (area < 10000) return "目安: 1,000〜10,000㎡(ふつう)"; return "目安: 10,000㎡以上(広め)"; } // 公園名で検索する。公園名だけだと別の自治体の同名公園に飛ぶので「小平市」を必ず付ける。 // 公園名がない場合だけ住所、それもなければ座標を使う。 function mapsUrl(p: Park): string { if (p["地図URL"]) return p["地図URL"]; const name = (p["公園名"] || "").trim(); const query = name ? `小平市${name}` : p["住所"] ? `小平市${p["住所"]}` : `${p["緯度"]},${p["経度"]}`; return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; } // 検索対象(公園名・住所・町名・種別・設備・備考)。 function searchIndex(p: Park): string { return [ p["公園名"], p["住所"], p["町名"], p["種別"], TYPE_LABEL[p["種別"]], p["設置者"], p["状態"], p["トイレ"], p["水飲み場"], p["給水"], p["草地広場"] ? "草地広場" : "", p["多目的広場"] ? "多目的広場" : "", p["築山"] ? "築山" : "", p["じゃぶじゃぶ池"] ? "じゃぶじゃぶ池" : "", p["ボール遊び"], ...FACILITY_KEYS.map((key) => p[key]), ] .join(" ") .toLowerCase(); } // 編集画面(park-editor)の確認チェックが「済」でないものを未確認とする。 // 対象は市内公園(市内の公共施設・市外の公園は各タブで管理する)。 function isUnconfirmed(p: Park): boolean { if (p["施設"] === "1") return false; if (p["種別"] === "市外") return false; return (p["確認"] || "") !== "済"; } ---
{parks.map((park) => ( ))}
住所 種別 施設・備考
{mappedNames.has(park["公園名"]) ? ( ) : park["公式URL"] ? ( {park["公園名"]} ) : ( park["公園名"] )} {park["状態"] === "閉鎖" ? ( 閉鎖 ) : null} {isUnconfirmed(park) ? ( 未確認 ) : null} {park["設置者"] && park["設置者"] !== "小平市" ? ( {ownerLabel(park["設置者"])} ) : null} {park["住所"] || EMPTY} {park["面積㎡"] ? `${areaText(park["面積㎡"])}㎡` : EMPTY} {park["種別"] ? ( ) : ( EMPTY )} {facilityRows(park).length ? ( {facilityRows(park).map((row) => ( {row.icon ? ( ) : ( )} {row.value} ))} ) : ( EMPTY )}