🌏

Flutter で Mapbox の地図を操作してみる

2021/09/24に公開

こちらは Qiita に投稿した Flutter で Mapbox の地図を現在位置を中心にして表示してみる の続きです。

Zenn のアカウントを取得してから記事を一つも書いていなかったので、試しにこちらに書いてみました。

なお、ソースコードは GitHub にも置いてあります(随時、機能追加などを行っています)。

前回実装した機能

  • Flutter SDK 2.5.3
  • mapbox_gl 0.13.0
  • location 4.3.0

で、

  • 地図の表示
  • GPS 追従の ON / OFF

※詳細は前回の記事をご覧ください。

今回追加した地図操作の機能

  • ズーム(地図の縮尺)を初期表示に戻す
  • 画面の上=北(初期表示)に戻す
  • 長押しした地点にピンを立てる

0. 追加で必要なライブラリ

pubspec.yamldependencies:に追加します。

  gap: ^2.0.0

1. ズーム(地図の縮尺)を初期表示に戻す

フローティングアイコンボタン(±)を押すと初期表示のズームに戻すようにしました。

  // 地図のズームを初期状態に
  void _resetZoom() {
    _controller.future.then((mapboxMap) {
      mapboxMap.moveCamera(CameraUpdate.zoomTo(_initialZoom));
    });
  }

MapboxMapControllermoveCameraで、CameraUpdate.zoomToするだけです。

2. 画面の上=北(初期表示)に戻す

同じくフローティングアイコンボタン(N)を押すと初期表示の方向(画面の上が北)に戻すようにしました。

  // 地図の上を北に
  void _resetBearing() {
    _controller.future.then((mapboxMap) {
      mapboxMap.animateCamera(CameraUpdate.bearingTo(_initialBearing));
    });
  }

ほぼ 1. と同時ですが、CameraUpdate.bearingTo0.0を指定することで、画面の上=北に向けています。
なお、1.・2. 以外の画面移動では、

の使い分けをしています。

※私の所有機材(Android 10)でanimateCameraが正しく動かないためです(地図の回転は正しく動作しますが、移動は中途半端な地点で止まってしまいます)。

3. 長押しした地点にピンを立てる

地図の任意の地点を長押しすると、ピン(マーク)を立ててピンのラベルとして時刻を表示するようにしました。

MapBoxMaponMapLongClickで次のコードを呼び出しています。

  // マーク(ピン)を立てて時刻(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,
      ));
    });
  }

MapboxMapControlleraddSymbolでピン(マーク)画像とラベルを追加しています。

※なお、日付フォーマッタとして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