宝くじ高額当選売場マップの開発 - 地図アプリ実装の実践
「高額当選が出やすい宝くじ売場はどこだろう?」——そんな疑問から始まったのが、「宝くじ高額当選売場マップ」の開発プロジェクトです。全国の宝くじ売場データをGoogleマップ風に可視化し、ユーザーが近くの当選実績がある売場を簡単に探せるツールを作りました。
この記事では、Leaflet.jsを使った地図アプリの実装、300件以上の売場データの処理、マーカークラスタリング、Firebase Hostingでのデプロイまで、開発の全プロセスを詳しく解説します。地図アプリを作りたい方の参考になれば幸いです。
プロジェクトの概要
開発の動機
きっかけは、年末ジャンボ宝くじを買う前に「どこで買うのがいいか?」と調べたことでした。宝くじ公式サイトには高額当選売場のリストはあるものの、テキストの羅列で非常に見にくい。地図上で一目で分かるツールがあれば便利だと思い、開発を決意しました。
主な機能
- 地図上に売場を表示:300以上の高額当選売場をマーカー表示
- マーカークラスタリング:ズームアウト時に近接マーカーをまとめる
- 売場詳細表示:クリックで当選実績、住所、営業時間を表示
- 現在地から検索:GPS機能で最寄りの売場を表示
- 都道府県別フィルター:地域を絞って検索
技術スタック
- 地図ライブラリ:Leaflet.js
- クラスタリング:Leaflet.markercluster
- データ形式:CSV → JSON変換
- ホスティング:Firebase Hosting
- 開発期間:約2週間(実働20時間)
なぜLeaflet.jsを選んだか
Google Maps API との比較
| 項目 | Google Maps API | Leaflet.js |
|---|---|---|
| 料金 | 月28,000読み込みまで無料 超過で従量課金 |
完全無料 |
| 導入 | APIキー必要 | CDNで即利用可能 |
| カスタマイズ | ★★★☆☆ | ★★★★★ |
| 軽量性 | ★★☆☆☆ | ★★★★★ |
| 機能 | ★★★★★ | ★★★★☆ |
個人開発で無料運用を重視したため、Leaflet.jsを選択しました。
Leaflet.jsのメリット
- 完全無料:商用利用もOK、制限なし
- 軽量:39KBのみ(Google Maps APIの1/10以下)
- プラグイン豊富:クラスタリング、ヒートマップなど
- 学習コスト低:ドキュメントが分かりやすい
開発ステップ1:基本的な地図の表示
Leaflet.jsの導入
CDN経由で簡単に導入できます:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
#map {
width: 100%;
height: 600px;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>
地図の初期化
// 地図を初期化(東京駅を中心に表示)
const map = L.map('map').setView([35.6812, 139.7671], 13);
// タイル層を追加(OpenStreetMap)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
これだけで、インタラクティブな地図が表示されます!
マーカーの追加
// 西銀座チャンスセンターにマーカーを追加
const marker = L.marker([35.6717, 139.7643]).addTo(map);
// ポップアップを追加
marker.bindPopup('<b>西銀座チャンスセンター</b><br>高額当選多数!');
開発ステップ2:データの準備と処理
データ収集
宝くじ公式サイトから高額当選売場のデータをスクレイピング...と思いましたが、利用規約でNGだったため、手動でCSVファイルを作成しました。
CSVファイル例(lottery_shops.csv)
name,address,prefecture,lat,lng,wins,notes
西銀座チャンスセンター,東京都中央区銀座4-1,東京都,35.6717,139.7643,523,1等最多
大阪駅前第4ビル特設売場,大阪府大阪市北区梅田1-11-4,大阪府,34.7024,135.4959,312,近畿最多
名駅前チャンスセンター,愛知県名古屋市中村区名駅1-1-4,愛知県,35.1706,136.8816,187,中部最多
CSVをJSONに変換
Excelで編集しやすいCSVで管理し、JavaScriptでJSONに変換:
// Papa Parse ライブラリを使用(CDN経由)
// <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
Papa.parse('lottery_shops.csv', {
download: true,
header: true,
complete: function(results) {
const shops = results.data;
addMarkersToMap(shops);
}
});
複数マーカーの追加
function addMarkersToMap(shops) {
shops.forEach(shop => {
// 座標が有効な場合のみマーカー追加
if (shop.lat && shop.lng) {
const marker = L.marker([parseFloat(shop.lat), parseFloat(shop.lng)])
.addTo(map);
// ポップアップ内容
const popupContent = `
<div class="shop-popup">
<h3>${shop.name}</h3>
<p><strong>住所:</strong> ${shop.address}</p>
<p><strong>高額当選:</strong> ${shop.wins}回</p>
<p>${shop.notes}</p>
</div>
`;
marker.bindPopup(popupContent);
}
});
}
開発ステップ3:マーカークラスタリング
問題:マーカーが多すぎて見づらい
300個以上のマーカーを全て表示すると、地図がマーカーだらけで非常に見づらくなりました。特に東京都内は売場が密集しており、マーカーが重なって個別にクリックできません。
解決策:Leaflet.markercluster
ズームレベルに応じて、近接するマーカーを自動的にまとめるプラグインを導入:
<!-- CDN でクラスタリングプラグインを読み込み -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
クラスタリングの実装
// マーカークラスターグループを作成
const markers = L.markerClusterGroup({
// クラスターアイコンのカスタマイズ
iconCreateFunction: function(cluster) {
const count = cluster.getChildCount();
let size = 'small';
if (count > 100) size = 'large';
else if (count > 20) size = 'medium';
return L.divIcon({
html: `<div><span>${count}</span></div>`,
className: 'marker-cluster marker-cluster-' + size,
iconSize: L.point(40, 40)
});
},
// 最大ズームレベル(これ以上拡大するとクラスター解除)
disableClusteringAtZoom: 18
});
// マーカーをクラスターグループに追加
shops.forEach(shop => {
if (shop.lat && shop.lng) {
const marker = L.marker([parseFloat(shop.lat), parseFloat(shop.lng)]);
marker.bindPopup(createPopupContent(shop));
markers.addLayer(marker);
}
});
// クラスターグループを地図に追加
map.addLayer(markers);
クラスターアイコンのスタイリング
.marker-cluster {
background-color: rgba(37, 99, 235, 0.6);
border-radius: 50%;
text-align: center;
color: white;
font-weight: bold;
}
.marker-cluster-small {
width: 40px;
height: 40px;
}
.marker-cluster-medium {
width: 50px;
height: 50px;
background-color: rgba(234, 88, 12, 0.6);
}
.marker-cluster-large {
width: 60px;
height: 60px;
background-color: rgba(220, 38, 38, 0.6);
}
これにより、ズームアウト時は「東京都: 85件」のようにまとめて表示され、ズームインすると個別のマーカーが表示されるようになりました。
開発ステップ4:検索とフィルター機能
都道府県別フィルター
// HTMLセレクトボックス
<select id="prefecture-filter">
<option value="">全国</option>
<option value="東京都">東京都</option>
<option value="大阪府">大阪府</option>
<!-- ... その他の都道府県 -->
</select>
// JavaScript
document.getElementById('prefecture-filter').addEventListener('change', (e) => {
const selectedPref = e.target.value;
// 既存のマーカーをクリア
markers.clearLayers();
// フィルタリングして再追加
const filteredShops = selectedPref
? shops.filter(shop => shop.prefecture === selectedPref)
: shops;
filteredShops.forEach(shop => {
if (shop.lat && shop.lng) {
const marker = L.marker([parseFloat(shop.lat), parseFloat(shop.lng)]);
marker.bindPopup(createPopupContent(shop));
markers.addLayer(marker);
}
});
// 選択された地域に地図を移動
if (selectedPref && filteredShops.length > 0) {
const bounds = L.latLngBounds(
filteredShops.map(shop => [parseFloat(shop.lat), parseFloat(shop.lng)])
);
map.fitBounds(bounds);
}
});
現在地から最寄りの売場を探す
document.getElementById('find-nearby').addEventListener('click', () => {
if (!navigator.geolocation) {
alert('お使いのブラウザは位置情報をサポートしていません');
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const userLat = position.coords.latitude;
const userLng = position.coords.longitude;
// ユーザーの位置にマーカー追加
L.marker([userLat, userLng], {
icon: L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
})
}).addTo(map).bindPopup('現在地').openPopup();
// 地図を現在地に移動
map.setView([userLat, userLng], 14);
// 最寄りの売場を計算
const nearest = findNearestShop(userLat, userLng, shops);
if (nearest) {
alert(`最寄りの高額当選売場: ${nearest.name}(約${nearest.distance.toFixed(1)}km)`);
}
},
(error) => {
alert('位置情報の取得に失敗しました');
}
);
});
function findNearestShop(userLat, userLng, shops) {
let nearestShop = null;
let minDistance = Infinity;
shops.forEach(shop => {
if (shop.lat && shop.lng) {
const distance = calculateDistance(
userLat, userLng,
parseFloat(shop.lat), parseFloat(shop.lng)
);
if (distance < minDistance) {
minDistance = distance;
nearestShop = { ...shop, distance };
}
}
});
return nearestShop;
}
// Haversine公式で2点間の距離を計算(km)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // 地球の半径(km)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
開発ステップ5:Firebase Hostingでデプロイ
Firebase Hostingを選んだ理由
- 無料:月10GBまで無料(個人開発には十分)
- HTTPS自動:SSL証明書が自動で設定される
- 高速:CDN経由で配信
- 簡単:CLIコマンド1つでデプロイ
デプロイ手順
# 1. Firebase CLIをインストール
npm install -g firebase-tools
# 2. Firebaseにログイン
firebase login
# 3. プロジェクトを初期化
firebase init hosting
# 質問に回答:
# - Use an existing project → 既存プロジェクトを選択(または新規作成)
# - What do you want to use as your public directory? → public
# - Configure as a single-page app? → No
# - Set up automatic builds and deploys with GitHub? → No
# 4. HTMLファイルをpublicフォルダに配置
# 5. デプロイ
firebase deploy
# デプロイ完了! https://your-project.web.app/ でアクセス可能
苦労した点と解決策
1. 緯度経度の取得
問題:売場の住所から緯度経度を手動で調べるのが大変(300件以上)
解決策:Google Geocoding APIで一括変換
// Node.jsスクリプトで一括変換
const axios = require('axios');
const fs = require('fs');
async function geocodeAddress(address) {
const API_KEY = 'YOUR_GOOGLE_API_KEY';
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${API_KEY}`;
const response = await axios.get(url);
if (response.data.results.length > 0) {
const location = response.data.results[0].geometry.location;
return { lat: location.lat, lng: location.lng };
}
return null;
}
// CSVファイルを読み込んで緯度経度を追加
// (実装は省略)
2. モバイル対応
問題:スマホで見るとマーカーが小さくてタップしにくい
解決策:レスポンシブデザイン + タッチ最適化
/* モバイル対応 */
@media (max-width: 768px) {
#map {
height: 400px; /* PC: 600px → スマホ: 400px */
}
.leaflet-popup-content {
font-size: 14px;
max-width: 250px;
}
/* マーカーを少し大きく */
.leaflet-marker-icon {
transform: scale(1.2);
}
}
3. 読み込み速度
問題:300件のマーカーを一度に読み込むと遅い
解決策:遅延読み込み + クラスタリング
- 初期表示は現在のビューポート内のマーカーのみ
- 地図移動時に追加読み込み
- クラスタリングで描画負荷を軽減
まとめ
開発で学んだこと
- Leaflet.jsは個人開発に最適:無料、軽量、簡単
- クラスタリングは必須:100件以上のマーカーには必須機能
- データ整備が重要:緯度経度の精度がUXを左右する
- Firebase Hostingは便利:無料でHTTPS、CDN、自動デプロイ
今後の改善予定
- 当選実績のグラフ表示(Chart.js)
- ルート検索機能(最寄り駅からのルート)
- ユーザーレビュー機能
- 年末ジャンボなどイベント時の混雑予測
地図アプリは、データさえあれば比較的簡単に作れます。Leaflet.jsを使えば、Google Maps APIのようなコストを気にせず、自由にカスタマイズできます。ぜひ、あなたも何か地図アプリを作ってみてください!