🌊
react-native-mapをオフラインで使う
ExpoでGoogle mapを使う
react-native-mapを使うことで簡単にgoogle mapをアプリケーションに組み込むことができる
ただし、GoogleのMap SDK for Android / iOSを使用する
詳細な設定は各所にあるので、ここでは割愛する。
react-native-mapをオフラインで使えるか?
使える。
しかし、事前にタイルデータを端末にダウンロードしておく必要がある。
タイルデータのソースとして今回はGoogle Tile APIを使うこととする。
無料枠はあるが、Map SDKとともに課金されるので注意
Google Maps Tiles APIを有効にする
GCPの設定を先に済ませておく
Tiles APIをテストする
Googleの公式ドキュメントに従って、
- セッショントークンを取得
- 任意の座標(x,y,z)のタイルを取得する
をテストしてみる
ts-nodeで実行するなら以下の通り
const API_KEY = 'your api key'
async function createSession() {
const url = `https://tile.googleapis.com/v1/createSession?key=${API_KEY}`;
const requestBody = {
mapType: "roadmap",
language: "en-US",
region: "JP"
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
const data = await response.json();
console.log('Session created:', data);
} catch (error) {
console.error('Error creating session:', error);
}
}
createSession()
タイルをExpo上でダウンロードする
タイルを取得する時には2つの条件を設定する
- 取得したいRegion
- ズームレベル
例として、Regionとズームレベルを固定して取得してみる
- Region = 大阪府大阪市
- 緯度経度のデルタは取って来たい範囲を示す
- 1度で110kmぐらい
- ズームレベル = 12
全体のコード
FetchTiles()を適当なボタンにでも仕込んで呼び出せば、端末のローカルストレージにタイルがダウンロードされる。
import * as FileSystem from 'expo-file-system';
import { Region } from 'react-native-maps';
const AppConstants = {
TILE_FOLDER: `${FileSystem.documentDirectory}tiles`,
MAP_URL: 'https://tile.googleapis.com/v1/2dtiles',
key: '<YOUR TILES API KEY>',
};
const mapRegion = {
latitude: 34.6937,
longitude: 135.5021,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
};
async function createSession(key: string) {
const url = `https://tile.googleapis.com/v1/createSession?key=${key}`;
const requestBody = {
mapType: 'roadmap',
language: 'en-US',
region: 'JP',
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
const data = await response.json();
return data.session;
} catch (error) {
console.error('Error creating session:', error);
}
}
export const FetchTiles = async () => {
const session = await createSession(AppConstants.key);
const inputZoomLevel = 0;
const currentZoom = 15;
const minZoom = currentZoom;
const maxZoom = currentZoom + inputZoomLevel;
const tiles = tileGridForRegion(mapRegion, minZoom, maxZoom);
// Create directory for tiles
// TODO: Batch to speed up
for (const tile of tiles) {
const folder = `${AppConstants.TILE_FOLDER}/${tile.z}/${tile.x}`;
await FileSystem.makeDirectoryAsync(folder, { intermediates: true });
// console.log('DocumentDir is ', FileSystem.documentDirectory);
console.log('Created directory', folder);
}
// Download tiles in batches to avoid excessive promises in flight
const BATCH_SIZE = 100;
let batch = [];
for (const tile of tiles) {
const fetchUrl = `${AppConstants.MAP_URL}/${tile.z}/${tile.x}/${tile.y}?session=${session}&key=${AppConstants.key}`;
console.log('fetchUrl is ', fetchUrl);
const localLocation = `${AppConstants.TILE_FOLDER}/${tile.z}/${tile.x}/${tile.y}.png`;
const tilePromise = FileSystem.downloadAsync(fetchUrl, localLocation);
batch.push(tilePromise);
if (batch.length >= BATCH_SIZE) {
await Promise.all(batch);
batch = [];
}
}
await Promise.all(batch);
};
export type Tile = {
x: number;
y: number;
z: number;
};
export function tileGridForRegion(region: Region, minZoom: number, maxZoom: number): Tile[] {
let tiles: Tile[] = [];
for (let zoom = minZoom; zoom <= maxZoom; zoom++) {
const subTiles = tilesForZoom(region, zoom);
tiles = [...tiles, ...subTiles];
}
return tiles;
}
function degToRad(deg: number): number {
return (deg * Math.PI) / 180;
}
function lonToTileX(lon: number, zoom: number): number {
return Math.floor(((lon + 180) / 360) * Math.pow(2, zoom));
}
function latToTileY(lat: number, zoom: number): number {
return Math.floor(
((1 - Math.log(Math.tan(degToRad(lat)) + 1 / Math.cos(degToRad(lat))) / Math.PI) / 2) * Math.pow(2, zoom),
);
}
function tilesForZoom(region: Region, zoom: number): Tile[] {
const minLon = region.longitude - region.longitudeDelta;
const minLat = region.latitude - region.latitudeDelta;
const maxLon = region.longitude + region.longitudeDelta;
const maxLat = region.latitude + region.latitudeDelta;
const minTileX = lonToTileX(minLon, zoom);
const maxTileX = lonToTileX(maxLon, zoom);
const minTileY = latToTileY(maxLat, zoom);
const maxTileY = latToTileY(minLat, zoom);
const tiles: Tile[] = [];
for (let x = minTileX; x <= maxTileX; x++) {
for (let y = minTileY; y <= maxTileY; y++) {
tiles.push({ x, y, z: zoom });
}
}
return tiles;
}
コンポーネント上での使用例
import React, { FC, useState } from 'react';
import { StyleSheet, Text, ActivityIndicator } from 'react-native';
import { Button } from 'src/components/ui/Button';
import { Card } from 'src/components/ui/Card';
import { TitleText } from 'src/components/ui/Text';
import { FetchTiles } from './fetch-tile';
type Props = {
// mapRegion?: Region;
onFinish: () => void;
};
export const DownloadTilesMenu: FC<Props> = ({ onFinish }) => {
const [isLoading, setIsLoading] = useState(false);
const handleFetchTiles = async () => {
setIsLoading(true);
await FetchTiles();
alert('Finished downloading tiles, you are now viewing the offline map.');
setIsLoading(false);
onFinish();
};
return (
<Card>
<TitleText>Push Buttons to download tiles</TitleText>
<Text style={styles.warningMessage}>
Warning! High Zoom level requests so many tiles that costs high when you use Tiles API.
</Text>
{isLoading && <ActivityIndicator />}
{!isLoading && <Button title="Dowload tiles" onPress={handleFetchTiles} />}
</Card>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 15,
left: 0,
right: 0,
zIndex: 999,
},
warningMessage: {
marginVertical: 10,
color: '#bbb',
fontStyle: 'italic',
fontSize: 10,
textAlign: 'center',
},
estimate: {
marginVertical: 15,
textAlign: 'center',
},
});
MapViewで表示する初期位置はFetchしたタイルとは別の地域を指定すると良い。
例として東京を指定しておく
const URL_TEMPLATE = `${FileSystem.documentDirectory}tiles/{z}/{x}/{y}.png`;
export function Map() {
return (
<>
<MapView
initialRegion={{
latitude: 35.6894,
longitude: 139.6917,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
style={styles.container}
>
<UrlTile
urlTemplate={URL_TEMPLATE}
zIndex={1}
/>
</MapView>
<DownloadTilesMenu onFinish={() => {}} />
</>
)
}
const createStyles = (theme: Theme) => {
return StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
Discussion