From 43b7725d09785a4845ff85600578999fe7a51462 Mon Sep 17 00:00:00 2001 From: Joscha <34318751+josxha@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:33:51 +0100 Subject: [PATCH] feat: add maplibre plugin (#52) - [x] `FlutterMapAdapter` - [x] `MapLibreLayer` - [x] update example app - [x] add documentation --- .github/ISSUE_TEMPLATE/1-bug.yml | 1 + .github/ISSUE_TEMPLATE/2-feature.yml | 1 + example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- example/devtools_options.yaml | 3 + example/lib/flutter_map_maplibre/config.dart | 8 ++ example/lib/flutter_map_maplibre/page.dart | 97 ++++++++++++++++ example/lib/flutter_map_maplibre/page2.dart | 97 ++++++++++++++++ example/lib/main.dart | 40 ++++--- .../flutter/generated_plugin_registrant.cc | 4 + example/linux/flutter/generated_plugins.cmake | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/pubspec.yaml | 3 + example/web/index.html | 53 +++++---- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 2 + flutter_map_maplibre/.gitignore | 29 +++++ flutter_map_maplibre/.metadata | 10 ++ flutter_map_maplibre/CHANGELOG.md | 5 + flutter_map_maplibre/LICENSE | 21 ++++ flutter_map_maplibre/README.md | 104 ++++++++++++++++++ flutter_map_maplibre/analysis_options.yaml | 1 + flutter_map_maplibre/example/example.md | 6 + .../lib/flutter_map_maplibre.dart | 3 + flutter_map_maplibre/lib/src/extensions.dart | 41 +++++++ .../lib/src/flutter_map_adapter.dart | 41 +++++++ .../lib/src/maplibre_layer.dart | 66 +++++++++++ flutter_map_maplibre/pubspec.yaml | 21 ++++ 28 files changed, 629 insertions(+), 39 deletions(-) create mode 100644 example/devtools_options.yaml create mode 100644 example/lib/flutter_map_maplibre/config.dart create mode 100644 example/lib/flutter_map_maplibre/page.dart create mode 100644 example/lib/flutter_map_maplibre/page2.dart create mode 100644 flutter_map_maplibre/.gitignore create mode 100644 flutter_map_maplibre/.metadata create mode 100644 flutter_map_maplibre/CHANGELOG.md create mode 100644 flutter_map_maplibre/LICENSE create mode 100644 flutter_map_maplibre/README.md create mode 100644 flutter_map_maplibre/analysis_options.yaml create mode 100644 flutter_map_maplibre/example/example.md create mode 100644 flutter_map_maplibre/lib/flutter_map_maplibre.dart create mode 100644 flutter_map_maplibre/lib/src/extensions.dart create mode 100644 flutter_map_maplibre/lib/src/flutter_map_adapter.dart create mode 100644 flutter_map_maplibre/lib/src/maplibre_layer.dart create mode 100644 flutter_map_maplibre/pubspec.yaml diff --git a/.github/ISSUE_TEMPLATE/1-bug.yml b/.github/ISSUE_TEMPLATE/1-bug.yml index 0f24a0c..db4949a 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yml +++ b/.github/ISSUE_TEMPLATE/1-bug.yml @@ -12,6 +12,7 @@ body: - "other" - "flutter_map_cache" - "flutter_map_compass" + - "flutter_map_maplibre" - "flutter_map_mbtiles" - "flutter_map_pmtiles" - "vector_map_tiles_mbtiles" diff --git a/.github/ISSUE_TEMPLATE/2-feature.yml b/.github/ISSUE_TEMPLATE/2-feature.yml index 9603573..f8617f9 100644 --- a/.github/ISSUE_TEMPLATE/2-feature.yml +++ b/.github/ISSUE_TEMPLATE/2-feature.yml @@ -12,6 +12,7 @@ body: - "other" - "flutter_map_cache" - "flutter_map_compass" + - "flutter_map_maplibre" - "flutter_map_mbtiles" - "flutter_map_pmtiles" - "vector_map_tiles_mbtiles" diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7c36a37..9abbe31 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -45,7 +45,7 @@ android { applicationId "com.example.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 23 // flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/build.gradle b/example/android/build.gradle index e83fb5d..2a2de25 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/example/lib/flutter_map_maplibre/config.dart b/example/lib/flutter_map_maplibre/config.dart new file mode 100644 index 0000000..4b04e6b --- /dev/null +++ b/example/lib/flutter_map_maplibre/config.dart @@ -0,0 +1,8 @@ +/// **Use your own key for your project!** +/// This key will be rotated occasionally. +/// Protomaps offers free usage for non commercial projects and affordable +/// pricing for commercial projects. +/// +/// A list with a lot of compatible tile providers can be found here: +/// https://github.com/maplibre/awesome-maplibre?tab=readme-ov-file#maptile-providers +const protomapsKey = '48c711a2fc69c6f0'; diff --git a/example/lib/flutter_map_maplibre/page.dart b/example/lib/flutter_map_maplibre/page.dart new file mode 100644 index 0000000..602250b --- /dev/null +++ b/example/lib/flutter_map_maplibre/page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_maplibre/flutter_map_maplibre.dart'; +import 'package:flutter_map_plugins_example/common/attribution_widget.dart'; +import 'package:flutter_map_plugins_example/flutter_map_maplibre/config.dart'; +import 'package:latlong2/latlong.dart'; + +class MapLibreFlutterMapPage extends StatefulWidget { + const MapLibreFlutterMapPage({super.key}); + + @override + State createState() => _MapLibreFlutterMapPageState(); +} + +class _MapLibreFlutterMapPageState extends State { + final _mapController = MapController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text('MapLibre in FlutterMap'), + ), + body: FlutterMap( + mapController: _mapController, + options: MapOptions( + initialZoom: 4, + initialCenter: const LatLng(0, 0), + maxZoom: 20, + // maplibre does not support an unbound latitude + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ), + ), + children: [ + const MapLibreLayer( + initStyle: + 'https://api.protomaps.com/styles/v2/light.json?key=$protomapsKey', + ), + const CircleLayer( + circles: [ + CircleMarker( + point: LatLng(10, 20), + radius: 15, + color: Colors.blue, + borderColor: Colors.black, + borderStrokeWidth: 2, + ), + ], + ), + const MarkerLayer( + markers: [ + Marker( + point: LatLng(15, 5), + width: 40, + height: 40, + child: Icon(Icons.location_on, color: Colors.red, size: 40), + alignment: Alignment.topCenter, + ), + ], + ), + PolylineLayer( + polylines: [ + Polyline( + points: const [ + LatLng(-20, -10), + LatLng(-15, -15), + LatLng(-20, -25), + ], + color: Colors.purple, + strokeWidth: 3, + ), + ], + ), + PolygonLayer( + polygons: [ + Polygon( + points: const [ + LatLng(8, -25), + LatLng(-5, -23), + LatLng(5, -10), + LatLng(10, -15), + ], + color: Colors.pink.withOpacity(0.8), + ), + ], + ), + const OsmAttributionWidget(), + ], + ), + ); + } +} diff --git a/example/lib/flutter_map_maplibre/page2.dart b/example/lib/flutter_map_maplibre/page2.dart new file mode 100644 index 0000000..a73f3a0 --- /dev/null +++ b/example/lib/flutter_map_maplibre/page2.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as fm; +import 'package:flutter_map_maplibre/flutter_map_maplibre.dart'; +import 'package:flutter_map_plugins_example/flutter_map_maplibre/config.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:maplibre/maplibre.dart'; + +class FlutterMapMapLibrePage extends StatefulWidget { + const FlutterMapMapLibrePage({super.key}); + + @override + State createState() => _FlutterMapMapLibrePageState(); +} + +class _FlutterMapMapLibrePageState extends State { + /// The MapLibreMap controller + // ignore: unused_field, use_late_for_private_fields_and_variables + MapController? _controller; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text('FlutterMap in MapLibre'), + ), + body: MapLibreMap( + onMapCreated: (controller) => _controller = controller, + options: MapOptions( + initCenter: Position(0, 0), + initZoom: 3, + maxPitch: 0, // flutter_map doesn't support pitch, disable it here + initStyle: + 'https://api.protomaps.com/styles/v2/light.json?key=$protomapsKey', + ), + children: [ + const FlutterMapAdapter( + child: fm.CircleLayer( + circles: [ + fm.CircleMarker( + point: LatLng(10, 20), + radius: 15, + color: Colors.blue, + borderColor: Colors.black, + borderStrokeWidth: 2, + ), + ], + ), + ), + const FlutterMapAdapter( + child: fm.MarkerLayer( + markers: [ + fm.Marker( + point: LatLng(15, 5), + width: 40, + height: 40, + child: Icon(Icons.location_on, color: Colors.red, size: 40), + alignment: Alignment.topCenter, + ), + ], + ), + ), + FlutterMapAdapter( + child: fm.PolylineLayer( + polylines: [ + fm.Polyline( + points: const [ + LatLng(-20, -10), + LatLng(-15, -15), + LatLng(-20, -25), + ], + color: Colors.purple, + strokeWidth: 3, + ), + ], + ), + ), + FlutterMapAdapter( + child: fm.PolygonLayer( + polygons: [ + fm.Polygon( + points: const [ + LatLng(8, -25), + LatLng(-5, -23), + LatLng(5, -10), + LatLng(10, -15), + ], + color: Colors.pink.withOpacity(0.8), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 4fcf88f..ff25cf8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map_plugins_example/flutter_map_cache/page.dart'; import 'package:flutter_map_plugins_example/flutter_map_compass/page.dart'; +import 'package:flutter_map_plugins_example/flutter_map_maplibre/page.dart'; +import 'package:flutter_map_plugins_example/flutter_map_maplibre/page2.dart'; import 'package:flutter_map_plugins_example/flutter_map_mbtiles/page.dart'; import 'package:flutter_map_plugins_example/flutter_map_pmtiles/page.dart'; import 'package:flutter_map_plugins_example/vector_map_tiles_mbtiles/page.dart'; @@ -29,13 +31,15 @@ class MyApp extends StatelessWidget { initialRoute: '/', routes: { '/': (context) => const SelectionPage(), - 'flutter_map_cache': (context) => const FlutterMapCachePage(), - 'flutter_map_pmtiles': (context) => const FlutterMapPmTilesPage(), - 'vector_map_tiles_pmtiles': (context) => VectorMapTilesPmTilesPage(), - 'flutter_map_mbtiles': (context) => const FlutterMapMbTilesPage(), - 'vector_map_tiles_mbtiles': (context) => + '/flutter_map_cache': (context) => const FlutterMapCachePage(), + '/flutter_map_pmtiles': (context) => const FlutterMapPmTilesPage(), + '/flutter_map_maplibre': (context) => const MapLibreFlutterMapPage(), + '/flutter_map_maplibre2': (context) => const FlutterMapMapLibrePage(), + '/vector_map_tiles_pmtiles': (context) => VectorMapTilesPmTilesPage(), + '/flutter_map_mbtiles': (context) => const FlutterMapMbTilesPage(), + '/vector_map_tiles_mbtiles': (context) => const VectorMapTilesMbTilesPage(), - 'flutter_map_compass': (context) => const FlutterMapCompassPage(), + '/flutter_map_compass': (context) => const FlutterMapCompassPage(), }, ); } @@ -51,33 +55,43 @@ class SelectionPage extends StatelessWidget { title: 'flutter_map_cache', desc: 'A slim yet powerful caching plugin for flutter_map ' 'tile layers.', - routeName: 'flutter_map_cache', + routeName: '/flutter_map_cache', ), SelectionItemWidget( title: 'flutter_map_compass', desc: 'A simple compass layer to indicate the map rotation and ' 'reset the rotation on click', - routeName: 'flutter_map_compass', + routeName: '/flutter_map_compass', + ), + SelectionItemWidget( + title: 'flutter_map_maplibre', + desc: 'Performant vector tiles for flutter_map', + routeName: '/flutter_map_maplibre', + ), + SelectionItemWidget( + title: 'flutter_map_maplibre 2', + desc: 'Use flutter_map layers in maplibre', + routeName: '/flutter_map_maplibre2', ), SelectionItemWidget.disabledOnWeb( title: 'flutter_map_mbtiles', desc: 'MBTiles for flutter_map', - routeName: 'flutter_map_mbtiles', + routeName: '/flutter_map_mbtiles', ), SelectionItemWidget( title: 'flutter_map_pmtiles', desc: 'PMTiles for flutter_map', - routeName: 'flutter_map_pmtiles', + routeName: '/flutter_map_pmtiles', ), SelectionItemWidget.disabledOnWeb( title: 'vector_map_tiles_mbtiles', desc: 'MBTiles for vector_map_files / flutter_map', - routeName: 'vector_map_tiles_mbtiles', + routeName: '/vector_map_tiles_mbtiles', ), SelectionItemWidget.disabledOnWeb( title: 'vector_map_tiles_pmtiles', desc: 'PMTiles for vector_map_files / flutter_map', - routeName: 'vector_map_tiles_pmtiles', + routeName: '/vector_map_tiles_pmtiles', ), ]; @@ -161,7 +175,7 @@ class SelectionItemWidget extends StatelessWidget { ); return Card( - color: disabled ? Colors.white54 : Colors.white, + color: disabled ? Colors.white70 : Colors.white, child: InkWell( onTap: disabled ? null : () => Navigator.of(context).pushNamed(routeName), diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index 4c0025f..0c539c4 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) maplibre_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MaplibrePlugin"); + maplibre_plugin_register_with_registrar(maplibre_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index ad279a8..253e313 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,11 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + maplibre sqlite3_flutter_libs url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 8165d1e..bc840bd 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import connectivity_plus +import maplibre import path_provider_foundation import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + MaplibrePlugin.register(with: registry.registrar(forPlugin: "MaplibrePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c32dc8e..60b4888 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -24,11 +24,14 @@ dependencies: vector_map_tiles: ^8.0.0 vector_tile_renderer: ^5.2.0 mbtiles: ^0.4.0 + maplibre: ^0.1.0 flutter_map_cache: path: ../flutter_map_cache flutter_map_compass: path: ../flutter_map_compass + flutter_map_maplibre: + path: ../flutter_map_maplibre flutter_map_mbtiles: path: ../flutter_map_mbtiles flutter_map_pmtiles: diff --git a/example/web/index.html b/example/web/index.html index 1aa025d..21fdb20 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -1,38 +1,43 @@ - - + This is a placeholder for base href that will be replaced by the value of + the `--base-href` argument provided to `flutter build`. + --> + - - - + + + - - - - - + + + + + - - + + - example - + example + + + + + - + diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 2a9dc10..a184cfb 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + MaplibrePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MaplibrePluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 7565d04..b70ebf8 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,11 +4,13 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus + maplibre sqlite3_flutter_libs url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/flutter_map_maplibre/.gitignore b/flutter_map_maplibre/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/flutter_map_maplibre/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/flutter_map_maplibre/.metadata b/flutter_map_maplibre/.metadata new file mode 100644 index 0000000..a567cc4 --- /dev/null +++ b/flutter_map_maplibre/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "603104015dd692ea3403755b55d07813d5cf8965" + channel: "stable" + +project_type: package diff --git a/flutter_map_maplibre/CHANGELOG.md b/flutter_map_maplibre/CHANGELOG.md new file mode 100644 index 0000000..8068a3f --- /dev/null +++ b/flutter_map_maplibre/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.0.1 + +- Add `FlutterMapAdapter` +- Add `MapLibreLayer` +- Initial release \ No newline at end of file diff --git a/flutter_map_maplibre/LICENSE b/flutter_map_maplibre/LICENSE new file mode 100644 index 0000000..6370cf7 --- /dev/null +++ b/flutter_map_maplibre/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Joscha Eckert (josxha) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_map_maplibre/README.md b/flutter_map_maplibre/README.md new file mode 100644 index 0000000..8e4126e --- /dev/null +++ b/flutter_map_maplibre/README.md @@ -0,0 +1,104 @@ +# flutter_map_maplibre + +This package offers performant Mapbox Vector Tiles (MVT) support +for [flutter_map](https://pub.dev/packages/flutter_map) +powered by native [MapLibre SDKs](https://maplibre.org). + +## Getting started + +Add the dependencies to your `pubspec.yaml` file. + +```yaml +dependencies: + flutter_map: ^7.0.0 + flutter_map_maplibre: ^0.0.1 + maplibre: ^0.1.0 +``` + +## Usage + +There are two ways how to use bridge the gap between `flutter_map` +and `maplibre`: + +- Add a MapLibre map as a layer to a FlutterMap map +- or use FlutterMap layers in a MapLibre map + +Each solution has its pro and cons. You can check out both implementations in +the [hosted example app](https://flutter-map-plugins.web.app/). + +#### Use MapLibre as a vector layer in FlutterMap + +Here we add a `MapLibreMap` as a layer to `FlutterMap` and let flutter_map +handle all gesture inputs. + +```dart +@override +Widget build(BuildContext context) { + return FlutterMap( + mapController: _mapController, + options: const MapOptions( + initialZoom: 4, + initialCenter: LatLng(0, 0), + maxZoom: 20, + // maplibre does not support an unbound latitude + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ), + ), + children: [ + const MapLibreLayer( + initStyle: 'insert your style url here', + ), + // add other flutter_map layers here + ], + ); +} +``` + +#### Use FlutterMap layers inside MapLibre + +In this solution we use `FlutterMap` layers in `MapLibreMap`. +All gesture inputs are handled by maplibre. + +```dart +@override +Widget build(BuildContext context) { + return MapLibreMap( + onMapCreated: (controller) => _controller = controller, + options: MapOptions( + initCenter: Position(0, 0), + initZoom: 3, + maxPitch: 0, // flutter_map doesn't support camera pitch, disable it here + initStyle: 'insert your style url here', + ), + children: [ + // wrap each of your flutter_map layer with a FlutterMapAdapter widget + const FlutterMapAdapter( + child: MarkerLayer( + markers: [ + Marker( + point: LatLng(15, 5), + width: 40, + height: 40, + child: Icon(Icons.location_on, color: Colors.red, size: 40), + alignment: Alignment.topCenter, + ), + ], + ), + ), + ], + ); +} +``` + +## Additional information + +Pull requests or bug reports are welcome. + +If you need help you +can [open an issue](https://github.com/josxha/flutter_map_plugins/issues/new/choose) +or join +the [`flutter_map` discord server](https://discord.gg/BwpEsjqMAH). diff --git a/flutter_map_maplibre/analysis_options.yaml b/flutter_map_maplibre/analysis_options.yaml new file mode 100644 index 0000000..0caccc8 --- /dev/null +++ b/flutter_map_maplibre/analysis_options.yaml @@ -0,0 +1 @@ +include: ../analysis_options.yaml \ No newline at end of file diff --git a/flutter_map_maplibre/example/example.md b/flutter_map_maplibre/example/example.md new file mode 100644 index 0000000..4e7e971 --- /dev/null +++ b/flutter_map_maplibre/example/example.md @@ -0,0 +1,6 @@ +- Read + the [README.md](https://github.com/josxha/flutter_map_plugins/blob/main/flutter_map_maplibre/README.md) +- Check out + the [combined example app](https://github.com/josxha/flutter_map_plugins/tree/main/example) + that showcases this and other flutter_map + packages. \ No newline at end of file diff --git a/flutter_map_maplibre/lib/flutter_map_maplibre.dart b/flutter_map_maplibre/lib/flutter_map_maplibre.dart new file mode 100644 index 0000000..9f145e8 --- /dev/null +++ b/flutter_map_maplibre/lib/flutter_map_maplibre.dart @@ -0,0 +1,3 @@ +export 'src/extensions.dart'; +export 'src/flutter_map_adapter.dart'; +export 'src/maplibre_layer.dart'; diff --git a/flutter_map_maplibre/lib/src/extensions.dart b/flutter_map_maplibre/lib/src/extensions.dart new file mode 100644 index 0000000..cc1a80b --- /dev/null +++ b/flutter_map_maplibre/lib/src/extensions.dart @@ -0,0 +1,41 @@ +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_map/flutter_map.dart' as fm; +import 'package:latlong2/latlong.dart'; +import 'package:maplibre/maplibre.dart'; + +/// Extension methods on [Position]; +extension PositionExt on Position { + /// Convert [Position] to [LatLng]. + LatLng toLatLng() => LatLng(lat.toDouble(), lng.toDouble()); +} + +/// Extension methods on [LatLng]; +extension LatLngExt on LatLng { + /// Convert [LatLng] to [Position]. + Position toPosition() => Position(longitude, latitude); +} + +/// Extension methods on [MapController]; +extension MapControllerExt on MapController { + /// Convert [MapController] to [fm.MapController]. + fm.MapController toFlutterMapController() => fm.MapController(); +} + +/// Extension methods on [MapCamera]; +extension MapCameraExt on MapCamera { + /// Convert [MapCamera] to [fm.MapCamera]. + fm.MapCamera toFlutterMapCamera(BoxConstraints constraints) { + return fm.MapCamera( + zoom: zoom + 1, + center: center.toLatLng(), + rotation: -bearing, + crs: const fm.Epsg3857(), + nonRotatedSize: math.Point( + constraints.maxWidth, + constraints.maxHeight, + ), + ); + } +} diff --git a/flutter_map_maplibre/lib/src/flutter_map_adapter.dart b/flutter_map_maplibre/lib/src/flutter_map_adapter.dart new file mode 100644 index 0000000..4cb37d9 --- /dev/null +++ b/flutter_map_maplibre/lib/src/flutter_map_adapter.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart' as fm; +// ignore: implementation_imports +import 'package:flutter_map/src/map/inherited_model.dart' as fm; +import 'package:flutter_map_maplibre/flutter_map_maplibre.dart'; +import 'package:maplibre/maplibre.dart'; + +/// Wrap a flutter_map layer with a [FlutterMapAdapter] to use it as a layer +/// in the [MapLibreMap.children] list as a layer. +class FlutterMapAdapter extends StatelessWidget { + /// Create a new [FlutterMapAdapter] to use flutter_map layers inside of + /// [MapLibreMap.children]. + const FlutterMapAdapter({required this.child, super.key}); + + /// The flutter_map layer widget. + final Widget child; + + @override + Widget build(BuildContext context) { + final controller = MapController.maybeOf(context); + final camera = MapCamera.maybeOf(context); + if (controller == null || camera == null) return const SizedBox.shrink(); + + return RepaintBoundary( + child: LayoutBuilder( + builder: (context, constraints) { + return fm.MapInheritedModel( + options: const fm.MapOptions( + interactionOptions: fm.InteractionOptions( + flags: fm.InteractiveFlag.none, + ), + ), + controller: controller.toFlutterMapController(), + camera: camera.toFlutterMapCamera(constraints), + child: child, + ); + }, + ), + ); + } +} diff --git a/flutter_map_maplibre/lib/src/maplibre_layer.dart b/flutter_map_maplibre/lib/src/maplibre_layer.dart new file mode 100644 index 0000000..1f38619 --- /dev/null +++ b/flutter_map_maplibre/lib/src/maplibre_layer.dart @@ -0,0 +1,66 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart' as fm; +import 'package:flutter_map_maplibre/src/extensions.dart'; +import 'package:maplibre/maplibre.dart'; + +/// Use the [MapLibreLayer] to embedd [MapLibreMap] as a layer in +/// [fm.FlutterMap.children]. +class MapLibreLayer extends StatefulWidget { + /// Create a new [MapLibreLayer] to use it as a layer in + /// [fm.FlutterMap.children]. + const MapLibreLayer({ + this.children = const [], + this.initStyle = 'https://demotiles.maplibre.org/style.json', + this.onEvent, + super.key, + }); + + /// The [MapLibreMap.children] list. + final List children; + + /// The [MapLibreMap.onEvent] callback. + final MapEventCallback? onEvent; + + /// The style URL that should get used. If not set, the default MapLibre style + /// is used (https://demotiles.maplibre.org/style.json). + final String initStyle; + + @override + State createState() => _MapLibreLayerState(); +} + +class _MapLibreLayerState extends State { + MapController? _controller; + + @override + Widget build(BuildContext context) { + final fmCamera = fm.MapCamera.of(context); + // final fmController = fm.MapController.of(context); + // final fmOptions = fm.MapOptions.of(context); + + // sync the FlutterMap movement with MapLibreMap + _controller?.moveCamera( + center: fmCamera.center.toPosition(), + zoom: fmCamera.zoom - 1, + bearing: -fmCamera.rotation, + ); + + return MapLibreMap( + options: MapOptions( + initCenter: fmCamera.center.toPosition(), + initBearing: -fmCamera.rotation, + initZoom: fmCamera.zoom - 1, + initStyle: widget.initStyle, + attribution: false, + maxPitch: 0, + nativeCompass: false, + nativeLogo: false, + gestures: const MapGestures.none(), + ), + gestureRecognizers: const {}, + onEvent: widget.onEvent, + onMapCreated: (controller) => _controller = controller, + children: widget.children, + ); + } +} diff --git a/flutter_map_maplibre/pubspec.yaml b/flutter_map_maplibre/pubspec.yaml new file mode 100644 index 0000000..3ea04ae --- /dev/null +++ b/flutter_map_maplibre/pubspec.yaml @@ -0,0 +1,21 @@ +name: flutter_map_maplibre +description: "Performant Mapbox Vector Tiles (MVT) support for flutter_map powered by native MapLibre SDKs." +version: 0.0.1 +repository: https://github.com/josxha/flutter_map_plugins +issue_tracker: https://github.com/josxha/flutter_map_plugins/issues + +environment: + sdk: ^3.5.4 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_map: ^7.0.0 + latlong2: ^0.9.0 + maplibre: ^0.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^6.0.0