まえがき
「Googleマップを使えば、どんな地図アプリでも簡単に作れる」
——そう思っている人は、意外と多いのではないでしょうか。
確かに、Google Maps API は高機能で、商用サイトでも広く使われています。
しかし最近では、「地図をもっと自由に」「コストを抑えて」「独自のデザインで」活用したいというニーズが高まっています。
そこで注目されているのが、オープンソースの地図技術「OpenStreetMap(OSM)」 と、
軽量な JavaScript ライブラリ Leaflet.js(リーフレット) の組み合わせです。
Leaflet は地図アプリ開発の“定番フレームワーク”として世界中で利用されており、
たった数行のコードで美しい地図を表示できます。
しかも、OSMのオープンデータを使えば無料で地図を提供できるため、
小規模なサイトや個人プロジェクト、自治体・観光協会の案内ページなどに最適です。
今回のチュートリアルでは、
「観光地をマップ上に可視化し、ジャンル別に表示・非表示を切り替え、
さらにルート表示まで行えるインタラクティブな地図アプリ」を
HTML・CSS・JavaScriptだけで構築する方法を解説します。
基本から応用までのステップを、
実際のコードサンプルとともに丁寧に追っていくので、
プログラミング初心者でも最後まで作りきれる内容です。
このチュートリアルを終える頃には、
あなたも 「自分の街を自分の手で描く」 地図クリエイターになっているはずです。
はじめに
なぜ Leaflet + OpenStreetMap なのか
Web上で地図を扱いたいとき、Google Maps や Mapbox など選択肢は多くあります。しかし、以下の理由から Leaflet + OpenStreetMap(OSM) の組み合わせは非常に魅力的です:
- オープン性:Leaflet は軽量なオープンソース JavaScript ライブラリです 。
- ライセンス自由度:OpenStreetMap のデータはオープンライセンスであり、商用利用も可能。ただし適切な属性表示(attribution 表示)は必要です。
- 軽量で拡張性あり:Leaflet 自体は小さく、プラグインも豊富。必要な機能だけを追加できる柔軟性があります。
- コミュニティと事例の豊富さ:多くのサイトやシステムで使われており、事例や情報がネット上に多くあります 。
- タイルソースを選べる自由:OSM以外のタイルソースも使える設計で、見た目/性能の選択肢が拡がります。
とはいえ、デメリットもあります。たとえば、タイル提供元の制約(過剰なリクエスト制限やキャッシュ制御など)、ルート検索 API の別構築、地図データの更新・同期、負荷分散など運用上の考慮は必要です。
このチュートリアルでは、それらを踏まえつつも「観光地マップを自前で作る」ための実践手順をできるだけ丁寧に解説します。
本チュートリアルで作るもののゴール
最終的には、次のような機能を備えた「観光地マップアプリ」を作ることを目指します:
- 都道府県や都市などの地図中心表示
- 観光スポット(地点)を GeoJSON 形式で管理
- マーカーでスポットを配置し、クリックで名称・説明・画像などをポップアップ表示
- 観光ジャンル別に表示を絞る UI 制御
- 出発地/目的地を指定して、地図上にルートを描画
- モバイル対応、インタラクティブな UX
- 高速化・最適化を意識した実装
この記事を通じて「地図を扱う基本」「GeoJSON を使ったデータ構造」「インタラクティブ操作の実装法」「運用上の注意点」などを体系的に学べるように設計します。
前提知識・対象読者
本記事は Web フロントエンド(HTML/CSS/JavaScript)の基本的な知識を持つ方向けを対象としています。ただし、Leaflet や地理情報に不慣れな方でも読み進められるよう、逐次説明を入れます。
また、バックエンド技術(サーバーサイド言語、DB 等)は深く触れませんが、GeoJSON を返す API 設計などについては軽く触れる場面があります。
Leaflet と OpenStreetMap の基礎知識
Leaflet とは何か/特徴・利点
Leaflet(リーフレット)は、Web 上でインタラクティブ地図を扱うための軽量 JavaScript ライブラリです。HTML5/CSS3 に対応し、モバイルでも快適に動作する設計です。leafletjs.com+2wiki.openstreetmap.org+2
主な特徴:
- 軽量設計:基本ライブラリは小さく、必要な機能をプラグインで追加できる
- 直感的 API:
L.map
、L.tileLayer
、L.marker
といった関数体系が分かりやすい - 拡張性:多くのプラグイン(クラスタリング、ヒートマップ、ルーティング、GeoJSON 操作など)が存在
- 多様なレイヤー対応:タイルレイヤー、ベクターレイヤー、GeoJSON、WMS など
- イベント対応:クリック、パン、ズームなど、各種操作に応じたイベントハンドリングが可能
一方で、Leaflet は GIS が得意とする高度なジオプロセシング機能(空間クエリ、ジオメトリ演算など)は基本的に含んでおらず、必要な場合は別ライブラリ(Turf.js や他 GIS ライブラリ)と組み合わせて使うことが多いです。
OpenStreetMap の概要と利用の注意点
OpenStreetMap(OSM)は、オープンな地理空間データプロジェクトであり、世界中のユーザーが地図情報を編集・参照できます。地図タイルやデータを自前で利用できる自由度が高い点が魅力です。
ただし、使う際には以下の注意があります:
- タイル提供元の制約:公式の OSM タイルサーバーは、商用サービスや高トラフィック用途を前提とした提供ではありません。過剰な負荷をかけるとアクセス制限を受ける可能性があります。
- キャッシュと CDN:自分でタイルキャッシュや CDN 配信を検討することが望ましい。
- 属性表示義務(Attribution):地図下部などに「© OpenStreetMap contributors」などの表示が必要です。
- データ更新:OSM データは随時更新されているため、キャッシュや定期更新の戦略を持つとよいです。
- 利用規約・マナー:API 利用制限、利用ルール、使途の範囲などを確認して使うこと。
タイルサーバー(地図地図タイル)の仕組み
地図アプリで使われる「タイル(タイル画像)」は、ズームレベル z
、X/Y の座標で構成される小さな画像単位の地図ブロックです。ブラウザは必要なズームレベルと範囲に応じて適切なタイル画像をリクエストし、地図全体を構成します。
Leaflet では、以下のようにタイル URL テンプレートを指定して読み込むことができます。
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
上記の {z}
、{x}
、{y}
はズーム/X座標/Y座標にそれぞれ展開されます。{s}
はサブドメイン(例:a, b, c)で並列リクエスト分散に使われます。
この方式のおかげで、クライアント側では効率よくタイルをレンダリングでき、必要なタイルのみを逐次取得できる構造になります。
開発環境の準備
ディレクトリ構成
まずは手を動かすためのファイル構成を決めましょう。例えば次のような構成がシンプルで扱いやすいです。
/project-root/
index.html
css/
style.css
js/
main.js
data/
spots.geojson
assets/
icons/
images/
README.md
index.html
:地図アプリ本体ページcss/style.css
:地図表示用および追加 UI 用スタイルjs/main.js
:メインの JavaScript ロジックdata/spots.geojson
:観光スポット情報を保持する GeoJSONassets/
:アイコン画像、スポット画像など
このあたりは、ご自身のブログ構成やファイル配置ルールに合わせて調整しても構いません。
HTML, CSS, JavaScript ファイルのひな形
まず、index.html
の基本骨格を作ります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>観光地マップ with Leaflet & OSM</title>
<!-- Leaflet CSS を読み込む -->
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha512-…(省略)"
crossorigin=""
/>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1>観光地マップ</h1>
<div id="map"></div>
<!-- 必要なら UI エレメント(例:ジャンル選択、ルート入力フォームなど) -->
<div id="controls">
<!-- ジャンル選択など -->
</div>
<!-- Leaflet JS を読み込む -->
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha512-…(省略)"
crossorigin=""
></script>
<script src="js/main.js"></script>
</body>
</html>
css/style.css
には最低限、地図 div の高さを確保するスタイルを入れておきます。
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#map {
width: 100%;
height: 80vh; /* ブラウザ高さの 80% を地図領域に使う例 */
}
#controls {
padding: 1em;
/* 必要に応じて UI 部分のスタイル */
}
js/main.js は後ほどロジックを入れていきます。まずは空の構造だけ作っておきます:
js
コードをコピーする
// main.js
// 初期化処理
function initMap() {
// 地図生成やマーカー読み込み等をここに書く
}
// ページ読み込み後に実行
window.addEventListener('load', () => {
initMap();
});
Leaflet の読み込み(CDN/npm)
上の HTML 例では CDN 版を使う形式を採っていますが、モジュール構成やビルドツールを使っている場合は npm で Leaflet をインストールして取り込む方法でも構いません。
npm install leaflet
そして JavaScript 側(たとえば bundler 環境)で次のように
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
という形で取り込むやり方です。ただし、この方式を使う場合、アイコン画像パスや CSS 読み込み処理の設定に注意が必要です(バンドル環境に応じた設定)。
このチュートリアルでは、手軽で理解しやすい CDN 読み込み方式を中心に説明を進めます。
CSSで地図 div に高さ確保
地図を描画する #map
要素には高さがゼロだと表示されないので、必ず CSS で高さを指定する必要があります(vh 単位や固定 px 単位など)。
例として先に示した height: 80vh
のような指定や、height: 500px
などでも構いません。レスポンシブ対応を考えるなら、@media
を使ってスマホ・PCで高さ調整をする方法も後で紹介します。
地図を表示する(ベースマップ設定)
ここから、実際に地図を画面に表示する手順を進めていきます。
地図初期表示位置とズームレベルの設定
main.js
に以下のような基本コードを入れます。
function initMap() {
// 観光地マップの初期中心位置(例:東京駅周辺)
const centerLatLng = [35.681236, 139.767125]; // 緯度, 経度
const initialZoom = 13;
// 地図オブジェクト生成
const map = L.map('map').setView(centerLatLng, initialZoom);
// タイルレイヤー(OSM)を追加
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// オプション: 他操作設定
// map.zoomControl.setPosition('topright');
}
このコードを走らせると、#map
の位置に地図が表示され、指定した中心とズームレベルで初期表示されます。
OpenStreetMap タイルの追加
上記の L.tileLayer(...)
が OSM のタイルを地図に読み込む役割です。タイルの URL テンプレートとオプション(maxZoom
, attribution
など)を指定しています。
tlesLayer
のオプションでは、以下のような設定がよく使われます:
maxZoom
: 最大ズームレベルminZoom
: 最小ズームレベルattribution
: 地図著作権表示subdomains
:{s}
に使うサブドメイン配列(例:['a','b','c']
)tileSize
,zoomOffset
:タイルの仕様調整
属性表示(Attribution 表示)
タイルプロバイダー(この場合 OSM)には著作権表示義務(Attribution)があるため、地図下部などに必ず表示を行います。Leaflet の attribution
オプションを使って設定可能です。
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
このように HTML タグを使って地図表示元へのリンクを含む表記を記述します。
タイルサービス利用の注意(利用規約、負荷、キャッシュなど)
- 公式 OSM タイルサーバーの制約
多くのプロジェクトでは、公式 OSM タイルサーバー(tile.openstreetmap.org
)をそのまま商用サイトで使うのは推奨されません。過剰リクエスト時には制限が課される可能性があります。 - 代替タイル提供サービス
Mapbox、Maptiler、Stadia Maps、Carto 等、有償または無料プランのタイルサービスを併用する手段があります。 - キャッシュ/CDN 利用
プロキシキャッシュや CDN を使って、タイル取得負荷を軽減する構成が望まれます。 - リクエスト節約
不要なズームレベルでの読み込みを制限したり、不要なレイヤー読み込みを遅延化(Lazy Load)する工夫が重要です。
マーカー配置とポップアップ表示
次は観光スポットをマーカーで表示し、クリック時に詳細情報を表示するようにします。
単一マーカーの追加とポップアップ
まずは簡単に、ひとつのマーカーを追加してポップアップを表示する例です。
// 地図 map が生成済とする
const marker = L.marker([35.6895, 139.6917]).addTo(map);
marker.bindPopup('<b>東京タワー</b><br>東京の有名な展望塔です。');
.bindPopup(...)
でマーカーポップアップ内容を指定できます。.openPopup()
をつけると初期表示時からポップアップ展開できます。
複数マーカーを配列で管理して追加
観光スポットが複数ある場合は、JavaScript の配列で管理し、ループでマーカーを生成する形が一般的です。
const spots = [
{
name: '東京タワー',
desc: '東京の有名な展望塔です。',
lat: 35.65858,
lng: 139.745433,
},
{
name: '浅草寺',
desc: '浅草の歴史あるお寺。',
lat: 35.714765,
lng: 139.796655,
},
// 他スポット…
];
spots.forEach((spot) => {
const marker = L.marker([spot.lat, spot.lng]).addTo(map);
marker.bindPopup(`<b>${spot.name}</b><br>${spot.desc}`);
});
この形式であればスポット数が増えても拡張しやすくなります。
カスタムアイコンを使う
マーカーアイコンをオリジナルの画像に変更する例
const customIcon = L.icon({
iconUrl: 'assets/icons/spot-icon.png',
iconSize: [32, 37], // 幅・高さ(px)
iconAnchor: [16, 37], // アイコンの尖った位置(マーカー先端)を基準点に
});
spots.forEach((spot) => {
const marker = L.marker([spot.lat, spot.lng], { icon: customIcon }).addTo(map);
marker.bindPopup(`<b>${spot.name}</b><br>${spot.desc}`);
});
アイコン画像のパスやサイズ調整を適切に行うことで、見栄えを改善できます。
ポップアップ内に HTML やリンクを入れる
ポップアップには HTML を自由に含められます。
marker.bindPopup(`
<div>
<h3>${spot.name}</h3>
<img src="assets/images/${spot.image}" alt="${spot.name}" width="200">
<p>${spot.desc}</p>
<a href="${spot.url}" target="_blank">公式サイトへ</a>
</div>
`);
画像やリンク、スタイルを入れることで、ユーザにとってリッチな情報を提示できます。
GeoJSON データの読み込みと表示
多数のスポット情報を管理するには、GeoJSON 形式でデータを扱うのが便利です。
GeoJSON とは何か
GeoJSON は地理空間情報を JSON で記述するデータフォーマットで、点(Point)、線(LineString)、面(Polygon)などを表現できます。属性情報(プロパティ)も含められるため、地図アプリでは扱いやすい形式です。
GeoJSON の構造例(観光スポット用):
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "東京タワー",
"desc": "東京の有名な展望塔です。",
"image": "tokyo-tower.jpg",
"url": "https://www.tokyotower.co.jp/"
},
"geometry": {
"type": "Point",
"coordinates": [139.745433, 35.65858]
}
},
{
"type": "Feature",
"properties": {
"name": "浅草寺",
"desc": "浅草の歴史あるお寺。",
"image": "sensouji.jpg",
"url": "https://www.senso-ji.jp/"
},
"geometry": {
"type": "Point",
"coordinates": [139.796655, 35.714765]
}
}
// …他のスポット
]
}
注意:Leaflet は緯度・経度順 [lat, lon]
ではなく、GeoJSON の座標は [longitude, latitude]
の順で定義する点に注意する必要があります。
GeoJSON を使って観光スポット情報を保持する設計
data/spots.geojson
に上記の形式でスポットを記述しておき、クライアント側でこれを取得してマップ上にレンダリングします。この設計により、データと表示ロジックを分離でき、後からスポットの追加・更新が容易になります。
L.geoJSON() による読み込みとマップへの配置
main.js
に以下のようなコードを追加します。
function initMap() {
// …地図初期化処理…
// GeoJSON をフェッチして表示
fetch('data/spots.geojson')
.then((resp) => resp.json())
.then((geojson) => {
L.geoJSON(geojson, {
onEachFeature: onEachSpotFeature,
}).addTo(map);
})
.catch((err) => console.error('GeoJSON 読み込みエラー:', err));
}
// 各 Feature に対する処理
function onEachSpotFeature(feature, layer) {
if (feature.properties && feature.properties.name) {
const popupContent = `
<div>
<h3>${feature.properties.name}</h3>
<p>${feature.properties.desc}</p>
${feature.properties.image ? `<img src="assets/images/${feature.properties.image}" width="200" />` : ''}
${feature.properties.url ? `<br><a href="${feature.properties.url}" target="_blank">公式サイト</a>` : ''}
</div>
`;
layer.bindPopup(popupContent);
}
}
このように、GeoJSON の各 Feature を L.geoJSON
で読み込むと、自動的に適切な Leaflet レイヤー(この場合は点 → マーカー)として変換され、onEachFeature
関数でポップアップ処理などを設定できます。
onEachFeature によるポップアップバインド
onEachFeature(feature, layer)
関数は、GeoJSON の各 Feature に対して呼ばれ、layer
(Leaflet 上のレイヤー)にポップアップやイベントをバインドできます。前述の例では layer.bindPopup(...)
を使っています。
この関数をもっと拡張して、クリックイベントや mouseover
/mouseout
イベントなども設定できます。
スタイリング(スタイル関数)
GeoJSON のレイヤーにスタイルを与えることも可能ですが、点(Point)ではマーカーアイコンで代替することが多く、線・面(Polyline/Polygon)を描く用途で有効です。
たとえば、線(観光ルート)やエリア(観光エリア範囲)を GeoJSON で定義してスタイルを指定する例:
L.geoJSON(geojsonLineOrPolygon, {
style: function(feature) {
return {
color: feature.properties.color || 'blue',
weight: feature.properties.weight || 3,
opacity: feature.properties.opacity || 0.7,
};
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.name) {
layer.bindPopup(feature.properties.name);
}
},
}).addTo(map);
観光地マップでは、線/エリアを使う機会はルート表示やエリアハイライトといった拡張部分で出てきます。
レイヤー制御、グループ化、絞り込み機能
L.layerGroup/L.featureGroup の活用
観光地マップでは、スポットの種類(例:史跡・自然・グルメなど)ごとにマーカーをまとめたい場面があります。Leaflet にはそのための機能として L.layerGroup()
と L.featureGroup()
が用意されています。
L.layerGroup()
:単純に複数レイヤー(マーカーなど)をまとめるためのコンテナ。L.featureGroup()
:L.layerGroup
に加え、グループ全体にイベント(クリック、mouseover など)を設定できる。
const historicalGroup = L.layerGroup();
const foodGroup = L.layerGroup();
const natureGroup = L.layerGroup();
// 各カテゴリに属するマーカーを追加
L.marker([35.71, 139.79]).bindPopup('浅草寺').addTo(historicalGroup);
L.marker([35.67, 139.77]).bindPopup('築地市場').addTo(foodGroup);
L.marker([35.68, 139.73]).bindPopup('日比谷公園').addTo(natureGroup);
// デフォルトでは全部表示
historicalGroup.addTo(map);
foodGroup.addTo(map);
natureGroup.addTo(map);
コントロール(レイヤー切り替え)を追加
Leaflet には地図上でレイヤーを切り替えるUI部品が標準で用意されています。
次のようにして、チェックボックス式のレイヤーコントロールを追加できます。
const baseMaps = {
"OpenStreetMap": osmLayer,
"OSM Humanitarian": humanitarianLayer,
};
const overlayMaps = {
"史跡": historicalGroup,
"グルメ": foodGroup,
"自然": natureGroup
};
L.control.layers(baseMaps, overlayMaps).addTo(map);
これで地図右上にレイヤー選択ウィジェットが追加され、ユーザが表示・非表示を切り替えられるようになります。観光情報のカテゴリ分けに最適です。
UI制御でカテゴリごとに絞る
レイヤー切り替えの代わりに、HTML の <select>
や <button>
を使ってカテゴリごとに絞る方式もよく使われます。
<select id="category">
<option value="all">すべて</option>
<option value="historical">史跡</option>
<option value="food">グルメ</option>
<option value="nature">自然</option>
</select>
JavaScript 側ではイベントを監視して絞り込みを制御します。
document.getElementById('category').addEventListener('change', (e) => {
const val = e.target.value;
[historicalGroup, foodGroup, natureGroup].forEach(g => map.removeLayer(g));
if (val === 'all' || val === 'historical') historicalGroup.addTo(map);
if (val === 'all' || val === 'food') foodGroup.addTo(map);
if (val === 'all' || val === 'nature') natureGroup.addTo(map);
});
UIと連携することで、より自然な操作感を実現できます。
フィルタリング機能の応用
GeoJSON データを大量に扱う場合、クライアント側でフィルタリングしてから描画する方法も有効です。
fetch('data/spots.geojson')
.then(res => res.json())
.then(json => {
const filtered = json.features.filter(f => f.properties.category === 'historical');
L.geoJSON({ type: 'FeatureCollection', features: filtered }).addTo(map);
});
これにより、データを都度再描画する形で軽量に扱うこともできます。
ルート表示・ナビゲーション機能(応用)
ルート API の選定
Leaflet 自体には経路探索機能はありません。そのため、外部のルーティングサービスを利用します。代表的なものとして
サービス名 | 特徴 | 無料枠 |
---|---|---|
OpenRouteService | OSM データを基にしたルート検索 | 1日あたり多数 |
OSRM | 高速なオープンソースルータ | 自前ホスティング可 |
GraphHopper | 商用利用も可能なAPI | 月間制限あり |
ルート取得リクエスト
OpenRouteService の例を見てみましょう。
以下のようなエンドポイントに GET リクエストを送信します。
https://api.openrouteservice.org/v2/directions/foot-walking?api_key=YOUR_KEY&start=139.745433,35.65858&end=139.796655,35.714765
返却される JSON は GeoJSON 形式のルート情報を含んでおり、これを L.geoJSON()
で表示可能です。
GeoJSON によるルート描画
fetch(url)
.then(res => res.json())
.then(data => {
const route = data.features[0];
L.geoJSON(route, {
style: {
color: 'red',
weight: 4
}
}).addTo(map);
});
これで、2地点間のルートが地図上に赤いラインで表示されます。
出発地・目的地の入力フォーム
ユーザーに自由に経路を指定させる場合は、HTML フォームを追加します。
<input id="start" placeholder="出発地(緯度,経度)">
<input id="end" placeholder="目的地(緯度,経度)">
<button id="routeBtn">ルート表示</button>
ボタンクリックで API にリクエストを送り、ルートを描画する仕組みです。
UIを工夫すれば、クリックで地点を選んだり、現在地を自動入力したりもできます。
複数ルート・案内表示
OpenRouteService などは複数ルート案を返すこともあり、これを切り替えて表示することで利便性を向上できます。
さらに、ルートの距離や所要時間をポップアップやサイドバーに表示すれば、簡易ナビとしても機能します。
インタラクティブ性を高める工夫
クリック・ズームイベントの活用
Leaflet では地図やマーカーに多数のイベントを設定できます。
map.on('click', function(e) {
L.popup()
.setLatLng(e.latlng)
.setContent(`緯度: ${e.latlng.lat.toFixed(5)}, 経度: ${e.latlng.lng.toFixed(5)}`)
.openOn(map);
});
地図をクリックした場所の座標を確認できるデバッグ用の機能として便利です。
現在地取得と追従
ブラウザの Geolocation API を使えば、現在地を取得して地図中心を自動移動できます。
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
map.setView([lat, lng], 15);
L.marker([lat, lng]).addTo(map).bindPopup("現在地");
});
}
旅行中にスマートフォンで使う観光マップでは特に有用です。
クラスター化
観光地が数百件を超えると、マーカーが重なり視認性が落ちます。leaflet.markercluster
プラグインを使うと、近接したマーカーをグループ化してまとめて表示できます。
const markers = L.markerClusterGroup();
spots.forEach((s) => {
const m = L.marker([s.lat, s.lng]).bindPopup(s.name);
markers.addLayer(m);
});
map.addLayer(markers);
クラスターをクリックするとズームして展開され、軽快な操作感が得られます。
UX 改善の小技
- マウスオーバーでスポット名をツールチップ表示
- マーカークリック時にスクロールを抑止
flyTo()
やpanTo()
を使ってスムーズに地図中心を移動- ズームや回転をアニメーション化(モバイルでは特に効果的)
スマホ対応のポイント
地図は height: 100vh
で全画面表示し、UIボタンは固定配置にします。
スマホではポップアップサイズを調整し、誤タップを防ぐ余白を設けると良いでしょう。
パフォーマンスと最適化
Leaflet は軽量ですが、データ量が多いと描画負荷が上がります。ここでは高速化のポイントを整理します。
タイルキャッシュ・CDN
タイル画像を自前でキャッシュするか、Maptiler などの CDN タイルを利用することで、レスポンスを大幅に改善できます。
GeoJSON の軽量化
- 不要な属性を削除
- 緯度経度を小数点4桁程度に丸める
- GeoJSON を分割して遅延読み込み
これによりデータ転送量を削減できます。
レンダリング最適化
マーカーが多すぎる場合は、すべてを一度に描画せず、ズームレベルに応じて表示数を制御します。
map.on('zoomend', () => {
const zoom = map.getZoom();
if (zoom < 10) {
map.removeLayer(detailGroup);
} else {
map.addLayer(detailGroup);
}
});
ズームレベル制限
あまり細かくズームできる必要がない場合、maxZoom
を低めに設定すると、不要なタイルロードを防げます。
レイヤー遅延読み込み
スクロールやボタン操作で特定エリアに来たときだけデータを読み込む方式(Lazy Load)も有効です。
デプロイと運用上の注意点
HTTPS 必須
地図APIやGeolocation APIを利用するには、サイトがHTTPSである必要があります。
特に現在地取得はHTTP環境では動作しません。
タイルサーバーの選択
商用サービスでの安定運用には、次のような代替タイル提供元を検討します。
- Maptiler Cloud
- Stadia Maps
- Thunderforest
- CARTO Basemaps
それぞれスタイルや利用制限、価格体系が異なるため、用途に合わせて選定します。
APIキー管理
OpenRouteService などの外部APIを使う場合は、.env
やサーバ側でキーを隠蔽するのが鉄則です。
フロントに直接埋め込むと不正利用のリスクが高まります。
モニタリングとログ
アクセスログやリクエスト数を可視化し、負荷が高い時間帯を把握することでタイル制限を回避しやすくなります。
著作権とライセンス表記
地図の下部に OSM のクレジット表記を残すことは必須です。
自作タイルを使う場合でも、元データが OSM の場合は「© OpenStreetMap contributors」と明記します。
完成例と拡張アイデア
完成例イメージ
完成したマップは、観光地スポットを色分けしたマーカーで表示し、クリックで写真・説明文を表示します。
ジャンル別の絞り込みUIと、ルート検索ボタンを併設することで、実用的な観光ナビとして機能します。
拡張アイデア
- レビュー機能:各スポットにコメントや評価を追加
- 写真ギャラリー:ポップアップにスライダーを埋め込み
- オフラインキャッシュ:PWA対応で一部キャッシュ
- 多言語化:Leaflet のラベルやUIを英語・中国語対応
- 時系列データ:季節イベントや期間限定情報を時間軸で管理
活用ケース
- 自治体や観光協会の案内サイト
- イベントマップ(花火大会・祭り)
- トレッキング/自転車ルート案内
- ショッピングモールや施設内マップ
まとめと次のステップ
Leaflet.js と OpenStreetMap を組み合わせることで、無料かつ柔軟な観光地マップを構築できました。
今回のポイントを整理します。
- Leaflet の基礎構造:
L.map()
で地図を生成し、L.tileLayer()
でOSMタイルを追加。 - マーカーとポップアップ:
L.marker()
と.bindPopup()
で地点情報を視覚化。 - GeoJSON の利用:観光地データを分離し、柔軟に更新可能。
- レイヤー管理:カテゴリごとにグループ化し、ユーザ操作で切り替え。
- ルート描画:外部APIと組み合わせてナビゲーション実装。
- パフォーマンスと運用:キャッシュ、API管理、著作権表記を徹底。
Leaflet はプラグインが豊富で、今後さらに機能拡張が可能です。
次のステップとしては以下を試すとよいでしょう。
- Leaflet Routing Machine を使った経路案内
- Turf.js による地理解析(距離計算・範囲抽出)
- Leaflet Search プラグインによる検索機能追加
- サーバ連携:バックエンドから GeoJSON を動的取得
コメント