🗺️

Flutter + Mapbox で Symbol をタップして id を取得・表示する

2021/09/27に公開

以前書いた、

の続きです。

前回、画面にピン(マーク)を立てる機能を追加しましたが、このピン(Symbolとして表示)をタップしたときに(Symbolidを取得し、AlertDialogで表示してみます。

Symbolidを表示するだけでは特に何の意味もありませんが、このidMapやデータベースに入れた情報と突き合わせて表示する場合などに使えます。ただし、アプリケーションを終了するなどして一旦地図上から消えたピン(Symbol)をデータベースから読み込んだ(緯度・経度・ラベルなどの)情報を使って再配置するケースでは再配置の都度idが変わってしまうため、データベースにidを保存するのではなく、表示中のピン(Symbol)のidをデータベース上の主キー列に変換するMapが必要になりそうです(後日トライして記事化する予定)。

ポイント

です。

加えて、

方法についても考慮が必要になります。

ピン(Symbol)をタップしたときに呼び出す処理をセットする

Symbol)をタップしたときに呼び出す処理は、MapboxMapController.onSymbolTappedaddでセットしますが、この処理のセットは一度だけ行います。

最初、ピンを立てるごとに毎回セットするコードを書いたところ、うまく動いた…と思ったらピンをタップしたときに同じアラートダイアログがピンの数だけ重なって表示されてしまいました。
そのため、最初にピンを立てたときだけセットするように修正しました。

【↓ここから不要↓】

  // onSymbolTapped 設定済み?
  bool _symbolSet = false;
  // マーク(ピン)を立てて時刻(hh:mm)のラベルを付ける
  void _addMark(LatLng tapPoint) {
    // 時刻を取得
    DateTime _now = DateTime.now();
    String _hhmm = _fillZero(_now.hour) + ':' + _fillZero(_now.minute);
    // マーク(ピン)を立てる
    _controller.future.then((mapboxMap) {
      Future<Symbol> _symbol = mapboxMap.addSymbol(SymbolOptions(
        geometry: tapPoint,
        textField: _hhmm,
        textAnchor: "top",
        textColor: "#000",
        textHaloColor: "#FFF",
        textHaloWidth: 3,
        iconImage: "mapbox-marker-icon-blue",
        iconSize: 1,
      ));
      if (!_symbolSet) {
        _symbol.then((symbol) {
          mapboxMap.onSymbolTapped.add(_onSymbolTap);
          setState(() {
            _symbolSet = true;
          });
        });
      }
    });
  }

【↑ここまで不要↑】

mapboxMap.onSymbolTapped.add(_onSymbolTap);が処理をセットしている部分です。

こちらが呼び出される側です。

  // マークをタップしたときに Symbol の情報を表示する
  void _onSymbolTap(Symbol symbol) {
    _dispSymbolInfo(symbol);
  }

  // Symbol の情報を表示する
  void _dispSymbolInfo(Symbol symbol) {
    showDialog(
      context: navigatorKey.currentContext!,
      builder: (BuildContext context) => AlertDialog(
        title: Text('Symbol ID : ${symbol.id}'),
        actions: [
          IconButton(
            icon: const Icon(Icons.close),
            color: Colors.blue,
            onPressed: () {
              Navigator.pop(context);
            },
          ),
        ],
      ),
    );
  }

AlertDialogを表示する

この形でアラートダイアログを表示しようとすると、親ウィジェットからBuildContextを引き継げません。

この場合は、以下のように準備したNavigatorStateの、contextプロパティからBuildContextを取得します。

final navigatorKey = GlobalKey<NavigatorState>();
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Mapbox',
      home: const MapPage(),
      navigatorKey: navigatorKey,
    );
  }
}

※アラートダイアログを表示する側では、shawDialogcontext: navigatorKey.currentContext!を指定(前掲のコードのとおり)。

画面例

ソースコード

今回 2 つに分割しました。

main.dart

import 'package:flutter/material.dart';

import 'map_page.dart';

final navigatorKey = GlobalKey<NavigatorState>();
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Mapbox',
      home: const MapPage(),
      navigatorKey: navigatorKey,
    );
  }
}

map_page.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';
import 'package:maptool/main.dart';

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);
        _controller.future.then((mapboxMap) {
          mapboxMap.onSymbolTapped.add(_onSymbolTap);
        });
            },
      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) {
      Future<Symbol> _symbol = mapboxMap.addSymbol(SymbolOptions(
        geometry: tapPoint,
        textField: _hhmm,
        textAnchor: "top",
        textColor: "#000",
        textHaloColor: "#FFF",
        textHaloWidth: 3,
        iconImage: "mapbox-marker-icon-blue",
        iconSize: 1,
      ));
    });
  }

  // マークをタップしたときに Symbol の情報を表示する
  void _onSymbolTap(Symbol symbol) {
    _dispSymbolInfo(symbol);
  }

  // Symbol の情報を表示する
  void _dispSymbolInfo(Symbol symbol) {
    showDialog(
      context: navigatorKey.currentContext!,
      builder: (BuildContext context) => AlertDialog(
        title: Text('Symbol ID : ${symbol.id}'),
        actions: [
          IconButton(
            icon: const Icon(Icons.close),
            color: Colors.blue,
            onPressed: () {
              Navigator.pop(context);
            },
          ),
        ],
      ),
    );
  }

  // 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