--- // 小平市の公園一覧(/kodaira-data/parks/)を表示する。 // データは src/data/kodaira-parks.json(市立332件+市立以外)。 // DOM を抑えるため、設備は1セルにまとめ、アイコンは CSS の擬似要素で描く。 // 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 import parksData from "@/data/kodaira-parks.json"; import osmData from "@/data/parks-geojson"; import nearbyData from "@/data/kodaira-nearby-parks.json"; type Park = Record; function nearbyCentroid(feature: any): [number, number] { const g = feature.geometry; const ring = g.type === "Polygon" ? g.coordinates[0] : g.coordinates[0][0]; if (!ring?.length) return [0, 0]; let lat = 0; let lon = 0; for (const [x, y] of ring) { lon += x; lat += y; } return [lat / ring.length, lon / ring.length]; } // 市境500m以内の市外公園。小平市の公園と同じ表に「市外」バッジ付きで並べる。 const nearbyRows: Park[] = ((nearbyData as any).features ?? []).map((f: any) => { const p = f.properties || {}; const [lat, lon] = nearbyCentroid(f); const area = Math.round(Number(p.area_m2) || 0); const name = String(p.name || "").trim(); // 市外の公園も公園名で開く。市名が分かる場合は市名を付ける。 const mapsQuery = name ? p.city ? `${p.city}${name}` : name : `${lat},${lon}`; return { "公園名": name, "住所": p.city ? `${p.city}(市外)` : "市外", "町名": "市外", "面積㎡": area ? String(area) : "", "500㎡以上": "", "種別": "市外", "南西部指定管理": "", "トイレ": "", "水飲み場": "", "ベンチ・椅子": "", "テーブル": "", "パーゴラ": "", "東屋": "", "防災用具": "", "ボール遊び": "", "遊具": "", "駐車場": "", "その他設備": "", "緯度": String(lat), "経度": String(lon), "備考": `市外・小平市境から約${Math.round(Number(p.distance_m) || 0)}m`, "設置者": "市外", "状態": "", "地図キー": `${p.osm_type || ""}/${p.osm_id || ""}`, "地図URL": `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(mapsQuery)}`, }; }); const parks = [...(parksData as Park[]), ...nearbyRows]; // OpenStreetMapにポリゴンがあり、地図から位置を表示できる公園名。 // 市外の近隣公園も地図に表示しているので、同じように地図で見られる。 const mappedNames = new Set([ ...(osmData as any).features.map((f: any) => f.properties?.our_name).filter(Boolean), ...((nearbyData as any).features ?? []) .map((f: any) => String(f.properties?.name || "").trim()) .filter(Boolean), ]); 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", }; // 市立以外の公園に付けるバッジ(設置者が空欄なら市立として扱う)。 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"; } // 設備バッジ(アイコンは CSS の ::before で描く)。 type Badge = { kind: string; label: string; title: string }; function facilityBadges(p: Park): Badge[] { const badges: Badge[] = []; if (p["トイレ"]) { badges.push({ kind: "toilet", label: "トイレ", title: `トイレ: ${p["トイレ"]}` }); } if (p["水飲み場"]) { badges.push({ kind: "water", label: "水飲み場", title: `水飲み場: ${p["水飲み場"]}` }); } if (p["遊具"]) { badges.push({ kind: "toy", label: "遊具", title: `遊具: ${p["遊具"]}` }); } if (p["パーゴラ"]) { badges.push({ kind: "pergola", label: "パーゴラ", title: `パーゴラ: ${p["パーゴラ"]}` }); } if (p["東屋"]) { badges.push({ kind: "azumaya", label: "東屋", title: `東屋: ${p["東屋"]}` }); } const ball = p["ボール遊び"]; if (ball && ball !== "禁止") { const label = ball.includes("一部") ? "ボール遊び一部可" : "ボール遊び可"; badges.push({ kind: "ball", label, title: `ボール遊び: ${ball}` }); } if (p["南西部指定管理"]) { badges.push({ kind: "cm", label: "指定管理", title: "南西部地域の指定管理公園" }); } return badges; } // 設備フィルタ用のトークン(data-fac)。 function facTokens(p: Park): string { const tokens: string[] = []; if (p["トイレ"]) tokens.push("toilet"); if (p["水飲み場"]) tokens.push("water"); if (p["遊具"]) tokens.push("toy"); if (p["パーゴラ"]) tokens.push("pergola"); if (p["東屋"]) tokens.push("azumaya"); const ball = p["ボール遊び"]; if (ball && ball !== "禁止") tokens.push("ball"); if (p["南西部指定管理"]) tokens.push("cm"); return tokens.join(" "); } // 設備セルの補足テキスト(バッジで示した項目の詳細と、その他の項目)。 // ラベル文字は使わず、アイコンで項目を示す。 const DETAIL_FIELDS: { key: string; icon: string }[] = [ { key: "ベンチ・椅子", icon: "🪑" }, { key: "テーブル", icon: "🍽" }, { key: "防災用具", icon: "🧯" }, { key: "遊具", icon: "🎠" }, { key: "駐車場", icon: "🅿" }, { key: "その他設備", icon: "🔧" }, { key: "備考", icon: "" }, ]; function detailText(p: Park): string { const parts: string[] = []; for (const { key, icon } of DETAIL_FIELDS) { const value = p[key]; if (!value) continue; parts.push(icon ? `${icon} ${value}` : value); } const ball = p["ボール遊び"]; if (ball && ball !== "禁止" && ball !== "可") parts.push(`⚽ ${ball}`); return parts.join(" / "); } // 面積(数値) 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["ボール遊び"], ...DETAIL_FIELDS.map((f) => p[f.key]), ] .join(" ") .toLowerCase(); } const towns = [...new Set(parks.map((p) => p["町名"]).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ja"), ); const total = parks.length; ---
設備・管理で絞る
{total}件

設備の情報は、市の資料などに基づきます。設備で絞ると、この情報がある公園だけが対象になります。

{parks.map((park) => ( ))}
住所 種別 施設・備考
{mappedNames.has(park["公園名"]) ? ( ) : ( park["公園名"] )} {park["状態"] === "閉鎖" ? ( 閉鎖 ) : null} {park["設置者"] && park["設置者"] !== "小平市" ? ( {ownerLabel(park["設置者"])} ) : null} {park["住所"] || EMPTY} {park["面積㎡"] ? `${areaText(park["面積㎡"])}㎡` : EMPTY} {park["種別"] ? ( {park["種別"]} ) : ( EMPTY )} {facilityBadges(park).map((b) => ( ))} {detailText(park) ? {detailText(park)} : null} {facilityBadges(park).length === 0 && !detailText(park) ? EMPTY : null}