Flutter で Mapbox の地図を操作してみる
こちらは Qiita に投稿した Flutter で Mapbox の地図を現在位置を中心にして表示してみる の続きです。
Zenn のアカウントを取得してから記事を一つも書いていなかったので、試しにこちらに書いてみました。
なお、ソースコードは GitHub にも置いてあります(随時、機能追加などを行っています)。
前回実装した機能
- Flutter SDK 2.5.3
- mapbox_gl 0.13.0
- location 4.3.0
で、
- 地図の表示
- GPS 追従の ON / OFF
※詳細は前回の記事をご覧ください。
今回追加した地図操作の機能
- ズーム(地図の縮尺)を初期表示に戻す
- 画面の上=北(初期表示)に戻す
- 長押しした地点にピンを立てる
0. 追加で必要なライブラリ
pubspec.yaml
のdependencies:
に追加します。
gap: ^2.0.0
1. ズーム(地図の縮尺)を初期表示に戻す
フローティングアイコンボタン(±)を押すと初期表示のズームに戻すようにしました。
// 地図のズームを初期状態に
void _resetZoom() {
_controller.future.then((mapboxMap) {
mapboxMap.moveCamera(CameraUpdate.zoomTo(_initialZoom));
});
}
MapboxMapController
のmoveCamera
で、CameraUpdate.zoomTo
するだけです。
2. 画面の上=北(初期表示)に戻す
同じくフローティングアイコンボタン(N)を押すと初期表示の方向(画面の上が北)に戻すようにしました。
// 地図の上を北に
void _resetBearing() {
_controller.future.then((mapboxMap) {
mapboxMap.animateCamera(CameraUpdate.bearingTo(_initialBearing));
});
}
ほぼ 1. と同時ですが、CameraUpdate.bearingTo
で0.0
を指定することで、画面の上=北に向けています。
なお、1.・2. 以外の画面移動では、
- iOS は
animateCamera
- Android は
moveCamera
の使い分けをしています。
※私の所有機材(Android 10)でanimateCamera
が正しく動かないためです(地図の回転は正しく動作しますが、移動は中途半端な地点で止まってしまいます)。
3. 長押しした地点にピンを立てる
地図の任意の地点を長押しすると、ピン(マーク)を立ててピンのラベルとして時刻を表示するようにしました。
MapBoxMap
のonMapLongClick
で次のコードを呼び出しています。
// マーク(ピン)を立てて時刻(hh:mm)のラベルを付ける
void _addMark(LatLng tapPoint) {
// 時刻を取得
DateTime _now = DateTime.now();
String _hhmm = _fillZero(_now.hour) + ':' + _fillZero(_now.minute);
// マーク(ピン)を立てる
_controller.future.then((mapboxMap) {
mapboxMap.addSymbol(SymbolOptions(
geometry: tapPoint,
textField: _hhmm,
textAnchor: "top",
textColor: "#000",
textHaloColor: "#FFF",
textHaloWidth: 3,
iconImage: "mapbox-marker-icon-blue",
iconSize: 1,
));
});
}
MapboxMapController
のaddSymbol
でピン(マーク)画像とラベルを追加しています。
※なお、日付フォーマッタとしてintl
を使おうと思ったのですが、dart:html
周りでトラブルが発生したので一旦諦めました。
なお、こちらは事前に地図(Style)の準備が必要です。
「↓ Download ZIP」 でマーカー(ピン)の画像ファイル(.zip
)をダウンロードして解凍したら、Mapbox Studio を使って標準対象の地図を編集し、画面情報の「images」メニューで.svg
ファイルをアップロードします。
※以前から同じ地図(Style)をアプリで表示していた場合、キャッシュに地図(Style)の古い情報が残るためか、しばらくの間はアップロードした画像が表示されないことがあります。
その他
地図を北向きだけでなく進行方向に向けるモードも実装したかったのですが、LocationData.heading
が-1.0
しか返してくれなかったので諦めました。
画面例
main.dart
)
ソースコード(import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:location/location.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Mapbox',
home: MapPage(),
);
}
}
class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key);
_MapPageState createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
final Completer<MapboxMapController> _controller = Completer();
final Location _locationService = Location();
// 地図スタイル用 Mapbox URL
final String _style = '【地図スタイルのURL】';
// Location で緯度経度が取れなかったときのデフォルト値
final double _initialLat = 35.6895014;
final double _initialLong = 139.6917337;
// ズームのデフォルト値
final double _initialZoom = 13.5;
// 方位のデフォルト値(北)
final double _initialBearing = 0.0;
// 現在位置
LocationData? _yourLocation;
// GPS 追従?
bool _gpsTracking = false;
// 現在位置の監視状況
StreamSubscription? _locationChangedListen;
void initState() {
super.initState();
// 現在位置の取得
_getLocation();
// 現在位置の変化を監視
_locationChangedListen =
_locationService.onLocationChanged.listen((LocationData result) async {
setState(() {
_yourLocation = result;
});
});
setState(() {
_gpsTracking = true;
});
}
void dispose() {
super.dispose();
// 監視を終了
_locationChangedListen?.cancel();
}
Widget build(BuildContext context) {
return Scaffold(
body: _makeMapboxMap(),
floatingActionButton: _makeFloatingIcons(),
);
}
// 地図ウィジェット
Widget _makeMapboxMap() {
if (_yourLocation == null) {
// 現在位置が取れるまではロード中画面を表示
return const Center(
child: CircularProgressIndicator(),
);
}
// GPS 追従が ON かつ地図がロードされている→地図の中心を移動
_moveCameraToGpsPoint();
// Mapbox ウィジェットを返す
return MapboxMap(
// 地図(スタイル)を指定
styleString: _style,
// 初期表示される位置情報を現在位置から設定
initialCameraPosition: CameraPosition(
target: LatLng(_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong),
zoom: _initialZoom,
),
onMapCreated: (MapboxMapController controller) {
_controller.complete(controller);
},
compassEnabled: true,
// 現在位置を表示する
myLocationEnabled: true,
// 地図をタップしたとき
onMapClick: (Point<double> point, LatLng tapPoint) {
_onTap(point, tapPoint);
},
// 地図を長押ししたとき
onMapLongClick: (Point<double> point, LatLng tapPoint) {
_addMark(tapPoint);
},
);
}
// フローティングアイコンウィジェット
Widget _makeFloatingIcons() {
return Column(mainAxisSize: MainAxisSize.min, children: [
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
// ズームを戻す
_resetZoom();
},
child: const Text('±', style: TextStyle(fontSize: 28.0, height: 1.0)),
),
const Gap(16),
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
// 北向きに戻す
_resetBearing();
},
child: const Text('N',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
),
const Gap(16),
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
_gpsToggle();
},
child: Icon(
// GPS 追従の ON / OFF に合わせてアイコン表示する
_gpsTracking ? Icons.gps_fixed : Icons.gps_not_fixed,
),
),
]);
}
// 現在位置を取得
void _getLocation() async {
_yourLocation = await _locationService.getLocation();
}
// GPS 追従を ON / OFF
void _gpsToggle() {
setState(() {
_gpsTracking = !_gpsTracking;
});
// ここは本来 iOS では不要
_moveCameraToGpsPoint();
}
// GPS 追従が ON なら地図の中心を現在位置へ
void _moveCameraToGpsPoint() {
if (_gpsTracking) {
_controller.future.then((mapboxMap) {
if (Platform.isAndroid) {
mapboxMap.moveCamera(CameraUpdate.newLatLng(LatLng(
_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong)));
} else if (Platform.isIOS) {
mapboxMap.animateCamera(CameraUpdate.newLatLng(LatLng(
_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong)));
}
});
}
}
// 地図をタップしたときの処理
void _onTap(Point<double> point, LatLng tapPoint) {
_moveCameraToTapPoint(tapPoint);
setState(() {
_gpsTracking = false;
});
}
// 地図の中心をタップした場所へ
void _moveCameraToTapPoint(LatLng tapPoint) {
_controller.future.then((mapboxMap) {
if (Platform.isAndroid) {
mapboxMap.moveCamera(CameraUpdate.newLatLng(tapPoint));
} else if (Platform.isIOS) {
mapboxMap.animateCamera(CameraUpdate.newLatLng(tapPoint));
}
});
}
// マーク(ピン)を立てて時刻(hh:mm)のラベルを付ける
void _addMark(LatLng tapPoint) {
// 時刻を取得
DateTime _now = DateTime.now();
String _hhmm = _fillZero(_now.hour) + ':' + _fillZero(_now.minute);
// マーク(ピン)を立てる
_controller.future.then((mapboxMap) {
mapboxMap.addSymbol(SymbolOptions(
geometry: tapPoint,
textField: _hhmm,
textAnchor: "top",
textColor: "#000",
textHaloColor: "#FFF",
textHaloWidth: 3,
iconImage: "mapbox-marker-icon-blue",
iconSize: 1,
));
});
}
// 2 桁 0 埋め(Intl が正しく動かなかったため仕方なく)
String _fillZero(int number) {
String _tmpNumber = ('0' + number.toString());
return _tmpNumber.substring(_tmpNumber.length - 2);
}
// 地図の上を北に
void _resetBearing() {
_controller.future.then((mapboxMap) {
mapboxMap.animateCamera(CameraUpdate.bearingTo(_initialBearing));
});
}
// 地図のズームを初期状態に
void _resetZoom() {
_controller.future.then((mapboxMap) {
mapboxMap.moveCamera(CameraUpdate.zoomTo(_initialZoom));
});
}
}
Discussion