aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/components/ScheduleInteractive.astro13
-rw-r--r--src/components/ScheduleMap.astro386
-rw-r--r--src/content/docs/kodaira-data/hp-archive.mdx1
-rw-r--r--src/content/docs/kodaira-data/hp-archive/schedule.mdx19
-rw-r--r--src/content/docs/kodaira-data/parks.mdx2
-rw-r--r--src/data/kodaira-event-venues.json149
-rw-r--r--src/styles/custom.css133
7 files changed, 700 insertions, 3 deletions
diff --git a/src/components/ScheduleInteractive.astro b/src/components/ScheduleInteractive.astro
index 955977d..3295125 100644
--- a/src/components/ScheduleInteractive.astro
+++ b/src/components/ScheduleInteractive.astro
@@ -354,6 +354,19 @@
});
const title = `${date.getMonth() + 1}月${date.getDate()}日(${weekday})の予定`;
+ // 会場の地図(ScheduleMap)に、その日の予定を知らせる。
+ const detail = {
+ date: key,
+ entries: list.map((e) => ({
+ name: e.name,
+ time: e.time,
+ category: e.category,
+ rowId: e.rowId,
+ })),
+ };
+ (window as any).__scheduleDay = detail;
+ document.dispatchEvent(new CustomEvent('schedule:day', { detail }));
+
if (list.length === 0) {
detailsEl.innerHTML = `<h3>${title}</h3><p>この日の予定はありません。</p>`;
return;
diff --git a/src/components/ScheduleMap.astro b/src/components/ScheduleMap.astro
new file mode 100644
index 0000000..bf84dd7
--- /dev/null
+++ b/src/components/ScheduleMap.astro
@@ -0,0 +1,386 @@
+---
+// カレンダーで選んだ日に開催される予定の会場を地図に表示する。
+// 会場名から緯度・経度を引くため、公共施設のデータと、イベント会場の追加データを使う。
+---
+
+<div class="schedule-map-block">
+ <div id="schedule-map" class="schedule-map" role="region" aria-label="選択した日の会場の地図"></div>
+ <p id="schedule-map-note" class="schedule-map-note" aria-live="polite"></p>
+ <p id="schedule-map-locate-status" class="schedule-map-locate-status" aria-live="polite" hidden></p>
+</div>
+
+<script>
+ import L from 'leaflet';
+ import 'leaflet/dist/leaflet.css';
+ import facilities from '@/data/kodaira-facilities.json';
+ import extraVenues from '@/data/kodaira-event-venues.json';
+ import boundary from '@/data/kodaira-boundary.json';
+
+ type Venue = { name: string; alias: string; lat: number; lon: number };
+ type Entry = { name: string; time: string; category: string; rowId: string };
+
+ function buildVenues(): Venue[] {
+ const out: Venue[] = [];
+ // 「小川町一丁目地域センター」と「小川町1丁目」のような漢数字・算用数字の違いを吸収する。
+ const toAsciiDigits = (s: string) =>
+ s.replace(/[一二三四五六七八九]/g, (c) => String('一二三四五六七八九'.indexOf(c) + 1));
+
+ const pushName = (name: string, lat: number, lon: number) => {
+ out.push({ name, alias: name, lat, lon });
+ const ascii = toAsciiDigits(name);
+ if (ascii !== name) out.push({ name, alias: ascii, lat, lon });
+ // 「ルネこだいら(小平市民文化会館)」のような正式名は、括弧の前でも照合する。
+ const short = name.replace(/(.*?)/g, '').trim();
+ if (short && short !== name) {
+ out.push({ name, alias: short, lat, lon });
+ const shortAscii = toAsciiDigits(short);
+ if (shortAscii !== short) out.push({ name, alias: shortAscii, lat, lon });
+ }
+ };
+
+ for (const f of facilities as any[]) {
+ const lat = Number(f.lat);
+ const lon = Number(f.lon);
+ if (!lat || !lon) continue;
+ pushName(String(f.name), lat, lon);
+ }
+ for (const v of extraVenues as any[]) {
+ for (const alias of (v.aliases ?? [v.name]) as string[]) {
+ out.push({ name: v.name, alias, lat: Number(v.lat), lon: Number(v.lon) });
+ }
+ }
+ // 長い別名から照合し、「東部市民センター」が短い別名で誤って一致しないようにする。
+ return out.sort((a, b) => b.alias.length - a.alias.length);
+ }
+
+ const venues = buildVenues();
+ const compact = (s: string) => s.replace(/[\s ]/g, '');
+
+ // 文章中の会場名を、左から順に・長い名前を優先して拾う。
+ // 「マナーアップキャンペーン」のように複数会場を並べた予定は、そのすべてを返す。
+ // 「新小平駅」を「小平駅」と取り違えないよう、一致した範囲は読み飛ばす。
+ function findVenues(text: string): Venue[] {
+ const t = compact(text);
+ const found: Venue[] = [];
+ let from = 0;
+ while (from < t.length) {
+ let best: { index: number; alias: string; venue: Venue } | null = null;
+ for (const v of venues) {
+ const alias = compact(v.alias);
+ const index = t.indexOf(alias, from);
+ if (index === -1) continue;
+ if (
+ !best ||
+ index < best.index ||
+ (index === best.index && alias.length > best.alias.length)
+ ) {
+ best = { index, alias, venue: v };
+ }
+ }
+ if (!best) break;
+ const picked = best;
+ if (!found.some((f) => f.lat === picked.venue.lat && f.lon === picked.venue.lon)) {
+ found.push(picked.venue);
+ }
+ from = picked.index + picked.alias.length;
+ }
+ return found;
+ }
+
+ const escapeHtml = (s: string) =>
+ s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c] as string));
+
+ const el = document.getElementById('schedule-map');
+ const noteEl = document.getElementById('schedule-map-note');
+
+ if (el && noteEl && !el.dataset.initialized) {
+ el.dataset.initialized = '1';
+
+ const map = L.map(el, {
+ scrollWheelZoom: false,
+ zoomSnap: 1,
+ zoomDelta: 1,
+ zoomAnimation: false,
+ fadeAnimation: false,
+ markerZoomAnimation: false,
+ }).setView([35.728, 139.477], 13);
+
+ map.attributionControl.setPrefix(false);
+ const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution:
+ '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
+ }).addTo(map);
+
+ // 小平市の市境(公園さんぽマップと同じ表示:塗りなし・太い実線)
+ const boundaryLayer = L.geoJSON(boundary as any, {
+ style: {
+ color: '#6a1b9a',
+ weight: 4,
+ opacity: 0.9,
+ fill: false,
+ },
+ interactive: false,
+ }).addTo(map);
+
+ const fitToBoundary = () => {
+ const bounds = boundaryLayer.getBounds();
+ if (bounds.isValid()) {
+ map.fitBounds(bounds, { padding: [20, 20], animate: false });
+ } else {
+ map.setView([35.728, 139.477], 13);
+ }
+ };
+
+ // レイアウト確定後にサイズを再計算し、タイルを再配置する。
+ // 初回は表示範囲も合わせ直す(タイルの行ずれを防ぐ)。
+ const refreshMap = (refit = false) => {
+ map.invalidateSize();
+ tileLayer.redraw();
+ if (!refit) return;
+ if (currentBounds.length === 0) fitToBoundary();
+ else if (currentBounds.length === 1) map.setView(currentBounds[0], 15);
+ else map.fitBounds(currentBounds, { padding: [40, 40], maxZoom: 16 });
+ };
+
+ let currentBounds: L.LatLngTuple[] = [];
+
+ let markers: L.LayerGroup | null = null;
+
+ // 現在地表示。位置情報はボタンを押したときだけ取得する。
+ const locateStatusEl = document.getElementById('schedule-map-locate-status');
+ const setLocateStatus = (message: string) => {
+ if (!locateStatusEl) return;
+ locateStatusEl.textContent = message;
+ locateStatusEl.hidden = !message;
+ };
+
+ const LOCATE_ICON =
+ '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="2.5" fill="currentColor"/><path d="M12 1.5v3M12 19.5v3M1.5 12h3M19.5 12h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>';
+ const ACTIVE_ICON =
+ '<span aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="2.5" fill="currentColor"/><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>';
+
+ let locationMarker: L.Marker | null = null;
+ let accuracyCircle: L.Circle | null = null;
+ let locateButton: HTMLButtonElement | null = null;
+
+ const updateLocateButton = () => {
+ if (!locateButton) return;
+ const active = Boolean(locationMarker);
+ locateButton.classList.toggle('is-active', active);
+ locateButton.setAttribute('aria-pressed', active ? 'true' : 'false');
+ const label = active ? '現在地の表示を消す' : '現在地を表示';
+ locateButton.title = label;
+ locateButton.setAttribute('aria-label', label);
+ locateButton.innerHTML = active ? ACTIVE_ICON : LOCATE_ICON;
+ };
+
+ const clearLocation = () => {
+ if (locationMarker) {
+ locationMarker.remove();
+ locationMarker = null;
+ }
+ if (accuracyCircle) {
+ accuracyCircle.remove();
+ accuracyCircle = null;
+ }
+ updateLocateButton();
+ };
+
+ const showLocation = (latitude: number, longitude: number, accuracy: number) => {
+ if (!locationMarker) {
+ const icon = L.divIcon({
+ className: 'schedule-locate-marker',
+ html: '<span class="schedule-locate-dot"></span>',
+ iconSize: [18, 18],
+ iconAnchor: [9, 9],
+ });
+ locationMarker = L.marker([latitude, longitude], {
+ icon,
+ interactive: false,
+ keyboard: false,
+ }).addTo(map);
+ } else {
+ locationMarker.setLatLng([latitude, longitude]);
+ }
+
+ if (!accuracyCircle) {
+ accuracyCircle = L.circle([latitude, longitude], {
+ radius: Math.min(accuracy, 1000),
+ color: '#1e88e5',
+ weight: 1,
+ fillColor: '#90caf9',
+ fillOpacity: 0.15,
+ interactive: false,
+ }).addTo(map);
+ } else {
+ accuracyCircle.setLatLng([latitude, longitude]).setRadius(Math.min(accuracy, 1000));
+ }
+
+ map.setView([latitude, longitude], Math.max(map.getZoom(), 15));
+ updateLocateButton();
+ setLocateStatus(
+ `現在地を表示しています(精度 約${Math.round(accuracy)}m)。もう一度押すと表示を消します。`
+ );
+ };
+
+ const locate = async () => {
+ if (locationMarker) {
+ clearLocation();
+ setLocateStatus('現在地の表示を消しました。');
+ return;
+ }
+
+ if (!navigator.geolocation) {
+ setLocateStatus('このブラウザでは現在地を取得できません。');
+ window.alert(
+ 'このブラウザでは現在地を取得できません。端末やアプリの設定をご確認ください。'
+ );
+ return;
+ }
+
+ // すでに「ブロック」されている場合は、ブラウザがもう許可を聞いてくれない。
+ let state = 'unknown';
+ try {
+ if (navigator.permissions?.query) {
+ state = (await navigator.permissions.query({ name: 'geolocation' as PermissionName })).state;
+ }
+ } catch {
+ state = 'unknown';
+ }
+
+ if (state === 'denied') {
+ setLocateStatus('位置情報がブロックされています。設定で許可してください。');
+ window.alert(
+ '位置情報の利用がブロックされています。お使いのブラウザやアプリ(ホーム画面に追加したアプリを含む)の設定で、このサイトの位置情報を許可してから、もう一度お試しください。'
+ );
+ return;
+ }
+
+ setLocateStatus('現在地を取得しています…');
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude, longitude, accuracy } = position.coords;
+ showLocation(latitude, longitude, accuracy);
+ },
+ (error) => {
+ let message = '現在地を取得できませんでした。';
+ if (error.code === error.PERMISSION_DENIED) {
+ message =
+ '位置情報の利用が許可されていません。ブラウザやアプリの設定で、このサイトの位置情報を許可してください。';
+ } else if (error.code === error.POSITION_UNAVAILABLE) {
+ message =
+ '現在地を取得できませんでした。端末の位置情報(GPS)が有効か、電波の状況をご確認ください。';
+ } else if (error.code === error.TIMEOUT) {
+ message = '現在地の取得に時間がかかっています。もう一度お試しください。';
+ }
+ setLocateStatus(message);
+ window.alert(message);
+ },
+ { enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 }
+ );
+ };
+
+ const LocateControl = L.Control.extend({
+ onAdd: () => {
+ const btn = L.DomUtil.create('button', 'schedule-locate') as HTMLButtonElement;
+ btn.setAttribute('type', 'button');
+ locateButton = btn;
+ L.DomEvent.disableClickPropagation(btn);
+ L.DomEvent.disableScrollPropagation(btn);
+ btn.addEventListener('click', () => {
+ void locate();
+ });
+ updateLocateButton();
+ return btn;
+ },
+ });
+
+ new LocateControl({ position: 'topright' }).addTo(map);
+
+ function render(entries: Entry[]) {
+ if (markers) markers.remove();
+ markers = L.layerGroup().addTo(map);
+
+ const groups = new Map<string, { venue: Venue; items: Entry[] }>();
+ let unmapped = 0;
+
+ for (const entry of entries) {
+ const matched = findVenues(`${entry.time} ${entry.name}`);
+ if (matched.length === 0) {
+ unmapped += 1;
+ continue;
+ }
+ for (const venue of matched) {
+ const key = `${venue.lat},${venue.lon}`;
+ const group = groups.get(key) ?? { venue, items: [] };
+ group.items.push(entry);
+ groups.set(key, group);
+ }
+ }
+
+ if (groups.size === 0) {
+ currentBounds = [];
+ fitToBoundary();
+ noteEl.textContent =
+ entries.length === 0
+ ? 'この日の予定はありません。'
+ : 'この日の予定の会場は、地図に出せる場所として登録されていません。';
+ return;
+ }
+
+ const bounds: L.LatLngTuple[] = [];
+
+ for (const { venue, items } of groups.values()) {
+ bounds.push([venue.lat, venue.lon]);
+ const icon = L.divIcon({
+ className: 'schedule-map-marker',
+ html: `<span class="schedule-map-pin">${escapeHtml(items[0].category || '📍')}</span>`,
+ iconSize: [30, 30],
+ iconAnchor: [15, 15],
+ });
+ const list = items
+ .map((item) => {
+ const name = item.name.startsWith(item.category)
+ ? item.name.slice(item.category.length).trim()
+ : item.name;
+ const time = item.time.split('/')[0];
+ return `<li><a href="#${escapeHtml(item.rowId)}">${escapeHtml(name)}</a><span>${escapeHtml(time)}</span></li>`;
+ })
+ .join('');
+
+ L.marker([venue.lat, venue.lon], { icon, title: venue.name })
+ .addTo(markers)
+ .bindPopup(
+ `<strong class="schedule-map-popup-title">${escapeHtml(venue.name)}</strong><ul class="schedule-map-popup-list">${list}</ul>`
+ );
+ }
+
+ if (bounds.length === 1) {
+ map.setView(bounds[0], 15);
+ } else {
+ map.fitBounds(bounds, { padding: [40, 40], maxZoom: 16 });
+ }
+ currentBounds = bounds;
+
+ const suffix =
+ unmapped > 0 ? `(会場が地図に未登録の予定${unmapped}件は表示していません)` : '';
+ noteEl.textContent = `${groups.size}会場を表示しています。${suffix}`;
+ }
+
+ document.addEventListener('schedule:day', (event) => {
+ const detail = (event as CustomEvent).detail as { entries?: Entry[] } | undefined;
+ render(detail?.entries ?? []);
+ });
+
+ // 読み込み順にかかわらず、すでに選択されている日の内容を反映する。
+ const initial = (window as any).__scheduleDay as { entries?: Entry[] } | undefined;
+ if (initial) render(initial.entries ?? []);
+ else noteEl.textContent = 'カレンダーで日付を選ぶと、その日の会場を地図に表示します。';
+
+ // レイアウト確定後にサイズを再計算し、タイルの行ずれを防ぐ。
+ requestAnimationFrame(() => refreshMap(true));
+ window.setTimeout(() => refreshMap(true), 300);
+ window.addEventListener('resize', () => refreshMap(false));
+ }
+</script>
diff --git a/src/content/docs/kodaira-data/hp-archive.mdx b/src/content/docs/kodaira-data/hp-archive.mdx
index 71bfd1c..9e7d3a9 100644
--- a/src/content/docs/kodaira-data/hp-archive.mdx
+++ b/src/content/docs/kodaira-data/hp-archive.mdx
@@ -70,6 +70,7 @@ import { LinkCard } from '@astrojs/starlight/components';
- **令和8年11月1日**: 市議会資料3から、第2回市民と議会の意見交換会(嘉悦大学・飛翔祭)を追加。
- **令和8年11月1日**: 令和8年度小平市議会行事予定表(6月修正版)から、本会議・委員会・特別委員会などの予定を審議会等の一覧に追加。
- **令和8年11月1日**: 行事・イベント一覧と案内ページの冒頭に、英語・中国語(簡体字)・韓国語・ベトナム語の案内を追加。案内ページの多言語の案内はページ冒頭に移した。
+- **令和8年11月1日**: 行事・イベント一覧に「会場の地図」を追加。カレンダーで選んだ日の会場を地図に表示し、公園さんぽマップと同じく小平市の市境を表示する。地図に現在地を表示するボタンも置いた。
- **令和8年10月28日**: こだいら観光まちづくり協会のイベント情報から、農産物直売所デジタルスタンプラリー、まち巡りガイドツアー、萩山公園ライトアップを追加。
<div class="tech-note">
diff --git a/src/content/docs/kodaira-data/hp-archive/schedule.mdx b/src/content/docs/kodaira-data/hp-archive/schedule.mdx
index 7c0bdee..00479a9 100644
--- a/src/content/docs/kodaira-data/hp-archive/schedule.mdx
+++ b/src/content/docs/kodaira-data/hp-archive/schedule.mdx
@@ -5,6 +5,7 @@ tableOfContents: true
---
import ScheduleInteractive from '../../../../components/ScheduleInteractive.astro';
+import ScheduleMap from '../../../../components/ScheduleMap.astro';
[← 案内ページ(小平市ホームページのアーカイブ(暫定)等)に戻る](/kodaira-data/hp-archive/)
@@ -40,6 +41,16 @@ import ScheduleInteractive from '../../../../components/ScheduleInteractive.astr
</div>
<div id="schedule-day-details" class="schedule-day-details" aria-live="polite"></div>
+## 会場の地図
+
+<ScheduleMap />
+
+カレンダーで日付を選ぶと、その日の予定の会場を地図に表示します。地図のピンを押すと、その会場の予定が出ます。紫の実線は小平市の市境です。
+
+地図の右上の現在地ボタンを押すと、端末の現在地を地図に表示します。現在地は、端末やブラウザ、ホーム画面に追加したアプリの設定で位置情報を許可しているときだけ使えます。ボタンを押しても動かないときは、警告を出して設定の確認をご案内します。
+
+会場の位置は、住所をもとにしたおおよその位置です。テレビ放送や「市内各所」、駅前・街なかなど、場所を1点に絞れない予定は地図に表示していません(一覧表には掲載しています)。
+
## 審議会等の開催予定
教育委員会の附属機関(社会教育委員の会議、部活動地域連携・地域移行推進委員会など)も含みます。日付は、保存されたページ、市公式X、市報、市議会の行事予定表の記載によるものです。会議名の前のアイコンは分野(子育て、福祉・相談など)を示します。分野を特定していない会議は🏛️です。
@@ -156,7 +167,9 @@ import ScheduleInteractive from '../../../../components/ScheduleInteractive.astr
| 11月1日(日) | 🎨 小平ふるさと村 村まつり | 午前10時から午後3時まで/小平ふるさと村 | 当日、会場へ。小平ふるさと村 042-345-8155 | 市報こだいら10月20日号5面 |
| 11月1日(日)〜3日(火・祝) | 🤝 小川パレット オープニングイベント | 11月1日は午前10時から午後5時まで、11月2日は午前9時から午後5時まで、11月3日は午前9時から午後10時まで/小川パレット(小川西町4-16-1、4・5階) | 展示やクイズラリーは午後5時まで。市民協働・男女参画推進課 042-346-9532 | 市報こだいら10月20日号8面 |
| 11月2日(月) | ♿ 住まいのリフォーム相談会 | 午前9時30分から午後2時30分まで/市役所1階ロビー | 当日、会場へ。市民協働・男女参画推進課 042-346-9809 | 市報こだいら10月20日号4面 |
-| 11月2日〜12月14日の月曜日、11月6日〜12月11日の金曜日、11月10日〜12月15日の火曜日 | 🧓 筋力アップ介護予防講座 | 小川町1丁目地域センター(月)、仲町公民館(金)、花小金井南公民館(火) | [申込みフォーム](https://logoform.jp/form/exdH/1736325)。高齢者支援課 042-346-9539 | [市のX](https://x.com/kodaira_tokyo/status/2104405745550922074) |
+| 11月2日〜12月14日の月曜日 | 🧓 筋力アップ介護予防講座(月曜日) | 小川町1丁目地域センター | [申込みフォーム](https://logoform.jp/form/exdH/1736325)。高齢者支援課 042-346-9539 | [市のX](https://x.com/kodaira_tokyo/status/2104405745550922074) |
+| 11月6日〜12月11日の金曜日 | 🧓 筋力アップ介護予防講座(金曜日) | 仲町公民館 | [申込みフォーム](https://logoform.jp/form/exdH/1736325)。高齢者支援課 042-346-9539 | [市のX](https://x.com/kodaira_tokyo/status/2104405745550922074) |
+| 11月10日〜12月15日の火曜日 | 🧓 筋力アップ介護予防講座(火曜日) | 花小金井南公民館 | [申込みフォーム](https://logoform.jp/form/exdH/1736325)。高齢者支援課 042-346-9539 | [市のX](https://x.com/kodaira_tokyo/status/2104405745550922074) |
| 11月3日(火・祝) | 🤝 市長と話しませんか タウンミーティング | 午後1時から午後2時30分まで(午後0時30分開場)/小川パレット5階 第1多目的室 | 定員25人(当日、会場へ。先着順)。市民課 042-346-9508。テーマは「小川パレットの魅力的な活用方法について」 | 市報こだいら10月19日号2面 |
| 11月3日(火・祝) | 🏃 FC東京 親子サッカー教室 | 4歳児の部は午前9時30分から午前10時45分まで(受付午前9時15分)、5歳児の部は午前11時15分から午後0時30分まで(受付午前11時)/FC東京小平グランド(多目的グランド、大沼町3-14-1) | 市内在住の4歳〜5歳の未就学児と保護者のペア。各回20組40人(抽選)。駐車場なし。10月19日(月)までに[申込みフォーム](https://logoform.jp/form/exdH/1741463)へ。文化スポーツ課 042-346-9612 | [LoGoフォーム](https://logoform.jp/form/exdH/1741463) |
| 11月3日(火・祝)、11月28日(土) | 🌳 ドッグラン&犬のしつけ相談会 | 午前10時から午後3時まで/11月3日は福祉会館北側旧市民広場、11月28日は子どもキャンプ場 | 狂犬病注射済票などを持参。環境政策課 042-312-1796 | [市のX](https://x.com/kodaira_tokyo/status/2105538200697761927) |
@@ -164,7 +177,9 @@ import ScheduleInteractive from '../../../../components/ScheduleInteractive.astr
| 11月3日(火・祝) | 🤝 第54回 小平社協福祉バザー | 午前10時から午後3時まで/中央公民館 | 雨天実施。10時までは場内に入れません。現金のみ、エコバッグ持参。こだいらボランティアセンター 042-346-1424。 | 社協だより11月1日号1面 |
| 11月4日(水) | 🌳 花壇の花株を無料で配布 | 午前10時から午後4時まで/西武線小平駅南口花壇 | 花株を入れる袋とスコップを持参。無くなりしだい終了。雨天時は5日(木)に順延。車での来場はご遠慮ください。道路課 042-346-9824 | 市報こだいら10月20日号4面 |
| 11月4日(水) | 👶 青少年健全育成講演会「サイバー空間に潜む『見えない落とし穴』~ネットトラブルの実態と身の守り方~」 | 午後3時から午後4時30分まで(開場午後2時30分)/中央公民館 ホール(2階) | 事前申込み不要。当日会場へ(先着順、定員70人)。講師は石川光泰さん(慶應義塾大学准教授)。無料。市報では「身近な犯罪から大切な人を守るために~ネットやSNSに潜む危険~」と記載。地域学習支援課 042-346-9834 | 市報こだいら10月1日号9面/[LoGoフォーム](https://logoform.jp/form/exdH/1851637) |
-| 11月4日(水)〜6日(金) | 🌳 環境美化マナーアップキャンペーン | 午前7時30分から午前8時30分まで/11月4日は西武線花小金井駅・小川駅、5日は一橋学園駅・小平駅、6日は鷹の台駅・新小平駅 | 環境政策課 042-312-1796 | 市報こだいら10月20日号3面 |
+| 11月4日(水) | 🌳 環境美化マナーアップキャンペーン | 午前7時30分から午前8時30分まで/西武線花小金井駅・小川駅 | 環境政策課 042-312-1796 | 市報こだいら10月20日号3面 |
+| 11月5日(木) | 🌳 環境美化マナーアップキャンペーン | 午前7時30分から午前8時30分まで/一橋学園駅・西武線小平駅 | 環境政策課 042-312-1796 | 市報こだいら10月20日号3面 |
+| 11月6日(金) | 🌳 環境美化マナーアップキャンペーン | 午前7時30分から午前8時30分まで/鷹の台駅・新小平駅 | 環境政策課 042-312-1796 | 市報こだいら10月20日号3面 |
| 11月6日(金)・7日(土) | 🤝 小川駅西口駅前広場整備に関するオープンハウス | 午前11時から午後4時まで(荒天中止)/小川駅西口(小川駅前交番)付近 | パネル展示とアンケート。駐車場なし。地域整備支援課 042-346-9592 | 市報こだいら10月20日号4面 |
| 11月6日(金) | 🤝 庁舎総合防災訓練・小平消防署消防演習 | 午前9時から午前10時20分まで/小平市役所 | 観覧可。初期消火訓練、避難誘導訓練などを実施。総務課 042-346-9511。 | [LoGoフォーム](https://logoform.jp/form/exdH/1876567) |
| 11月7日(土)〜8日(日) | 🎨 小川公民館まつり | 11月7日は午前10時から午後4時まで、11月8日は午前10時から午後3時まで/小川公民館 | 小川公民館 042-343-3620 | 市報こだいら10月20日号6面 |
diff --git a/src/content/docs/kodaira-data/parks.mdx b/src/content/docs/kodaira-data/parks.mdx
index dccb30a..ba15983 100644
--- a/src/content/docs/kodaira-data/parks.mdx
+++ b/src/content/docs/kodaira-data/parks.mdx
@@ -1,6 +1,6 @@
---
title: こだいら 公園さんぽマップ
-draft: false
+draft: true
description: 小平市と周辺の公園・公共施設一覧(愛称:こだいら 公園さんぽマップ)。小平市内の公園355件と市境500m以内の市外公園48件、周辺の図書館・公民館・児童館などの公共施設を、町名・キーワード・設備で検索できる一覧にまとめました。都立公園や都営・UR住宅内の公園、所有者未確認の緑地も含みます。小平市議会議員 安竹洋平の活動記録です。
---
diff --git a/src/data/kodaira-event-venues.json b/src/data/kodaira-event-venues.json
new file mode 100644
index 0000000..4a70987
--- /dev/null
+++ b/src/data/kodaira-event-venues.json
@@ -0,0 +1,149 @@
+[
+ {
+ "name": "小平市役所(本庁)",
+ "aliases": ["小平市役所(本庁)", "小平市役所", "市役所"],
+ "lat": 35.7279025,
+ "lon": 139.4774885,
+ "source": "公共施設データ(小平市役所(本庁))"
+ },
+ {
+ "name": "小平市福祉会館",
+ "aliases": ["小平市福祉会館", "福祉会館"],
+ "lat": 35.726604,
+ "lon": 139.478183,
+ "source": "OpenStreetMap(学園東町1-19-13)"
+ },
+ {
+ "name": "小平市健康センター",
+ "aliases": ["小平市健康センター", "健康センター"],
+ "lat": 35.727299,
+ "lon": 139.477875,
+ "source": "国土地理院 住所検索(学園東町1-19-12)"
+ },
+ {
+ "name": "小平市リサイクルセンター",
+ "aliases": ["小平市リサイクルセンター", "リサイクルセンター"],
+ "lat": 35.733582,
+ "lon": 139.474167,
+ "source": "国土地理院 住所検索(小川東町5-19-10)"
+ },
+ {
+ "name": "小平ふるさと村",
+ "aliases": ["小平ふるさと村", "ふるさと村"],
+ "lat": 35.7313612,
+ "lon": 139.5004565,
+ "source": "OpenStreetMap"
+ },
+ {
+ "name": "小平市東部市民センター",
+ "aliases": ["小平市東部市民センター", "東部市民センター"],
+ "lat": 35.7279279,
+ "lon": 139.5140728,
+ "source": "OpenStreetMap(花小金井1-8-1)"
+ },
+ {
+ "name": "小平市立学校給食センター",
+ "aliases": ["小平市立学校給食センター", "学校給食センター", "給食センター"],
+ "lat": 35.7327824,
+ "lon": 139.4721501,
+ "source": "OpenStreetMap"
+ },
+ {
+ "name": "小平市民総合体育館",
+ "aliases": ["小平市民総合体育館", "市民総合体育館"],
+ "lat": 35.7226522,
+ "lon": 139.4626664,
+ "source": "OpenStreetMap"
+ },
+ {
+ "name": "小川パレット",
+ "aliases": ["小川パレット"],
+ "lat": 35.737438,
+ "lon": 139.46312,
+ "source": "国土地理院 住所検索(小川西町4-16-1)"
+ },
+ {
+ "name": "嘉悦大学",
+ "aliases": ["嘉悦大学"],
+ "lat": 35.720676,
+ "lon": 139.514572,
+ "source": "国土地理院 住所検索(花小金井南町2-8-4)"
+ },
+ {
+ "name": "小川町二丁目児童館",
+ "aliases": ["小川町二丁目児童館"],
+ "lat": 35.728764,
+ "lon": 139.466812,
+ "source": "国土地理院 住所検索(小川町2-1154)"
+ },
+ {
+ "name": "小平市子ども家庭支援センター",
+ "aliases": ["小平市子ども家庭支援センター", "子ども家庭支援センター"],
+ "lat": 35.73999,
+ "lon": 139.474518,
+ "source": "国土地理院 住所検索(小川東町4-2-1)"
+ },
+ {
+ "name": "FC東京小平グランド",
+ "aliases": ["FC東京小平グランド", "小平グランド"],
+ "lat": 35.741024,
+ "lon": 139.499603,
+ "source": "国土地理院 住所検索(大沼町3-14-1)"
+ },
+ {
+ "name": "西武線小平駅(南口)",
+ "aliases": ["西武線小平駅南口", "小平駅南口", "西武線小平駅", "小平駅"],
+ "lat": 35.7369,
+ "lon": 139.4878,
+ "source": "OpenStreetMap(小平駅南口・駅前交番付近)"
+ },
+ {
+ "name": "新小平駅",
+ "aliases": ["JR新小平駅", "新小平駅"],
+ "lat": 35.73027,
+ "lon": 139.47105,
+ "source": "OpenStreetMap(新小平駅ホーム)"
+ },
+ {
+ "name": "西武線花小金井駅",
+ "aliases": ["西武線花小金井駅", "花小金井駅"],
+ "lat": 35.7262,
+ "lon": 139.513,
+ "source": "OpenStreetMap(花小金井駅南口付近)"
+ },
+ {
+ "name": "西武線小川駅(西口)",
+ "aliases": ["小川駅西口", "西武線小川駅", "小川駅"],
+ "lat": 35.7373,
+ "lon": 139.4632,
+ "source": "OpenStreetMap(小川駅西口・駅前交番付近)"
+ },
+ {
+ "name": "一橋学園駅",
+ "aliases": ["一橋学園駅"],
+ "lat": 35.7225,
+ "lon": 139.4799,
+ "source": "OpenStreetMap(一橋学園駅)"
+ },
+ {
+ "name": "鷹の台駅",
+ "aliases": ["鷹の台駅"],
+ "lat": 35.7232,
+ "lon": 139.4605,
+ "source": "OpenStreetMap(鷹の台駅周辺)"
+ },
+ {
+ "name": "さわやか館",
+ "aliases": ["さわやか館"],
+ "lat": 35.736778,
+ "lon": 139.510666,
+ "source": "国土地理院 住所検索(花小金井4-21-2)"
+ },
+ {
+ "name": "特別養護老人ホーム小川ホーム",
+ "aliases": ["特別養護老人ホーム小川ホーム", "小川ホーム"],
+ "lat": 35.737324,
+ "lon": 139.455902,
+ "source": "国土地理院 住所検索(小川西町2-1-35-12)"
+ }
+]
diff --git a/src/styles/custom.css b/src/styles/custom.css
index 4ea2724..a0b0a59 100644
--- a/src/styles/custom.css
+++ b/src/styles/custom.css
@@ -319,6 +319,139 @@ body:has(.parks-controls) .sl-markdown-content p {
color: var(--sl-color-text, inherit);
}
+/* ── 会場の地図(カレンダーで選んだ日の会場) ── */
+.schedule-map-block {
+ margin: 0.8rem 0 1.4rem;
+}
+.schedule-map {
+ height: min(55vh, 24rem);
+ min-height: 16rem;
+ border: 1px solid var(--sl-color-gray-5);
+ border-radius: 8px;
+ /* Leaflet の内部 z-index を閉じ込め、ヘッダーなどに重ならないようにする。 */
+ position: relative;
+ z-index: 0;
+}
+/* StarlightのMarkdown余白ルールがLeafletの各ペインに margin-top を付け、
+ タイルが1行分(約50px)下にずれるため、地図内のペインは余白なしにする。 */
+.schedule-map .leaflet-pane {
+ margin: 0 !important;
+}
+/* Starlightの canvas/svg 向け max-width が描画領域を狭めるため、地図内だけ上書きする。 */
+.schedule-map .leaflet-overlay-pane canvas,
+.schedule-map .leaflet-overlay-pane svg {
+ max-width: none !important;
+ max-height: none !important;
+ margin: 0 !important;
+}
+/* Starlightの画像用スタイル(margin / border / border-radius)がタイルに付くと、
+ 行ずれやタイルごとの枠が出るため打ち消す。 */
+.schedule-map .leaflet-tile {
+ margin: 0 !important;
+ border: 0 !important;
+ border-radius: 0 !important;
+}
+.schedule-map-note {
+ margin: 0.4rem 0 0;
+ color: var(--sl-color-gray-3);
+ font-size: 0.9rem;
+}
+.schedule-map-marker {
+ background: transparent;
+ border: 0;
+}
+.schedule-map-pin {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background: var(--sl-color-accent, #b0003a);
+ color: #fff;
+ font-size: 15px;
+ line-height: 1;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
+ animation: schedule-pin-glow 1.6s ease-out 2;
+}
+@keyframes schedule-pin-glow {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(255, 214, 0, 0.85), 0 2px 6px rgba(0, 0, 0, 0.35);
+ }
+ 70% {
+ box-shadow: 0 0 0 14px rgba(255, 214, 0, 0), 0 2px 6px rgba(0, 0, 0, 0.35);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(255, 214, 0, 0), 0 2px 6px rgba(0, 0, 0, 0.35);
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .schedule-map-pin {
+ animation: none;
+ }
+}
+.schedule-map-popup-title {
+ display: block;
+ margin-bottom: 0.2rem;
+}
+.schedule-map-popup-list {
+ margin: 0;
+ padding-left: 1.1rem;
+}
+.schedule-map-popup-list li {
+ margin: 0.15rem 0;
+ font-size: 0.9rem;
+}
+.schedule-map-popup-list li span {
+ display: block;
+ color: var(--sl-color-gray-3);
+ font-size: 0.82rem;
+}
+.schedule-map-locate-status {
+ margin: 0.3rem 0 0;
+ color: var(--sl-color-gray-3);
+ font-size: 0.85rem;
+}
+.schedule-locate {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ border: 0;
+ border-radius: 50%;
+ background: #fff;
+ color: #1e88e5;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
+ cursor: pointer;
+}
+.schedule-locate:hover {
+ background: #f0f7ff;
+}
+.schedule-locate.is-active {
+ background: #1e88e5;
+ color: #fff;
+}
+.schedule-locate svg {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+.schedule-locate-marker {
+ background: transparent;
+ border: 0;
+}
+.schedule-locate-dot {
+ display: block;
+ width: 18px;
+ height: 18px;
+ border: 3px solid #fff;
+ border-radius: 50%;
+ background: #1e88e5;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 0 0 8px rgba(30, 136, 229, 0.18);
+}
+
/* ── 技術的な補足(本文より一段小さい注記。ノートとは区別する) ── */
.tech-note {
margin: 1.6rem 0;