Reactで地図を表示したい!React Leafletで位置情報アプリを作ろう
React Leafletを使って地図を表示し、位置情報サービスを実装する方法を初心者向けに解説。マーカー表示、ユーザー位置取得、カスタマイズ方法まで実際のコード例とともに詳しく説明します。
「Reactアプリで地図を表示したいけど、どうすればいいの?」
こんな疑問を持ったことはありませんか?
「お店の位置を地図で見せたい」 「ユーザーの現在地を取得して地図に表示したい」 「地図にマーカーやポップアップを追加したい」
このような要望、多いですよね。
実は、React Leafletを使えば無料で簡単に地図アプリが作れます。 GoogleマップAPIのように有料制限もなく、初心者でも使いやすいライブラリなんです。
この記事では、React Leafletを使った地図表示から位置情報の取得まで、実際のコード例とともに分かりやすく解説します。 読み終わる頃には、あなたも地図を使ったWebアプリが作れるようになりますよ!
React Leafletって何?無料で使える地図ライブラリ
React Leafletは、人気のある地図ライブラリ「Leaflet」をReactで使いやすくしたものです。
React Leafletの魅力
こんなメリットがあります
- 完全無料で商用利用も可能
- 軽量で動作が速い
- スマートフォンでも快適に動く
- カスタマイズの自由度が高い
- プラグインが豊富
どんなことができるの?
- インタラクティブな地図の表示
- マーカーやポップアップの追加
- ユーザーの現在地を取得・表示
- 地図のデザインを自由にカスタマイズ
- 複数の地図スタイルから選択可能
他の地図サービスとの違い
地図サービスには色々な選択肢がありますが、React Leafletの特徴を見てみましょう。
// Google Maps API(有料制限あり)
import { GoogleMap, Marker } from '@react-google-maps/api';
// Mapbox(無料枠あり、カスタマイズ豊富)
import mapboxgl from 'mapbox-gl';
// React Leaflet(完全無料、オープンソース)
import { MapContainer, TileLayer, Marker } from 'react-leaflet';
Google Maps APIとの比較
Google Maps APIは有名ですが、無料枠を超えると料金が発生します。 React Leafletなら完全無料で使えるので、個人開発や小規模プロジェクトにぴったりです。
選ぶべき理由
予算を気にせず使えて、機能も十分充実しているからです。 まずはReact Leafletで始めてみることをおすすめします。
環境構築の準備をしよう
React Leafletを使うための準備から始めましょう。 意外と簡単なので、安心してくださいね。
必要なパッケージをインストール
まずは必要なパッケージをインストールします。
# React Leafletと関連パッケージのインストール
npm install react-leaflet leaflet
# TypeScriptを使用する場合
npm install @types/leaflet
何をインストールしているの?
react-leaflet
: React用のLeafletライブラリleaflet
: 地図機能の本体@types/leaflet
: TypeScript用の型定義(TypeScript使用時のみ)
CSSファイルを読み込もう
地図を正しく表示するために、LeafletのCSSファイルが必要です。
// App.jsまたはindex.jsで読み込み
import 'leaflet/dist/leaflet.css';
または、CSSファイルに直接書く方法もあります
/* App.css */
@import url('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
どちらでも同じ効果が得られます。 お好みの方法を選んでくださいね。
アイコンの問題を解決しよう
React Leafletでは、マーカーアイコンで問題が起きることがあります。 でも大丈夫です、簡単に解決できます。
// utils/leafletSetup.js
import L from 'leaflet';
// デフォルトアイコンの設定を修正
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
このコードの意味は?
Leafletのデフォルトアイコンの設定を修正しています。 Reactの環境では、アイコンのパスが正しく設定されないことがあるんです。
// App.jsで読み込み
import './utils/leafletSetup';
最後にApp.jsでこのファイルを読み込めば準備完了です。
最初の地図を表示してみよう
準備ができたら、実際に地図を表示してみましょう。
import React from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
function BasicMap() {
const position = [35.6762, 139.6503]; // 東京の座標
return (
<MapContainer
center={position}
zoom={13}
style={{ height: '400px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={position}>
<Popup>
東京駅の位置です
</Popup>
</Marker>
</MapContainer>
);
}
export default BasicMap;
コードの解説をしますね
MapContainer
が地図の本体です。
center
で地図の中心位置、zoom
で拡大レベルを指定します。
TileLayer
は地図の画像データを提供します。
今回はOpenStreetMapという無料の地図データを使っています。
Marker
で地図上にピンを表示できます。
Popup
でマーカーをクリックした時の吹き出しを設定します。
実行してみよう
このコードを実行すると、東京駅を中心とした地図が表示されます。 マーカーをクリックすると、「東京駅の位置です」というポップアップが出ますよ。
マーカーとポップアップを活用しよう
地図上にマーカーを配置して、情報を表示する方法を学びましょう。 これができると、お店の場所案内などに使えますね。
複数のマーカーを表示する
一つの地図に複数のマーカーを表示してみましょう。
import React from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function MultipleMarkers() {
const locations = [
{ id: 1, name: '東京駅', position: [35.6762, 139.6503], info: '日本の鉄道の中心駅' },
{ id: 2, name: '新宿駅', position: [35.6896, 139.7006], info: '世界一利用者数の多い駅' },
{ id: 3, name: '渋谷駅', position: [35.6580, 139.7016], info: '若者文化の発信地' },
{ id: 4, name: '池袋駅', position: [35.7295, 139.7109], info: '副都心の中心駅' }
];
return (
<MapContainer
center={[35.6762, 139.6503]}
zoom={12}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{locations.map(location => (
<Marker key={location.id} position={location.position}>
<Popup>
<div>
<h3>{location.name}</h3>
<p>{location.info}</p>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}
データの準備から始めます
locations
配列に、表示したい場所の情報をまとめています。
各場所には、ID、名前、座標、説明文を含んでいます。
map関数で繰り返し表示
locations.map()
を使って、配列の各要素に対してマーカーを作成します。
これで一度に複数のマーカーが表示されますね。
ポップアップの中身をカスタマイズ
ポップアップの中にHTMLを書けるので、見出しや段落で情報を整理できます。 デザインも自由に変更可能です。
カスタムアイコンを作ってみよう
デフォルトのマーカーアイコンを、オリジナルのものに変更できます。
import L from 'leaflet';
function CustomIconMap() {
// カスタムアイコンの定義
const customIcon = new L.Icon({
iconUrl: '/images/custom-marker.png',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
// 種類別のアイコン
const icons = {
restaurant: new L.Icon({
iconUrl: '/images/restaurant-icon.png',
iconSize: [30, 30],
iconAnchor: [15, 30]
}),
hotel: new L.Icon({
iconUrl: '/images/hotel-icon.png',
iconSize: [30, 30],
iconAnchor: [15, 30]
}),
station: new L.Icon({
iconUrl: '/images/station-icon.png',
iconSize: [30, 30],
iconAnchor: [15, 30]
})
};
const places = [
{ name: 'レストラン A', position: [35.6762, 139.6503], type: 'restaurant' },
{ name: 'ホテル B', position: [35.6896, 139.7006], type: 'hotel' },
{ name: '駅 C', position: [35.6580, 139.7016], type: 'station' }
];
return (
<MapContainer
center={[35.6762, 139.6503]}
zoom={12}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{places.map((place, index) => (
<Marker
key={index}
position={place.position}
icon={icons[place.type]}
>
<Popup>
<div>
<h3>{place.name}</h3>
<p>タイプ: {place.type}</p>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}
アイコンの設定について
L.Icon
でカスタムアイコンを作成します。
iconUrl
で画像のパス、iconSize
でサイズを指定します。
種類別にアイコンを使い分け
icons
オブジェクトで、場所の種類ごとに異なるアイコンを準備しています。
レストラン、ホテル、駅など、一目で分かるアイコンにすると便利ですね。
アイコンの配置調整
iconAnchor
でアイコンの基準点を設定します。
popupAnchor
でポップアップの表示位置を調整できます。
豪華なポップアップを作ろう
ポップアップに画像やボタンを追加して、より魅力的にしてみましょう。
function RichPopupMap() {
const restaurants = [
{
id: 1,
name: '美味しいレストラン',
position: [35.6762, 139.6503],
rating: 4.5,
price: '¥¥¥',
image: '/images/restaurant1.jpg',
description: '新鮮な食材を使った本格的なイタリアン'
},
{
id: 2,
name: 'カフェ サンプル',
position: [35.6896, 139.7006],
rating: 4.2,
price: '¥¥',
image: '/images/cafe1.jpg',
description: '落ち着いた雰囲気のカフェ'
}
];
return (
<MapContainer
center={[35.6762, 139.6503]}
zoom={12}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{restaurants.map(restaurant => (
<Marker key={restaurant.id} position={restaurant.position}>
<Popup maxWidth={300}>
<div style={{ padding: '10px' }}>
<img
src={restaurant.image}
alt={restaurant.name}
style={{ width: '100%', height: '150px', objectFit: 'cover', borderRadius: '5px' }}
/>
<h3 style={{ margin: '10px 0 5px 0' }}>{restaurant.name}</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
<span>評価: ⭐ {restaurant.rating}</span>
<span>価格: {restaurant.price}</span>
</div>
<p style={{ margin: '5px 0', fontSize: '14px' }}>{restaurant.description}</p>
<button
style={{
width: '100%',
padding: '8px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer'
}}
onClick={() => alert(`${restaurant.name}の詳細を表示`)}
>
詳細を見る
</button>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}
ポップアップの構成要素
この例では、画像、タイトル、評価、価格、説明文、ボタンを含んでいます。 実際のお店紹介アプリのような見た目になりますね。
スタイリングの工夫
インラインスタイルで見た目を調整しています。 もちろん、CSSクラスを使って管理することも可能です。
インタラクティブな要素
ボタンをクリックすると、詳細ページに移動したり、予約画面を開いたりできます。
onClick
イベントで好きな処理を追加してください。
ユーザーの位置情報を取得しよう
ユーザーの現在地を取得して、地図上に表示する方法を学びましょう。 位置情報を使えば、「現在地から一番近いお店」のような機能が作れますよ。
基本的な位置情報取得
まずは、シンプルな位置情報の取得から始めましょう。
import React, { useState, useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
function LocationMap() {
const [userPosition, setUserPosition] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 位置情報の取得
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserPosition([
position.coords.latitude,
position.coords.longitude
]);
setLoading(false);
},
(error) => {
setError(error.message);
setLoading(false);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
}
);
} else {
setError('位置情報がサポートされていません');
setLoading(false);
}
}, []);
if (loading) {
return <div>位置情報を取得中...</div>;
}
if (error) {
return <div>エラー: {error}</div>;
}
return (
<MapContainer
center={userPosition || [35.6762, 139.6503]}
zoom={15}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{userPosition && (
<Marker position={userPosition}>
<Popup>
<div>
<h3>あなたの現在地</h3>
<p>緯度: {userPosition[0].toFixed(6)}</p>
<p>経度: {userPosition[1].toFixed(6)}</p>
</div>
</Popup>
</Marker>
)}
</MapContainer>
);
}
位置情報の取得処理
navigator.geolocation.getCurrentPosition()
で現在地を取得します。
成功した場合は座標を、失敗した場合はエラーメッセージをstateに保存します。
オプションの説明
enableHighAccuracy
: より精度の高い位置情報を取得timeout
: 取得処理のタイムアウト時間(10秒)maximumAge
: キャッシュされた位置情報の有効期間(5分)
ユーザビリティの配慮
位置情報の取得には時間がかかるので、ローディング表示を入れています。 エラーが発生した場合も、分かりやすいメッセージを表示します。
地図の中心を動的に移動
ボタンをクリックで地図の中心を移動できるようにしてみましょう。
import { useMap } from 'react-leaflet';
function MapController({ center, zoom }) {
const map = useMap();
useEffect(() => {
if (center) {
map.setView(center, zoom);
}
}, [center, zoom, map]);
return null;
}
function DynamicCenterMap() {
const [userPosition, setUserPosition] = useState(null);
const [selectedLocation, setSelectedLocation] = useState(null);
const locations = [
{ name: '東京駅', position: [35.6762, 139.6503] },
{ name: '新宿駅', position: [35.6896, 139.7006] },
{ name: '渋谷駅', position: [35.6580, 139.7016] }
];
const handleLocationSelect = (location) => {
setSelectedLocation(location);
};
const handleGetUserLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const userPos = [position.coords.latitude, position.coords.longitude];
setUserPosition(userPos);
setSelectedLocation({ name: '現在地', position: userPos });
},
(error) => {
alert('位置情報の取得に失敗しました: ' + error.message);
}
);
}
};
return (
<div>
<div style={{ marginBottom: '10px' }}>
<button onClick={handleGetUserLocation}>
現在地を取得
</button>
{locations.map((location, index) => (
<button
key={index}
onClick={() => handleLocationSelect(location)}
style={{ marginLeft: '10px' }}
>
{location.name}
</button>
))}
</div>
<MapContainer
center={[35.6762, 139.6503]}
zoom={13}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{selectedLocation && (
<MapController center={selectedLocation.position} zoom={15} />
)}
{locations.map((location, index) => (
<Marker key={index} position={location.position}>
<Popup>{location.name}</Popup>
</Marker>
))}
{userPosition && (
<Marker position={userPosition}>
<Popup>あなたの現在地</Popup>
</Marker>
)}
</MapContainer>
</div>
);
}
MapControllerコンポーネントの役割
useMap
フックで地図のインスタンスを取得し、setView
メソッドで中心位置を変更します。
Reactのstate変更をLeafletの地図に反映させる橋渡し役ですね。
ユーザーインターフェースの工夫
ボタンをクリックするだけで地図の中心が移動するので、使いやすいです。 現在地取得ボタンも追加して、ユーザーの利便性を向上させています。
リアルタイムで位置を追跡
位置情報をリアルタイムで更新する機能も作れます。
function RealtimeLocationMap() {
const [userPosition, setUserPosition] = useState(null);
const [isTracking, setIsTracking] = useState(false);
const [watchId, setWatchId] = useState(null);
const startTracking = () => {
if (navigator.geolocation) {
const id = navigator.geolocation.watchPosition(
(position) => {
setUserPosition([
position.coords.latitude,
position.coords.longitude
]);
},
(error) => {
console.error('位置情報の取得エラー:', error);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
setWatchId(id);
setIsTracking(true);
}
};
const stopTracking = () => {
if (watchId) {
navigator.geolocation.clearWatch(watchId);
setWatchId(null);
setIsTracking(false);
}
};
return (
<div>
<div style={{ marginBottom: '10px' }}>
<button
onClick={isTracking ? stopTracking : startTracking}
style={{
padding: '10px 20px',
backgroundColor: isTracking ? '#dc3545' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
{isTracking ? '追跡を停止' : '位置追跡を開始'}
</button>
</div>
<MapContainer
center={userPosition || [35.6762, 139.6503]}
zoom={15}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{userPosition && (
<>
<MapController center={userPosition} zoom={15} />
<Marker position={userPosition}>
<Popup>
<div>
<h3>現在地</h3>
<p>緯度: {userPosition[0].toFixed(6)}</p>
<p>経度: {userPosition[1].toFixed(6)}</p>
<p>更新: {new Date().toLocaleTimeString()}</p>
</div>
</Popup>
</Marker>
</>
)}
</MapContainer>
</div>
);
}
位置追跡の仕組み
watchPosition
を使うと、位置が変わるたびに自動的にコールバック関数が呼ばれます。
散歩やドライブの軌跡を記録するようなアプリに使えますね。
開始・停止の制御
追跡を停止するにはclearWatch
を使います。
バッテリー消費を抑えるため、不要な時は停止できるようにしています。
更新時刻の表示
ポップアップに現在時刻を表示して、リアルタイム更新を視覚的に確認できます。
地図の見た目をカスタマイズしよう
地図のデザインを変更して、アプリに合った見た目にしてみましょう。 いろいろな地図スタイルから選べるんですよ。
異なる地図タイルを使ってみよう
地図の背景画像(タイル)を変更できます。
import React, { useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function CustomTileMap() {
const [selectedTile, setSelectedTile] = useState('openstreetmap');
const tileOptions = {
openstreetmap: {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
satellite: {
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
},
terrain: {
url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
},
dark: {
url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
}
};
return (
<div>
<div style={{ marginBottom: '10px' }}>
<label>地図スタイル: </label>
<select
value={selectedTile}
onChange={(e) => setSelectedTile(e.target.value)}
style={{ padding: '5px', marginLeft: '10px' }}
>
<option value="openstreetmap">標準地図</option>
<option value="satellite">衛星写真</option>
<option value="terrain">地形図</option>
<option value="dark">ダークモード</option>
</select>
</div>
<MapContainer
center={[35.6762, 139.6503]}
zoom={13}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url={tileOptions[selectedTile].url}
attribution={tileOptions[selectedTile].attribution}
/>
<Marker position={[35.6762, 139.6503]}>
<Popup>東京駅</Popup>
</Marker>
</MapContainer>
</div>
);
}
地図スタイルの種類
- 標準地図: 道路や建物が見やすい一般的なスタイル
- 衛星写真: 上空から撮影した実際の写真
- 地形図: 山や川などの地形が分かりやすい
- ダークモード: 目に優しい暗いテーマ
選択機能の実装
セレクトボックスで地図スタイルを切り替えられます。 ユーザーが好みの見た目を選べるので便利ですね。
地図コントロールを調整しよう
地図の操作機能をカスタマイズできます。
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function CustomControlsMap() {
return (
<MapContainer
center={[35.6762, 139.6503]}
zoom={13}
style={{ height: '500px', width: '100%' }}
zoomControl={false} // デフォルトのズームコントロールを非表示
scrollWheelZoom={true} // スクロールでズーム
doubleClickZoom={true} // ダブルクリックでズーム
dragging={true} // ドラッグ可能
attributionControl={false} // 著作権表示を非表示
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={[35.6762, 139.6503]}>
<Popup>東京駅</Popup>
</Marker>
</MapContainer>
);
}
コントロールオプションの説明
zoomControl
: ズームボタンの表示・非表示scrollWheelZoom
: マウスホイールでのズームdoubleClickZoom
: ダブルクリックでのズームdragging
: 地図のドラッグ移動attributionControl
: 著作権表示の制御
用途に応じた設定
モバイルアプリならscrollWheelZoom
を無効にしたり、埋め込み地図ならdragging
を制限したりできます。
レスポンシブ対応の地図
画面サイズに応じて地図のサイズを調整しましょう。
import React, { useState, useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function ResponsiveMap() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 1200,
height: typeof window !== 'undefined' ? window.innerHeight : 800
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// レスポンシブな地図サイズの計算
const getMapHeight = () => {
if (windowSize.width < 768) {
return '300px'; // モバイル
} else if (windowSize.width < 1024) {
return '400px'; // タブレット
} else {
return '500px'; // デスクトップ
}
};
const getMapZoom = () => {
if (windowSize.width < 768) {
return 12; // モバイル
} else {
return 13; // デスクトップ
}
};
return (
<div style={{ padding: '20px' }}>
<h2>レスポンシブ地図</h2>
<div style={{
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<MapContainer
center={[35.6762, 139.6503]}
zoom={getMapZoom()}
style={{
height: getMapHeight(),
width: '100%',
minHeight: '250px'
}}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={[35.6762, 139.6503]}>
<Popup>
<div>
<h3>東京駅</h3>
<p>画面サイズ: {windowSize.width}x{windowSize.height}</p>
<p>地図の高さ: {getMapHeight()}</p>
</div>
</Popup>
</Marker>
</MapContainer>
</div>
</div>
);
}
画面サイズの検出
window.innerWidth
で画面の幅を取得し、リサイズイベントで更新します。
画面サイズに応じて地図の高さとズームレベルを調整します。
ブレークポイントの設定
- 768px未満: モバイル
- 1024px未満: タブレット
- 1024px以上: デスクトップ
ユーザビリティの向上
どのデバイスでも快適に地図が使えるようになります。 モバイルでは地図を小さく、デスクトップでは大きく表示するのがポイントです。
実践的な店舗検索アプリを作ろう
今まで学んだ機能を組み合わせて、実際に使える店舗検索アプリを作ってみましょう。 検索機能、フィルタリング、距離計算まで含んだ本格的なアプリです。
完成版の店舗検索アプリ
import React, { useState, useMemo } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function StoreFinderApp() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [userPosition, setUserPosition] = useState(null);
const stores = [
{
id: 1,
name: 'カフェ・ド・パリ',
category: 'cafe',
position: [35.6762, 139.6503],
address: '東京都千代田区丸の内1-1-1',
phone: '03-1234-5678',
hours: '7:00-22:00',
rating: 4.5,
image: '/images/cafe1.jpg'
},
{
id: 2,
name: 'イタリアン・レストラン',
category: 'restaurant',
position: [35.6896, 139.7006],
address: '東京都新宿区新宿3-1-1',
phone: '03-2345-6789',
hours: '11:00-23:00',
rating: 4.2,
image: '/images/restaurant1.jpg'
},
{
id: 3,
name: 'ビジネスホテル',
category: 'hotel',
position: [35.6580, 139.7016],
address: '東京都渋谷区渋谷2-1-1',
phone: '03-3456-7890',
hours: '24時間',
rating: 4.0,
image: '/images/hotel1.jpg'
}
];
const categories = [
{ value: 'all', label: '全て' },
{ value: 'cafe', label: 'カフェ' },
{ value: 'restaurant', label: 'レストラン' },
{ value: 'hotel', label: 'ホテル' }
];
// フィルタリングされた店舗リスト
const filteredStores = useMemo(() => {
return stores.filter(store => {
const matchesSearch = store.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
store.address.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'all' || store.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}, [searchTerm, selectedCategory]);
// 現在地取得
const handleGetLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserPosition([position.coords.latitude, position.coords.longitude]);
},
(error) => {
alert('位置情報の取得に失敗しました: ' + error.message);
}
);
}
};
// 距離計算(簡易版)
const calculateDistance = (pos1, pos2) => {
const R = 6371; // 地球の半径(km)
const dLat = (pos2[0] - pos1[0]) * Math.PI / 180;
const dLon = (pos2[1] - pos1[1]) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(pos1[0] * Math.PI / 180) * Math.cos(pos2[0] * 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;
};
return (
<div style={{ padding: '20px' }}>
<h1>店舗検索アプリ</h1>
{/* 検索フォーム */}
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<input
type="text"
placeholder="店舗名または住所で検索"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ padding: '8px', minWidth: '200px' }}
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
style={{ padding: '8px' }}
>
{categories.map(cat => (
<option key={cat.value} value={cat.value}>
{cat.label}
</option>
))}
</select>
<button
onClick={handleGetLocation}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
現在地を取得
</button>
</div>
{/* 地図 */}
<MapContainer
center={[35.6762, 139.6503]}
zoom={12}
style={{ height: '400px', width: '100%', marginBottom: '20px' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{/* 現在地マーカー */}
{userPosition && (
<Marker position={userPosition}>
<Popup>
<div>
<h3>現在地</h3>
<p>あなたの位置</p>
</div>
</Popup>
</Marker>
)}
{/* 店舗マーカー */}
{filteredStores.map(store => (
<Marker key={store.id} position={store.position}>
<Popup maxWidth={300}>
<div style={{ padding: '10px' }}>
<h3>{store.name}</h3>
<p><strong>カテゴリ:</strong> {store.category}</p>
<p><strong>住所:</strong> {store.address}</p>
<p><strong>電話:</strong> {store.phone}</p>
<p><strong>営業時間:</strong> {store.hours}</p>
<p><strong>評価:</strong> ⭐ {store.rating}</p>
{userPosition && (
<p><strong>距離:</strong> {calculateDistance(userPosition, store.position).toFixed(2)} km</p>
)}
<button
style={{
width: '100%',
padding: '8px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={() => {
const url = `https://www.google.com/maps/dir/?api=1&destination=${store.position[0]},${store.position[1]}`;
window.open(url, '_blank');
}}
>
ルート案内
</button>
</div>
</Popup>
</Marker>
))}
</MapContainer>
{/* 店舗一覧 */}
<div>
<h2>検索結果 ({filteredStores.length}件)</h2>
<div style={{ display: 'grid', gap: '15px' }}>
{filteredStores.map(store => (
<div key={store.id} style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
backgroundColor: '#f9f9f9'
}}>
<h3>{store.name}</h3>
<p>{store.address}</p>
<p>営業時間: {store.hours}</p>
<p>評価: ⭐ {store.rating}</p>
{userPosition && (
<p>距離: {calculateDistance(userPosition, store.position).toFixed(2)} km</p>
)}
</div>
))}
</div>
</div>
</div>
);
}
アプリの主要機能
このアプリには以下の機能が含まれています。
- 検索機能: 店舗名や住所での検索
- カテゴリフィルタ: 種類による絞り込み
- 現在地取得: ユーザーの位置情報を表示
- 距離計算: 現在地からの距離を計算
- ルート案内: GoogleマップでのルートASE検索
検索とフィルタリング
useMemo
を使って、検索条件が変わった時だけフィルタリングを実行します。
パフォーマンスを向上させる重要な最適化ですね。
距離計算の実装
ハヴァシン公式を使って、2点間の距離を計算しています。 精密な計算ではありませんが、店舗検索アプリには十分な精度です。
外部連携機能
「ルート案内」ボタンでGoogleマップを開き、目的地までのルートを表示します。 実用性の高い機能で、ユーザーの利便性が向上します。
エラーハンドリングで安定したアプリに
位置情報や地図の読み込みで問題が起きた時の対処法を学びましょう。 エラーハンドリングがしっかりしていると、ユーザーに安心感を与えられます。
位置情報取得のエラー対策
位置情報の取得で起きるエラーに対応しましょう。
import React, { useState, useEffect } from 'react';
function RobustLocationMap() {
const [userPosition, setUserPosition] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleGetLocation = async () => {
setLoading(true);
setError(null);
try {
// 位置情報APIの利用可能性をチェック
if (!navigator.geolocation) {
throw new Error('位置情報サービスがサポートされていません');
}
// 権限のチェック
const permission = await navigator.permissions.query({ name: 'geolocation' });
if (permission.state === 'denied') {
throw new Error('位置情報の利用が拒否されています。ブラウザの設定を確認してください。');
}
// 位置情報の取得
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
resolve,
reject,
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000 // 5分間はキャッシュを利用
}
);
});
setUserPosition([position.coords.latitude, position.coords.longitude]);
} catch (err) {
let errorMessage = '位置情報の取得に失敗しました';
switch (err.code) {
case 1:
errorMessage = '位置情報の利用が拒否されました';
break;
case 2:
errorMessage = '位置情報が利用できません';
break;
case 3:
errorMessage = '位置情報の取得がタイムアウトしました';
break;
default:
errorMessage = err.message || errorMessage;
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div>
<div style={{ marginBottom: '20px' }}>
<button
onClick={handleGetLocation}
disabled={loading}
style={{
padding: '10px 20px',
backgroundColor: loading ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
{loading ? '位置情報を取得中...' : '現在地を取得'}
</button>
</div>
{error && (
<div style={{
padding: '10px',
backgroundColor: '#f8d7da',
color: '#721c24',
border: '1px solid #f5c6cb',
borderRadius: '5px',
marginBottom: '10px'
}}>
エラー: {error}
</div>
)}
{userPosition && (
<div style={{
padding: '10px',
backgroundColor: '#d4edda',
color: '#155724',
border: '1px solid #c3e6cb',
borderRadius: '5px',
marginBottom: '10px'
}}>
位置情報を取得しました: {userPosition[0].toFixed(6)}, {userPosition[1].toFixed(6)}
</div>
)}
</div>
);
}
エラーコードの対応
位置情報APIのエラーコードに応じて、適切なメッセージを表示します。 ユーザーが原因を理解しやすくなりますね。
権限チェックの実装
事前に位置情報の権限をチェックして、拒否されている場合は分かりやすいメッセージを表示します。
ユーザビリティの向上
ローディング中はボタンを無効化し、現在の状態が分かるようにしています。 エラーや成功メッセージも色分けして見やすくしました。
地図読み込みのエラー対策
地図タイルの読み込みエラーにも対応しましょう。
import React, { useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function ErrorHandlingMap() {
const [mapError, setMapError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const handleTileError = (error) => {
console.error('地図の読み込みエラー:', error);
setMapError('地図の読み込みに失敗しました');
};
const handleRetry = () => {
setMapError(null);
setRetryCount(prev => prev + 1);
};
return (
<div>
{mapError && (
<div style={{
padding: '15px',
backgroundColor: '#f8d7da',
color: '#721c24',
border: '1px solid #f5c6cb',
borderRadius: '5px',
marginBottom: '10px',
textAlign: 'center'
}}>
<p>{mapError}</p>
<button
onClick={handleRetry}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
再試行
</button>
</div>
)}
<MapContainer
center={[35.6762, 139.6503]}
zoom={13}
style={{ height: '500px', width: '100%' }}
key={retryCount} // 再試行時にコンポーネントを再マウント
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
onError={handleTileError}
/>
<Marker position={[35.6762, 139.6503]}>
<Popup>東京駅</Popup>
</Marker>
</MapContainer>
</div>
);
}
再試行機能の実装
地図の読み込みに失敗した場合、「再試行」ボタンで再度読み込みを試せます。 一時的なネットワークエラーでも対応できますね。
keyプロパティの活用
key
プロパティを変更することで、Reactコンポーネントを強制的に再マウントできます。
地図コンポーネントを完全にリセットしたい時に有効です。
エラーの視覚化
エラーメッセージを目立つように表示して、ユーザーが問題を認識しやすくしています。
まとめ:地図アプリ開発をマスターしよう
React Leafletを使った地図アプリケーションの実装方法について、詳しく解説してきました。
今回学んだ内容
- React Leafletの基本的な使い方
- マーカーとポップアップの活用法
- ユーザー位置情報の取得と表示
- 地図のカスタマイズとスタイリング
- 実践的な店舗検索アプリの作成
- エラーハンドリングの実装
React Leafletの魅力を再確認
無料で使える強力なライブラリで、位置情報を活用したWebアプリケーションが簡単に作れます。 GoogleマップAPIのような有料制限もないので、安心して開発に集中できますね。
次にやってみたいこと
地図機能を追加することで、あなたのアプリはユーザーにとってより価値のあるものになります。 今回学んだ技術を使って、ぜひオリジナルの地図アプリを作ってみてください。
お店の位置案内、イベント会場の案内、旅行記録アプリなど、アイデア次第で色々なサービスが作れますよ。 まずは簡単なものから始めて、徐々に機能を追加していきましょう!