🗺️

Flutterのマップパッケージ3選(Google Map, Flutter Map, Mapbox)

2024/12/24に公開

Flutter Advent Calendar 2024 の 24日目🎅🎄

Flutterのマップパッケージを紹介します。

packages 概要 料金
google_maps_flutter Google Map SDK 無料(ネイティブアプリの利用のみ)
flutter_map 地図のラスター/ベクターデータを指定して好みのマップを利用できる 無料
mapbox_maps_flutter Mapbox SDK 従量課金

本記事のサンプルコードはこちらで確認できます。

google_maps_flutter

サンプルコード

その1 その2 その3

Marker

MarkerGoogleMapmarkersにセットするとマーカーピンが表示されます。

Marker(
    markerId: MarkerId(id),
    icon: BitmapDescriptor.defaultMarker,
    position: LatLng(
      34.70239193591162,
      135.4958750992668,
    ),
    onTap: () {},
);

BitmapDescriptorはデフォルトは赤色ですが、それ以外の色も用意されています。

Marker(
    icon: BitmapDescriptor.defaultMarkerWithHue(
        BitmapDescriptor.hueAzure,
    ),
    ...
);
BitmapDescriptorの種類
  /// Convenience hue value representing red.
  static const double hueRed = 0.0;

  /// Convenience hue value representing orange.
  static const double hueOrange = 30.0;

  /// Convenience hue value representing yellow.
  static const double hueYellow = 60.0;

  /// Convenience hue value representing green.
  static const double hueGreen = 120.0;

  /// Convenience hue value representing cyan.
  static const double hueCyan = 180.0;

  /// Convenience hue value representing azure.
  static const double hueAzure = 210.0;

  /// Convenience hue value representing blue.
  static const double hueBlue = 240.0;

  /// Convenience hue value representing violet.
  static const double hueViolet = 270.0;

  /// Convenience hue value representing magenta.
  static const double hueMagenta = 300.0;

  /// Convenience hue value representing rose.
  static const double hueRose = 330.0;

カスタマイズ

表示したいマーカーをBitmapDescriptorに変換する必要があります。

Widget → 画像 → BitmapDescriptorに変換して、マーカーピンとしてセットします。

サンプルコードでは、CafeMarkerのWidgetを構築したものを画像にし、BitmapDescriptorに変換してセットします。

データはGoogle Places APIから取得したデータを利用しました。

CafeMarker
import 'package:flutter/material.dart';

class CafeMarker extends StatelessWidget {
  const CafeMarker({
    super.key,
    required this.id,
    required this.globalKey,
    required this.title,
    required this.rating,
    required this.isSelected,
  });

  final String id;
  final GlobalKey globalKey;
  final String title;
  final double rating;
  final bool isSelected;

  
  Widget build(BuildContext context) {
    final baseWidth = 120.0 * (isSelected ? 1.5 : 1);
    final markerSize = Size(baseWidth, baseWidth);
    final iconSize = 40.0 * (isSelected ? 1.5 : 1);
    final radius = iconSize * 1.2;

    final textWidth = markerSize.width * 1.9;

    final fontSize = 32.0 * (isSelected ? 1.1 : 1);

    const fontColor = Colors.black;
    const fontBorderColor = Colors.white;
    return RepaintBoundary(
      key: globalKey,
      child: SizedBox(
        width: textWidth,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Flexible(
              child: CircleAvatar(
                backgroundColor: Colors.redAccent,
                radius: radius,
                child: Padding(
                  padding: const EdgeInsets.only(top: 8, bottom: 8),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(
                        Icons.local_cafe,
                        color: Colors.white,
                        size: iconSize,
                      ),
                      Flexible(
                        child: Text(
                          rating.toStringAsFixed(1),
                          style: TextStyle(
                            height: 1.2,
                            color: Colors.white,
                            fontSize: fontSize * 0.9,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SizedBox(
              width: textWidth,
              child: Text(
                title,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: fontColor,
                  fontSize: fontSize,
                  height: 1.3,
                  shadows: const [
                    Shadow(
                      offset: Offset(-1.5, -1.5),
                      color: fontBorderColor,
                    ),
                    Shadow(
                      offset: Offset(1.5, -1.5),
                      color: fontBorderColor,
                    ),
                    Shadow(
                      offset: Offset(1.5, 1.5),
                      color: fontBorderColor,
                    ),
                    Shadow(
                      offset: Offset(-1.5, 1.5),
                      color: fontBorderColor,
                    ),
                  ],
                ),
                maxLines: 2,
                textAlign: TextAlign.center,
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
カスタムマーカーを表示
class GoogleMapPage extends StatefulWidget {
  const GoogleMapPage({super.key});

  
  State<GoogleMapPage> createState() => _State();
}

class _State extends State<GoogleMapPage> {

  List<CafeMarker> _cafeMarkers = [];
  List<({Place place, BitmapDescriptor bitmap})> _cafeBitmapDescriptors = [];
  CafeMarker? _selectedCafeMarker;
  ({Place place, BitmapDescriptor bitmap})? _selectedCafeBitmapDescriptor;

  ...

  
  void initState() {
    super.initState();
    Future(() async {
      final places = await fetchPlaces();

      setState(() {
        ...
        /// カスタムマーカー作成
        _cafeMarkers = places
            .map(
              (e) => CafeMarker(
                id: e.placeId,
                globalKey: GlobalKey(),
                title: e.detail.name,
                rating: e.rating,
                isSelected: false,
              ),
            )
            .toList();
        WidgetsBinding.instance.addPostFrameCallback((_) async {
          final result = <({Place place, BitmapDescriptor bitmap})>[];
          for (final widget in _cafeMarkers) {
            final bitmap = await createCustomMarker(widget.globalKey);
            if (bitmap == null) {
              continue;
            }
            final id = widget.id;
            final place = places.firstWhereOrNull((e) => e.placeId == id);
            if (place == null) {
              continue;
            }
            result.add(
              (place: place, bitmap: bitmap),
            );
          }

          setState(() {
            _cafeBitmapDescriptors = result;
          });
        });
      });
    });
  }

  
  Widget build(BuildContext context) {
    final markers = _cafeBitmapDescriptors.map((e) {
      final place = e.place;
      final isSelected = _selectedMarkerId?.value == place.placeId;
      return Marker(
        markerId: MarkerId(place.placeId),
        zIndex: isSelected ? 100 : place.rating,
        icon: isSelected
            ? _selectedCafeBitmapDescriptor?.bitmap ?? e.bitmap
            : e.bitmap,
        position: LatLng(
          place.geometry.location.lat,
          place.geometry.location.lng,
        ),
        onTap: () {
          setState(() {
            final markerId = MarkerId(place.placeId);
            final isEnabled = _selectedMarkerId != markerId;
            _selectedMarkerId = isEnabled ? markerId : null;
            _selectedCafeMarker = isEnabled
                ? CafeMarker(
                    id: place.placeId,
                    globalKey: GlobalKey(),
                    title: place.detail.name,
                    rating: place.rating,
                    isSelected: true,
                  )
                : null;
            final selectedCafeMarker = _selectedCafeMarker;
            if (selectedCafeMarker != null) {
              WidgetsBinding.instance.addPostFrameCallback((_) async {
                final bitmap =
                    await createCustomMarker(selectedCafeMarker.globalKey);
                if (bitmap == null) {
                  return;
                }

                setState(() {
                  _selectedCafeBitmapDescriptor = (
                    place: place,
                    bitmap: bitmap,
                  );
                });
              });
            } else {
              _selectedCafeBitmapDescriptor = null;
            }
          });
        },
      );
    }).toSet();

    return Scaffold(
      body: Stack(
        children: [
          // Widgetとして表示しないと画像化できないため
          if (_selectedCafeMarker != null) _selectedCafeMarker!,
          ..._cafeMarkers, 
          GoogleMap(
            markers: markers,
            ...
          ),
          ...
        ],
      ),
    );
  }
}
Widgetを画像に変換する
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

Future<BitmapDescriptor?> createCustomMarker(
  GlobalKey key, {
  double? width,
  double? height,
}) async {
  try {
    final context = key.currentContext;
    if (context == null) {
      return null;
    }
    final boundary = context.findRenderObject() as RenderRepaintBoundary?;
    if (boundary == null) {
      return null;
    }
    final image = await boundary.toImage();
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    if (byteData == null) {
      return null;
    }
    if (!context.mounted) {
      return null;
    }
    return BytesMapBitmap(
      byteData.buffer.asUint8List(),
      width: width,
      height: height,
      imagePixelRatio: MediaQuery.maybeDevicePixelRatioOf(context),
    );
    // ignore: avoid_catches_without_on_clauses
  } catch (e) {
    debugPrint(e.toString());
    return null;
  }
}
fetchPlaces
import 'dart:convert';

import 'package:flutter/services.dart';
import 'package:map_sample/core/entities/place.dart';
import 'package:map_sample/core/gen/assets.gen.dart';

Future<List<Place>> fetchPlaces() async {
  final data = await rootBundle.loadString(Assets.json.spots);
  final jsonList = jsonDecode(data) as List<dynamic>;
  final result =
      jsonList.map((e) => Place.fromJson(e as Map<String, dynamic>)).toList();

  return result;
}

Polyline

PolylineGoogleMappolylinesにセットするとルートが表示されます。

データはGoogle Places APIから取得したデータを利用しました。

Polyline(
    polylineId: const PolylineId('route1'),
    points: [LatLng(34.70205, 135.49614), LatLng(34.70204, 135.49616), ...],
    color: Colors.blueAccent,
    width: 8,
),

サンプルコードでは、大阪駅からカフェまでのルートを表示します。

大阪駅からカフェまでのルートを表示
class GoogleMapPage extends StatefulWidget {
  const GoogleMapPage({super.key});

  
  State<GoogleMapPage> createState() => _State();
}

class _State extends State<GoogleMapPage> {
  ...

  Set<Polyline> _polylines = {};

  
  void initState() {
    super.initState();
    Future(() async {
      final routes = await fetchRoute();

      setState(() {
        /// ルート
        _polylines = {
          Polyline(
            polylineId: const PolylineId('route1'),
            points: routes.map((e) => LatLng(e.first, e.last)).toList(),
            color: Colors.blueAccent,
            width: 8,
          ),
        };
      });
    });
  }

  
  Widget build(BuildContext context) {
    ...
    return Scaffold(
      body: Stack(
        children: [
          ...
          GoogleMap(
            polylines: _polylines,
            ...
          ),
          ...
        ],
      ),
    );
  }
}
fetchRoute
import 'dart:convert';

import 'package:flutter/services.dart';
import 'package:map_sample/core/gen/assets.gen.dart';

Future<List<List<double>>> fetchRoute() async {
  final data = await rootBundle.loadString(Assets.json.route);
  final jsonList = jsonDecode(data) as List<dynamic>;
  final result = jsonList
      .map((e) => (e as List<dynamic>).map((d) => d as double).toList())
      .toList();

  return result;
}

Polygon

PolygonGoogleMappolygonsにセットすると、指定範囲の色付けができます。

Polygon(
    polygonId: PolygonId(e.id),
    points: [LatLng(34.7086, 135.4945), LatLng(34.708, 135.4947), ...],
    strokeWidth: 4,
    fillColor: Colors.green.withOpacity(0.2),
    strokeColor: Colors.green,
),

サンプルコードでは、大阪駅周辺の範囲を緑色で表示します。

GeoJsonファイルは法務省登記所備付地図データを利用しました。

大阪駅周辺の範囲を緑色で表示
class GoogleMapPage extends StatefulWidget {
  const GoogleMapPage({super.key});

  
  State<GoogleMapPage> createState() => _State();
}

class _State extends State<GoogleMapPage> {
  ...

  Set<Polyline> _polylines = {};

  
  void initState() {
    super.initState();
    Future(() async {
      final geoJsons = await fetchGeoJson();

      setState(() {
        /// ポリゴン
        _polygons = geoJsons
            .map(
              (e) => Polygon(
                polygonId: PolygonId(e.id),
                points: e.geoPoints
                    .map((p) => LatLng(p.latitude, p.longitude))
                    .toList(),
                strokeWidth: 4,
                fillColor: Colors.green.withOpacity(0.2),
                strokeColor: Colors.green,
              ),
            )
            .toSet();
      });
    });
  }

  
  Widget build(BuildContext context) {
    ...
    return Scaffold(
      body: Stack(
        children: [
          ...
          GoogleMap(
            polygons: _polygons,
            ...
          ),
          ...
        ],
      ),
    );
  }
}
fetchGeoJson
import 'package:flutter/services.dart';
import 'package:geojson/geojson.dart';
import 'package:geopoint/geopoint.dart';
import 'package:map_sample/core/gen/assets.gen.dart';

typedef Result = ({
  String id,
  List<GeoPoint> geoPoints,
});

Future<List<Result>> fetchGeoJson() async {
  final data = await rootBundle.loadString(Assets.json.a271276R);
  final geo = GeoJson();
  await geo.parse(data);

  final results = <Result>[];
  for (var i = 0; i < geo.features.length; i++) {
    final feature = geo.features[i];
    final id = feature.properties?['ID'] as String? ?? '';
    final polygon = geo.polygons[i];
    final geoPoints =
        polygon.geoSeries.expand((element) => element.geoPoints).toList();
    results.add((id: id, geoPoints: geoPoints));
  }

  return results;
}

flutter_map

サンプルコード

その1

TileLayerurlTemplateにラスターデータとなるURLを指定すると、マップが表示されます。

FlutterMap(
    children: [
      TileLayer(
        urlTemplate: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png',
      ),
    ],
),

OpenStreetMapのWikiにラスタータイルのプロバイダーリストがまとめられています。

Raster tile providers

Marker

MarkerMarkerLayerにセットするとマーカーピンが表示されます。

FlutterMapはWidgetでマーカーピンを構築できるので、簡単にカスタマイズできて便利です。

Marker(
    point: LatLng(
        34.70239193591162,
        135.4958750992668,
    ),
    child: GestureDetector(
        onTap: () {
            ...
        },
        child: Icon(
            Icons.pin_drop_sharp,
        ),
    ),
);
大阪駅周辺のカフェを表示
class FlutterMapPage extends StatefulWidget {
  const FlutterMapPage({super.key});

  
  State<FlutterMapPage> createState() => _State();
}

class _State extends State<FlutterMapPage> {

  final _mapController = MapController();

  List<Place> _places = [];
  int? _selectedMarkerId;

  
  void initState() {
    super.initState();
    Future(() async {
      final places = await fetchPlaces();

      setState(() {
        /// デフォルトマーカー
        _places = places;
      });
    });
  }

  
  Widget build(BuildContext context) {
    final markers = _places.mapIndexed((index, e) {
      final isSelected = _selectedMarkerId == index;
      final point = LatLng(
        e.geometry.location.lat,
        e.geometry.location.lng,
      );

      const fontColor = Colors.black;
      const fontBorderColor = Colors.white;
      const fontSize = 10.0;
      return Marker(
        point: point,
        width: 120,
        height: 72,
        child: GestureDetector(
          onTap: () {
            final currentZoom = _mapController.camera.zoom;
            _mapController.move(point, currentZoom);
            setState(() {
              _selectedMarkerId = _selectedMarkerId != index ? index : null;
            });
          },
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(
                Icons.pin_drop_sharp,
                size: 40,
                color: isSelected ? Colors.deepPurple : Colors.redAccent,
              ),
              Flexible(
                child: Text(
                  e.name,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: fontColor,
                    fontSize: fontSize,
                    height: 1.3,
                    shadows: [
                      Shadow(
                        offset: Offset(-1.5, -1.5),
                        color: fontBorderColor,
                      ),
                      Shadow(
                        offset: Offset(1.5, -1.5),
                        color: fontBorderColor,
                      ),
                      Shadow(
                        offset: Offset(1.5, 1.5),
                        color: fontBorderColor,
                      ),
                      Shadow(
                        offset: Offset(-1.5, 1.5),
                        color: fontBorderColor,
                      ),
                    ],
                  ),
                  maxLines: 2,
                  textAlign: TextAlign.center,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ),
        ),
      );
    }).toList();
    return Scaffold(
      body: Stack(
        children: [
          FlutterMap(
            children: [
              TileLayer(
                urlTemplate:
                    'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png',
              ),
              MarkerLayer(
                markers: markers,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Polyline

PolylinePolylineLayerにセットするとルートを表示できます。

Polyline(
    points: [LatLng(34.70205, 135.49614), LatLng(34.70204, 135.49616), ...],
    color: Colors.blueAccent,
    strokeWidth: 8,
),
大阪駅からカフェまでのルートを表示
class FlutterMapPage extends StatefulWidget {
  const FlutterMapPage({super.key});

  
  State<FlutterMapPage> createState() => _State();
}

class _State extends State<FlutterMapPage> {

  List<Polyline> _polylines = [];

  
  void initState() {
    super.initState();
    Future(() async {
      final routes = await fetchRoute();

      setState(() {
        /// ルート
        _polylines = [
          Polyline(
            points: routes.map((e) => LatLng(e.first, e.last)).toList(),
            color: Colors.blueAccent,
            strokeWidth: 8,
          ),
        ];
      });
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          FlutterMap(
            children: [
              ...
              PolylineLayer(
                polylines: _polylines,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Polygon

PolygonPolygonLayerにセットすると、指定範囲の色付けができます。

Polygon(
    color: Colors.green.withOpacity(0.2),
    borderColor: Colors.green,
    borderStrokeWidth: 4,
    points: [LatLng(34.7086, 135.4945), LatLng(34.708, 135.4947), ...],
),
大阪駅周辺の範囲を緑色で表示
class FlutterMapPage extends StatefulWidget {
  const FlutterMapPage({super.key});

  
  State<FlutterMapPage> createState() => _State();
}

class _State extends State<FlutterMapPage> {

  List<Polygon> _polygons = [];

  
  void initState() {
    super.initState();
    Future(() async {
      final geoJsons = await fetchGeoJson();

      setState(() {
        /// ポリゴン
        _polygons = geoJsons
            .map(
              (e) => Polygon(
                color: Colors.green.withOpacity(0.2),
                borderColor: Colors.green,
                borderStrokeWidth: 4,
                points: e.geoPoints
                    .map(
                      (geoPoint) => LatLng(
                        geoPoint.latitude,
                        geoPoint.longitude,
                      ),
                    )
                    .toList(),
              ),
            )
            .toList();
      });
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          FlutterMap(
            children: [
              ...
              PolygonLayer(
                polygons: _polygons,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

mapbox_maps_flutter

サンプルコード

その1

MapWidgetonMapCreatedから得られるMapboxMapオブジェクトを利用して、マーカーなどの表示を実現できます。

MapWidget(
    onMapCreated: (mapboxMap) async {
      // mapBoxオブジェクトを利用してマップ上の操作を制御する。
    ),
),  

Marker

PointAnnotationManagerを利用してマーカーを表示します。

PointAnnotationOptionsにデータを指定し、PointAnnotationManagercreateMultiにセットすると表示できます。

マーカーは画像をバイト配列に変換して設定します。また、マーカーにテキストを表示するだけであれば簡単にできます。

マーカーの画像はこちらからダウンロードしました。

final manager =
  await mapboxMap.annotations.createPointAnnotationManager();
final bytes = await rootBundle.load(Assets.images.redMarker.path);
final image = bytes.buffer.asUint8List();
final options = [
    PointAnnotationOptions(
        geometry: Point(
            coordinates: Position(
              135.4958750992668, // 経度
              34.70239193591162, // 緯度
            ),
        ),
        textField: '大阪駅',
        textSize: 10,
        textOffset: [0, 2.5],
        textLineHeight: 1.3,
        textJustify: TextJustify.CENTER,
        image: image,
    ),
];
await manager.createMulti(options);

マーカーのタップイベントはOnPointAnnotationClickListenerを継承した実体クラスを作る必要があります。実体クラスでタップイベントを実装すると引数のバケツリレーが発生して面倒なのでコールバック関数でタップイベントを検知できるようにします。

import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

class AnnotationClickListener extends OnPointAnnotationClickListener {
  AnnotationClickListener({
    required this.onClick,
  });

  final void Function(PointAnnotation annotation) onClick;

  
  void onPointAnnotationClick(PointAnnotation annotation) {
    onClick(annotation);
  }
}
manager.addOnPointAnnotationClickListener(
    AnnotationClickListener(
      onClick: (annotation) {
        // タップイベントの処理はここに書く
      },
    ),
);
大阪駅周辺のカフェを表示
class MapboxPage extends StatefulWidget {
  const MapboxPage({super.key});

  
  State<MapboxPage> createState() => _State();
}

class _State extends State<MapboxPage> {
  late final MapboxMap mapboxMap;

  final defaultLatLng = Point(
    coordinates: Position(
      135.4958750992668,
      34.70239193591162,
    ),
  );

  final defaultZoom = 14.0;

  
  Widget build(BuildContext context) {
    /// マーカー設定
    Future<void> setupAnnotation(List<Place> places) async {
      final manager =
          await mapboxMap.annotations.createPointAnnotationManager();
      final bytes = await rootBundle.load(Assets.images.redMarker.path);
      final image = bytes.buffer.asUint8List();
      final options = places
          .map(
            (e) => PointAnnotationOptions(
              geometry: Point(
                coordinates: Position(
                  e.geometry.location.lng,
                  e.geometry.location.lat,
                ),
              ),
              textField: e.name,
              textSize: 10,
              textOffset: [0, 2.5],
              textLineHeight: 1.3,
              textJustify: TextJustify.CENTER,
              image: image,
            ),
          )
          .toList();
      await manager.createMulti(options);

      manager.addOnPointAnnotationClickListener(
        AnnotationClickListener(
          onClick: (annotation) {
            mapboxMap.flyTo(
              CameraOptions(center: annotation.geometry),
              MapAnimationOptions(duration: 500, startDelay: 0),
            );
          },
        ),
      );
    }

    return Scaffold(
      body: Stack(
        children: [
          MapWidget(
            cameraOptions: CameraOptions(
              center: defaultLatLng,
              zoom: defaultZoom,
            ),
            onMapCreated: (mapBox) async {
              mapboxMap = mapBox;

              /// マーカー
              final places = await fetchPlaces();
              await setupAnnotation(places);
            },
          ),
        ],
      ),
    );
  }
}

Polyline

PolylineAnnotationManagerを利用してルートを表示します。

PolylineAnnotationOptionsにデータを指定し、PolylineAnnotationManagercreateMultiにセットすると表示できます。

final manager =
  await mapboxMap.annotations.createPolylineAnnotationManager();
final options = [
    PolylineAnnotationOptions(
      geometry: LineString(
        coordinates: [
            LatLng(135.49614, 34.70205),
            LatLng(135.49616, 34.70204),
            ...
        ],
      ),
      lineColor: Colors.blueAccent.value,
      lineWidth: 6,
    ),
];
await manager.createMulti(options);
大阪駅からカフェまでのルートを表示
class MapboxPage extends StatefulWidget {
  const MapboxPage({super.key});

  
  State<MapboxPage> createState() => _State();
}

class _State extends State<MapboxPage> {
  late final MapboxMap mapboxMap;

  
  Widget build(BuildContext context) {
    /// ルート設定
    Future<void> setupRoutes(List<List<double>> routes) async {
      final manager =
          await mapboxMap.annotations.createPolylineAnnotationManager();

      final options = [
        PolylineAnnotationOptions(
          geometry: LineString(
            coordinates: routes
                .map(
                  (e) => Position(e.last, e.first),
                )
                .toList(),
          ),
          lineColor: Colors.blueAccent.value,
          lineWidth: 6,
        ),
      ];

      await manager.createMulti(options);
    }

    return Scaffold(
      body: Stack(
        children: [
          MapWidget(
            ...
            onMapCreated: (mapBox) async {
              mapboxMap = mapBox;

              /// ルート
              final routes = await fetchRoute();
              await setupRoutes(routes);
            },
          ),
        ],
      ),
    );
  }
}

Polygon

PolygonAnnotationManagerを利用してルートを表示します。

PolygonAnnotationOptionsにデータを指定し、PolygonAnnotationManagercreateMultiにセットすると表示できます。

PolygonAnnotationOptions(
    geometry: Polygon(
        coordinates: [
            [LatLng(135.4945, 34.7086), LatLng(135.4947, 34.708), ...],
        ],
    ),
    fillColor: Colors.green.withOpacity(0.5).value,
    fillOutlineColor: Colors.green.value,
),
大阪駅周辺の範囲を緑色で表示
class MapboxPage extends StatefulWidget {
  const MapboxPage({super.key});

  
  State<MapboxPage> createState() => _State();
}

class _State extends State<MapboxPage> {
  late final MapboxMap mapboxMap;

  
  Widget build(BuildContext context) {
    /// ポリゴン設定
    Future<void> setupPolygon(List<Result> polygonList) async {
      final manager =
          await mapboxMap.annotations.createPolygonAnnotationManager();

      final options = [
        PolygonAnnotationOptions(
          geometry: Polygon(
            coordinates: polygonList
                .map(
                  (e) => e.geoPoints
                      .map(
                        (geoPoint) =>
                            Position(geoPoint.longitude, geoPoint.latitude),
                      )
                      .toList(),
                )
                .toList(),
          ),
          fillColor: Colors.green.withOpacity(0.5).value,
          fillOutlineColor: Colors.green.value,
        ),
      ];
      await manager.createMulti(options);
    }

    return Scaffold(
      body: Stack(
        children: [
          MapWidget(
            ...
            onMapCreated: (mapBox) async {
              mapboxMap = mapBox;

              /// ポリゴン
              final geoJsons = await fetchGeoJson();
              await setupPolygon(geoJsons);
            },
          ),
        ],
      ),
    );
  }
}

終わりに

今回紹介したパッケージ以外にもマップパッケージはあります。

maplibre_glもぐもぐさんが詳しいのでぜひ聞いてみてください(無茶振り)

ここまで読んでいただきありがとうございました!

株式会社Never

Discussion