小山市オープンデータでAEDマップを作った話
はじめに
シビックテック任意団体 Code for OYAMA (栃木県小山市) の石橋です。
小山市のAED(自動体外式除細動器)設置場所を地図で確認できるWebアプリケーション「おやまAEDマップ」を作ってみました。緯度経度情報を含んだデータからその位置を地図上にマーキングし、必要に応じて詳細情報を表示するといったアプリケーションです。
プロジェクト概要
開発背景
小山市がオープンデータカタログとして公開しているいくつものオープンデータがあります。これを使って何かマップアプリを作ってみようと思ったのが開発のきっかけです。いくつかデータを眺めてみた中で、AED設置場所のデータが最初の題材として良さそうでした。(データが比較的新しくある程度データのボリュームもある)
ただ、せっかく作るならAED専用ではなく、他の用途にも転用できる汎用的なシステムにしておこうと考えました。後でレストランマップとか観光地マップとかにも使えそうですしね。
主な機能
- 📍 地図上での施設位置表示(OpenStreetMap + Leaflet.js)
- 🏪 施設詳細情報の表示(住所、電話番号、利用時間等)
- 🖼️ 画像ギャラリー・モーダル表示機能
- 🔐 管理者による施設情報のCRUD操作
- 📊 CSVファイルからの一括データインポート
- 🎯 カテゴリフィルター機能
- 📱 レスポンシブデザイン
実際の使い方
マップ表示
起動時に小山市役所を中心とした地図が表示され、AED設置施設にカテゴリ別に色分けされたマーカーが置かれます。地図はドラッグで移動でき、ピンチイン・アウトで縮尺を拡大縮小ができます。
カテゴリフィルター機能

施設の種類別に表示を絞り込むことができます。「コンビニだけ表示したい」「学校だけ見たい」といった使い方が可能です。カテゴリはAEDを探している曜日や時間帯にその施設が稼働しているかどうかの目安になるかと思います。
施設ポップアップ、施設詳細ページ

マーカーをクリックすると各施設の情報を表示するポップアップが表示されます。住所、電話番号、利用時間、小児対応機器の有無など、AED利用に必要な情報がまとめて確認できます。「詳細を見る」ボタンで詳細ページを表示できます。また画像がある場合はギャラリー形式で表示され、クリックするとモーダルで拡大表示されます。
技術構成
技術スタックの選択理由
フロントエンド:
- Leaflet.js - 軽量で高機能な地図ライブラリ
- OpenStreetMap - 無料で商用利用可能な地図データ
- Vanilla JavaScript - フレームワークに依存しないシンプルな実装
バックエンド:
- PHP 7.4+ - さくらインターネット等の共用サーバーで動作
- SQLite3 - ファイルベースで軽量、メンテナンスが容易
- WALモード - 同時実行性能を向上
要は「できるだけシンプルに、でも必要十分な機能」という感じで選びました。共用サーバーでも動くし、メンテナンスも楽だしで、実用性重視の構成です。
これはCode for OYAMAは基本的には無償でシビックテック活動を行う任意団体であるため、常にプロフェッショナルなITエンジニアが稼働できる、運用できるわけでもなく技術的ハードルを下げて分かりやすく、という面もあります。
システム設計の工夫
1. 完全汎用化設計
一番こだわったのは、動作・表示部、データ構造、データと大きく分けた構成にして、そのうちデータ構造に関して設定ファイル(config.php)で大部分をカスタマイズできる仕組みにしたことです。
'app' => [
'name' => 'おやまAEDマップ',
'facility_name' => 'AED設置場所',
'categories' => ['公共施設', '学校・教育機関', 'コンビニエンスストア', '医療機関', 'その他'],
'field_labels' => [
'name' => '施設名',
'category' => 'カテゴリー',
'phone' => '電話番号',
// ...
]
],
'map' => [
'initial_latitude' => 36.3141, // 小山市中心
'initial_longitude' => 139.8006,
'initial_zoom' => 14
]
この設定をいじるだけで、レストランマップ、観光地マップ、公共施設マップなど、いろんな用途に使い回せるようになってます。
2. CSVからデータベースへのアプローチ
実際にオープンデータをWeb表示するだけなら、提供されるCSVデータをそのまま活用してしまえばいいのですが、そこに情報を追加したいとか修正したい、それもWeb上から簡単にできたらいいという要素を考慮して、CSVをデータベースに読み込むという方法をとっています。
これにより、元のオープンデータを活用しつつ、管理者が画像を追加したり、詳細な説明を補完したり、間違いを修正したりといった運用が可能になります。
3. 柔軟なデータベース設計
データベースも、特定の用途に縛られないようシンプルにしました:
CREATE TABLE facilities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
address TEXT,
phone TEXT,
category TEXT,
-- AED固有フィールド
pediatric_support TEXT,
available_days TEXT,
-- 汎用フィールド
website TEXT,
:
:
実は、このデータベース構成自体も下記のようにconfig.phpで定義できるようにしています。テーブル構造、インデックス、外部キー制約まで設定ファイルで管理できるので、用途に応じてフィールドを追加・削除したり、全く新しいテーブル構成にすることも可能です。
'database' => [
'tables' => [
'facilities' => [
'columns' => [
'id' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
'name' => 'TEXT NOT NULL',
'lat' => 'REAL NOT NULL',
'lng' => 'REAL NOT NULL',
'address' => 'TEXT',
:
:
基本構造は汎用的にしているので、そこにAED固有のフィールドを追加したりなどあらゆる用途に対応しやすくなってます。
3. セキュリティを重視した2段階構成
システムは最小構成とフル機能版の2つの構成を提供します:
最小構成(一般ユーザー向け)
- 4つのPHPファイル + CSS
- 管理機能なし
- セキュリティリスク最小化
フル機能版(管理者向け)
- 12のPHPファイル + CSS
- 完全な管理機能
- CSRF対策、セッション管理等
フル機能版でもセキュリティ対策は入れてはいますが、最小構成をとることによってより安全に運用することができます。
UI/UX の工夫
1. 3分割CSS設計
css/
├── common.css # 共通スタイル(ヘッダー、基本レイアウト)
├── main.css # メインページ・詳細ページ用
└── admin.css # 管理画面用
この分け方だと、管理画面とユーザー画面で別々にスタイルを調整できるし、共通部分の管理も楽になります。
2. レスポンシブ対応
AEDって緊急時に使うものなので、スマホでの使いやすさを特に意識して、地図のタップ操作とか、片手でも操作しやすいようにCSSで定義しています。
3. カテゴリフィルター機能
// リアルタイムフィルタリング
function filterMarkers() {
const checkedCategories = Array.from(
document.querySelectorAll('#categoryFilter input:checked')
).map(cb => cb.value);
facilities.forEach(facility => {
const marker = facilityMarkers[facility.id];
if (checkedCategories.includes(facility.category)) {
map.addLayer(marker);
} else {
map.removeLayer(marker);
}
});
}
「コンビニだけ表示したい」とか「学校だけ見たい」みたいなときに便利です。
データの取り扱いで工夫した点
1. CSVインポート機能の実装
小山市のオープンデータをCSVで一括取り込みする機能で工夫したのが、施設名からのカテゴリ自動判定です。
function categorize_facility($facility_name) {
// コンビニエンスストア
if (preg_match('/ファミリーマート|セブンイレブン|ミニストップ/', $facility_name)) {
return 'コンビニエンスストア';
}
// 医療機関
if (preg_match('/歯科|医院|病院|クリニック/', $facility_name)) {
return '医療機関';
}
// 公共施設
if (preg_match('/市役所|小山市|庁舎|出張所|図書館|博物館|資料館|公園|保育所|センター/', $facility_name)) {
return '公共施設';
}
// 学校・教育機関
if (preg_match('/小学校|中学校|高等学校|義務教育学校|大学/', $facility_name)) {
return '学校・教育機関';
}
return 'その他';
}
元のCSVデータにはカテゴリ情報がないので、施設名の文字列をパターンマッチングして自動的にカテゴリ分けする仕組みを作りました。これで数百件のデータを手動で分類する手間が省けます。
この辺のマッチングは地味な作業にはなるのですが、世間一般的なカテゴライズをする必要はなく、登録されているデータについてのみ対応すればいいのでそれほど複雑ではありません。例えば上記の例でコンビニの項に「ローソン」が無いのですがこれは提供されているオープンデータの中にローソンがなかったためです。(ローソンにAEDが無いのか、小山市役所で把握していないのかは不明)
汎用性を重視した設計の成果
config.phpでの簡単カスタマイズ
実際、他の用途に転用するのはこんなに簡単です:
// レストランマップへの転用例
'app' => [
'name' => '小山グルメマップ',
'facility_name' => 'レストラン',
'categories' => ['和食', '洋食', '中華', 'イタリアン', 'カフェ']
]
実装レベルでのカスタマイズポイント
ただ、設定だけだと限界があるので、プログラマーが手を入れやすいポイントも用意しました:
-
facility_detail.php- 詳細ページの表示項目調整 -
admin_add.php/admin_edit.php- 管理画面のフォーム項目 -
api_facilities.php- APIで公開する情報の制御
つまり、設定ファイルで基本的なところは簡単にカスタマイズして、細かいUI/UXに関わる部分は実際にコードをいじって最適化という、両方のバランスを取った感じです。
セキュリティへの配慮
段階的セキュリティ設計
-
基本セキュリティ(最小構成)
- 入力値サニタイズ
- SQLインジェクション対策
- 設定ファイルのWeb外配置
-
高度セキュリティ(フル機能版)
- CSRF対策
- セッション管理
- ファイルアップロード制限
- 定期的なタイムアウト
API情報制限の実装
// セキュアなフィールド選択
$res = $db->query('
SELECT id, name, lat, lng, address, category
FROM facilities
'); // 機密情報・内部管理情報は除外
公開用のAPIでは、内部管理情報とかは除外して、本当に必要な情報だけを出すようにしています。
今後の展望
汎用的に作ったので、こんな用途にも使えそうです:
- 🍽️ レストランマップ - グルメ情報の提供
- 🏛️ 観光地マップ - 観光情報システム
- 🏥 公共施設マップ - 市民向け施設案内
- 🚻 多目的トイレマップ - バリアフリー情報
- 🚗 EV充電スタンドマップ - 充電設備情報
設定をちょっといじって、必要に応じてコードも調整すれば、これらの用途にも対応できるようになってます。
このあたりの他のマップへの応用については具体的にどのような手順でやると良いのかといったことを別の記事で紹介したいと思います。
まとめ
今回のAEDマップ開発を通して、「使い捨て」じゃなくて「使い回せる」システムを作ることで、こんなメリットがありました:
- 再利用性 - 他の用途への転用が容易
- 保守性 - シンプルな構成でメンテナンスが容易
- 拡張性 - 設定とコードの両面で柔軟にカスタマイズ可能
- セキュリティ - 用途に応じた適切なセキュリティレベル
開発者目線でも、設定ファイルでサクッと基本設定して、こだわりたい部分だけコードを調整すれば、効率よく良いマップシステムが作れる仕組みになったと思います。
技術選択も、「実際に運用できるかどうか」を重視して、共用サーバーで動くとか、メンテが楽とか、コストも抑えられるとか、実用性を優先しました。
似たようなマップシステムを作ろうと思ってる人の参考になれば嬉しいです。
プロジェクト情報:
- webサイト: おやまAEDマップ
- GitHub: oyama_aed_map
- 開発者: 石橋利也 Code for OYAMA
Discussion