flutter_maplibre_glでstyleをいろいろ試してみる ※2024/01/31更新

# はじめに

NZ Topo50(ニュージーランドの地図)を出したいと言われていて、ググっても全く情報がなくて困っていました。
いくつか地図のサービスを利用してみてできないものかと試した結果になります。

結論としては、出せないものはスタイル作ればいける！（たぶん)

おそらくニッチなネタですね、これ。

# 更新情報
- 2024/01/31 注意点追加

# 対象

- [地理院](https://github.com/gsi-cyberjapan/gsivectortile-mapbox-gl-js)のスタイル
- [flutter_maplibre_gl](https://github.com/maplibre/flutter-maplibre-gl)のサンプルのスタイル
- [Mapbox Studio](https://www.mapbox.jp/mapbox-studio)で作成したスタイル各種
- [OpenTopoMap](https://github.com/der-stefan/OpenTopoMap/tree/master)のスタイル
- [Maptailer Clout](https://cloud.maptiler.com/maps/)のスタイル各種
- [NZ Topo50](https://data.linz.govt.nz/layer/50767-nz-topo50-maps/)を表示するために作成したスタイル
    - 一番やりたかった

|提供元|パターン|結果|備考|
|--|--|--|--|
|地理院|標準|〇||
|地理院|標準改|〇|jsonに背景色を追加しただけ|
|地理院|淡色地図|〇||
|地理院|白地図|〇||
|flutter_maplibre_gl|サンプルのスタイル|〇||
|MapBox|WMTS|✕||
|MapBox|ArcGIS|✕|タイルの提供なのでスタイルの作成が必要|
|MapBox|CARTO|✕|タイルの提供なのでスタイルの作成が必要|
|MapBox|Tabeau|✕||
|MapBox|Fulcrum|✕|CARTOとURL同じだった|
|OpenTopoMap||✕|背景の色が変わってタイルの読み込みに失敗しているのでjsonを修正すれば使えるかも？|
|MapTiler|Topo|〇|当然と言えば当然|
|MapTiler|MIERUNE Streets|〇|当然と言えば当然|
|MapTiler|MIERUNE Dark|〇|当然と言えば当然|
|MapTiler|MIERUNE Gray|〇|当然と言えば当然|
|LINZ|NZ Topo50|〇|スタイル作成で表示|

MaptTlerは当然結果かな(Maplibreの大本だし)

# コード

もし、動かしたい場合はアクセストークンなどは用意してコードを修正してください。

## Flutter

Flutter側は特に難しいことは何もしていませんね。
DropdownMenuItemの〇✕は地図が出たか出ないか。
あと、何度もスタイル切り替えてると地図が出なくなる事象に遭遇。
なんだろう？

```dart
import 'dart:async';

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final Completer<MaplibreMapController> _mapController = Completer<MaplibreMapController>();
  String _style = _style_gis_std;

  static const _style_gis_std        = 'https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/std.json';
  static const _style_gis_std2       = 'https://taka-hama.sakura.ne.jp/map/gisjp/std.json';
  static const _style_gis_pale       = 'https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/pale.json';
  static const _style_gis_blank      = 'https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/blank.json';

  static const _style_maplibre_demo  = 'https://demotiles.maplibre.org/style.json';

  static const _style_mapbox_wmts    = 'https://api.mapbox.com/styles/v1/krohigewagm/clqmimy8100am01obh9x8257o/wmts?access_token=xxxxxxxx';
  static const _style_mapbox_arcgis  = 'https://api.mapbox.com/styles/v1/krohigewagm/clqmimy8100am01obh9x8257o/tiles/256/{level}/{col}/{row}@2x?access_token=xxxxxxxx';
  static const _style_mapbox_carto   = 'https://api.mapbox.com/styles/v1/krohigewagm/clqmimy8100am01obh9x8257o/tiles/256/{z}/{x}/{y}@2x?access_token=xxxxxxxx';
  static const _style_mapbox_tabeau  = 'https://api.mapbox.com/styles/v1/krohigewagm/clqmimy8100am01obh9x8257o.html?title=true&access_token=xxxxxxxx';
  static const _style_mapbox_fulcrum = 'https://api.mapbox.com/styles/v1/krohigewagm/clqmimy8100am01obh9x8257o/tiles/256/{z}/{x}/{y}@2x?access_token=xxxxxxxx';

  static const _style_opentopomap    = 'https://hogehoge/opentopomap.json';

  static const _style_maptiler_topo  = 'https://api.maptiler.com/maps/topo-v2/style.json?key=xxxxxxxx';
  static const _style_maptiler_jp_mierune  = 'https://api.maptiler.com/maps/jp-mierune-streets/style.json?key=xxxxxxxx';
  static const _style_maptiler_jp_mierune_dark  = 'https://api.maptiler.com/maps/jp-mierune-dark/style.json?key=xxxxxxxx';
  static const _style_maptiler_jp_mierune_gray  = 'https://api.maptiler.com/maps/jp-mierune-gray/style.json?key=xxxxxxxx';

  static const _style_linz_cs_w  = 'https://data.linz.govt.nz/services/csw/?service=CSW&request=GetCapabilities';
  static const _style_linz_topo50  = 'https://hogehoge/topo50.json';

  @override
  Widget build(BuildContext context) {
    var textStyle = const TextStyle(color:Colors.black);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Maplibre Style Sample'),
        actions: [
          DropdownButton(
            elevation: 2,
            icon:const Icon(Icons.menu, size: 32),
            items: [
              DropdownMenuItem(
                value:_style_gis_std,
                child: Row(
                  children: [
                    _style == _style_gis_std ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]地理院 標準', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_gis_std2,
                child: Row(
                  children: [
                    _style == _style_gis_std2 ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]地理院 標準改', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_gis_pale,
                child: Row(
                  children: [
                    _style == _style_gis_pale ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]地理院 淡色地図', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_gis_blank,
                child: Row(
                  children: [
                    _style == _style_gis_blank ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]地理院 白地図', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_maplibre_demo,
                child: Row(
                  children: [
                    _style == _style_maplibre_demo ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]MapLibre demo', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_mapbox_wmts,
                child: Row(
                  children: [
                    _style == _style_mapbox_wmts ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox WMTS', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_mapbox_arcgis,
                child: Row(
                  children: [
                    _style == _style_mapbox_arcgis ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox ArcGIS', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_mapbox_carto,
                child: Row(
                  children: [
                    _style == _style_mapbox_carto ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox CARTO', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_mapbox_tabeau,
                child: Row(
                  children: [
                    _style == _style_mapbox_tabeau ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox Tabeau', style:textStyle),
                  ],
                ),
              ),
              DropdownMenuItem(
                value:_style_mapbox_fulcrum,
                child: Row(
                  children: [
                    _style == _style_mapbox_fulcrum ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox Fulcrum', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_mapbox_fulcrum,
                child: Row(
                  children: [
                    _style == _style_mapbox_fulcrum ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]MapBox Fulcrum', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_opentopomap,
                child: Row(
                  children: [
                    _style == _style_opentopomap ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]Open Topo map', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_maptiler_topo,
                child: Row(
                  children: [
                    _style == _style_maptiler_topo ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]MapTiler topo', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_maptiler_jp_mierune,
                child: Row(
                  children: [
                    _style == _style_maptiler_jp_mierune ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]MapTiler JP MIERUNE Streets', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_maptiler_jp_mierune_dark,
                child: Row(
                  children: [
                    _style == _style_maptiler_jp_mierune_dark ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]MapTiler JP MIERUNE Dark', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_maptiler_jp_mierune_gray,
                child: Row(
                  children: [
                    _style == _style_maptiler_jp_mierune_gray ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[〇]MapTiler JP MIERUNE Gray', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_linz_cs_w,
                child: Row(
                  children: [
                    _style == _style_linz_cs_w ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]LINZ CS-W', style:textStyle),
                  ],
                ),
              ),

              DropdownMenuItem(
                value:_style_linz_topo50,
                child: Row(
                  children: [
                    _style == _style_linz_topo50 ? const Icon( Icons.check_box, size: 32) : const Icon( Icons.check_box_outline_blank, size: 32),
                    Text('[✕]LINZ Topo50', style:textStyle),
                  ],
                ),
              ),


            ], onChanged: (String? value) {
              setState(() {
                _style = value!;
              });
            },
          ),
        ],
      ),
      body:
          MaplibreMap(
            styleString: _style,
            onStyleLoadedCallback: () {
              debugPrint("style loaded");
            },
            initialCameraPosition: const CameraPosition(
              target: LatLng(35.658584, 139.7454316),
              zoom: 10.0,
            ),

            onMapCreated: (MaplibreMapController controller) {
              _mapController.complete(controller);
            },

            myLocationEnabled: true,
          ),
    );
  }
}
```

## Topo50のスタイル

[MapLibre Style Spec](https://maplibre.org/maplibre-style-spec/) を適当に見ただけではだめだった。
ちゃんと読めば分かったかな？
面倒だったので、[Maputnik](https://maputnik.github.io/editor)でスタイルをエクスポートしてみたけど、うまくいかず、ChatGPTに聞いたらほぼほぼこの形で教えてくれました。
ちなみに、うまくいかなかった原因は、layersでタイルレイヤーを指定してなかった為。
しかし、情報なくて、お金がかかるのかどうなのかもわからなかった(無料プランがあることが分かったので試せた）

```json:topo50.json
{
  "version": 8,
  "sources": {
    "tiles-source": {
      "type": "raster",
      "tiles": [
        "https://tiles-cdn.koordinates.com/services;key=xxxxx/tiles/v4/layer=50767/EPSG:3857/{z}/{x}/{y}.png"
      ],
      "tileSize": 256,
      "attribution": "Open Geospatial Consortium"    }
  },
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": {
        "background-color": "rgb(239,239,239)"
      }
    },
    {
      "id": "tiles-layer",
      "type": "raster",
      "source": "tiles-source",
      "minzoom": 0,
      "maxzoom": 22
    }
  ]
}
```

# 注意点

スタイルを切り替えると、シンボルがクリアされるようなので、onStyleLoadedCallbackでシンボルをMaplibreに登録するようにしたほうがよさそう。


# 参考
- [Maputnik](https://maputnik.github.io/editor)
- [MapLibre Style Spec](https://maplibre.org/maplibre-style-spec/)
- [NZ Topo50 Maps | WMTS | LINZ Data Service](https://data.linz.govt.nz/layer/50767-nz-topo50-maps/services/tiles/)
    - ToPo50を使う場合はここに登録が必要
    - 料金等のプランは [Koordinates](https://koordinates.com/my/export-history/)のサイトに載ってる
- [ベクトルタイル用スタイルの書き方メモ（Mapbox, Maplibre, ArcGIS API for Javascript）](https://qiita.com/T-ubu/items/02a9725dd6329d35d477)
- [MapLibre GL JSのスタイルファイルを読む](https://qiita.com/moritoru/items/e160f14e822ada67461c)