From 3c94741da3712e95a07e10c620351c92fcda321c Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Mon, 22 Jul 2024 23:17:20 +0800 Subject: [PATCH] Add OSM backend for map browser --- app/lib/widget/map_browser/view.dart | 27 +++++ app/pubspec.lock | 24 ++++ np_gps_map/lib/src/interactive_map.dart | 13 +- np_gps_map/lib/src/interactive_map/osm.dart | 125 ++++++++++++++++++++ np_gps_map/pubspec.yaml | 1 + 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 np_gps_map/lib/src/interactive_map/osm.dart diff --git a/app/lib/widget/map_browser/view.dart b/app/lib/widget/map_browser/view.dart index ec70b892..10854903 100644 --- a/app/lib/widget/map_browser/view.dart +++ b/app/lib/widget/map_browser/view.dart @@ -49,6 +49,9 @@ class _MapViewState extends State<_MapView> { }, googleClusterBuilder: (context, dataPoints) => _GoogleMarkerBuilder(context).build(dataPoints), + osmClusterBuilder: (context, dataPoints) => _OsmMarker( + count: dataPoints.length, + ), contentPadding: EdgeInsets.only( top: MediaQuery.of(context).padding.top, bottom: MediaQuery.of(context).padding.bottom, @@ -68,6 +71,30 @@ class _MapViewState extends State<_MapView> { InteractiveMapController? _controller; } +class _OsmMarker extends StatelessWidget { + const _OsmMarker({ + required this.count, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primary, + ), + child: Center( + child: Text( + count.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ); + } + + final int count; +} + class _PanelContainer extends StatefulWidget { const _PanelContainer({ required this.isShow, diff --git a/app/pubspec.lock b/app/pubspec.lock index 90cbb32c..821469f9 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -26,6 +26,14 @@ packages: url: "https://gitlab.com/nc-photos/plus_plugins" source: git version: "3.1.1" + animated_stack_widget: + dependency: transitive + description: + name: animated_stack_widget + sha256: ce4788dd158768c9d4388354b6fb72600b78e041a37afc4c279c63ecafcb9408 + url: "https://pub.dev" + source: hosted + version: "0.0.4" args: dependency: transitive description: @@ -545,6 +553,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + flutter_map_marker_cluster: + dependency: transitive + description: + name: flutter_map_marker_cluster + sha256: a324f48da5ee83a3f29fd8d08b4b1e6e3114ff5c6cab910124d6a2e1f06f08cc + url: "https://pub.dev" + source: hosted + version: "1.3.6" + flutter_map_marker_popup: + dependency: transitive + description: + name: flutter_map_marker_popup + sha256: ec563bcbae24a18ac16815fb75ac5ab33ccba609e14db70e252a67de19c6639c + url: "https://pub.dev" + source: hosted + version: "6.1.2" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/np_gps_map/lib/src/interactive_map.dart b/np_gps_map/lib/src/interactive_map.dart index d5fc75ac..d44ed04e 100644 --- a/np_gps_map/lib/src/interactive_map.dart +++ b/np_gps_map/lib/src/interactive_map.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:np_gps_map/src/gps_map.dart'; import 'package:np_gps_map/src/interactive_map/google.dart'; +import 'package:np_gps_map/src/interactive_map/osm.dart'; import 'package:np_gps_map/src/map_coord.dart'; import 'package:np_gps_map/src/util.dart'; import 'package:np_platform_util/np_platform_util.dart'; @@ -26,6 +27,7 @@ class InteractiveMap extends StatelessWidget { this.initialZoom, this.dataPoints, this.onClusterTap, + this.osmClusterBuilder, this.googleClusterBuilder, this.contentPadding, this.onMapCreated, @@ -35,7 +37,15 @@ class InteractiveMap extends StatelessWidget { Widget build(BuildContext context) { if (providerHint == GpsMapProvider.osm || (getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) { - return const SizedBox.shrink(); + return OsmInteractiveMap( + initialPosition: initialPosition, + initialZoom: initialZoom, + dataPoints: dataPoints, + onClusterTap: onClusterTap, + clusterBuilder: osmClusterBuilder, + contentPadding: contentPadding, + onMapCreated: onMapCreated, + ); } else { return GoogleInteractiveMap( initialPosition: initialPosition, @@ -57,6 +67,7 @@ class InteractiveMap extends StatelessWidget { final List? dataPoints; final void Function(List dataPoints)? onClusterTap; final GoogleClusterBuilder? googleClusterBuilder; + final OsmClusterBuilder? osmClusterBuilder; final EdgeInsets? contentPadding; final void Function(InteractiveMapController controller)? onMapCreated; } diff --git a/np_gps_map/lib/src/interactive_map/osm.dart b/np_gps_map/lib/src/interactive_map/osm.dart new file mode 100644 index 00000000..b7b0ca43 --- /dev/null +++ b/np_gps_map/lib/src/interactive_map/osm.dart @@ -0,0 +1,125 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:np_common/object_util.dart'; +import 'package:np_gps_map/src/interactive_map.dart'; +import 'package:np_gps_map/src/map_coord.dart'; + +typedef OsmClusterBuilder = Widget Function( + BuildContext context, List dataPoints); + +class OsmInteractiveMap extends StatefulWidget { + const OsmInteractiveMap({ + super.key, + this.initialPosition, + this.initialZoom, + this.dataPoints, + this.clusterBuilder, + this.onClusterTap, + this.contentPadding, + this.onMapCreated, + }); + + @override + State createState() => _OsmInteractiveMapState(); + + final MapCoord? initialPosition; + final double? initialZoom; + final List? dataPoints; + final OsmClusterBuilder? clusterBuilder; + final void Function(List dataPoints)? onClusterTap; + final EdgeInsets? contentPadding; + final void Function(InteractiveMapController controller)? onMapCreated; +} + +class _OsmInteractiveMapState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_parentController == null) { + _parentController = _ParentController(_controller); + widget.onMapCreated?.call(_parentController!); + } + }); + } + + @override + Widget build(BuildContext context) { + return FlutterMap( + mapController: _controller, + options: MapOptions( + initialCenter: widget.initialPosition?.toLatLng() ?? const LatLng(0, 0), + initialZoom: max(2.5, widget.initialZoom ?? 2.5), + minZoom: 2.5, + ), + children: [ + TileLayer( + urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + ), + if (widget.dataPoints != null) + MarkerClusterLayerWidget( + options: MarkerClusterLayerOptions( + markers: widget.dataPoints! + .map((e) => _OsmDataPoint( + original: e, + child: _buildMarker(context, [e]), + )) + .toList(), + builder: (context, markers) => _buildMarker( + context, + markers.cast<_OsmDataPoint>().map((e) => e.original).toList(), + ), + ), + ), + Padding( + padding: widget.contentPadding ?? EdgeInsets.zero, + child: const SimpleAttributionWidget( + source: Text("OpenStreetMap contributors"), + ), + ), + ], + ); + } + + Widget _buildMarker(BuildContext context, List dataPoints) { + if (widget.clusterBuilder == null) { + return const SizedBox.shrink(); + } else { + return GestureDetector( + onTap: widget.onClusterTap?.let((l) => () => l(dataPoints)), + child: widget.clusterBuilder!(context, dataPoints), + ); + } + } + + _ParentController? _parentController; + late final _controller = MapController(); +} + +class _OsmDataPoint extends Marker { + _OsmDataPoint({ + required this.original, + required super.child, + }) : super(point: original.position.toLatLng()); + + final DataPoint original; +} + +class _ParentController implements InteractiveMapController { + const _ParentController(this.controller); + + @override + void setPosition(MapCoord position) { + controller.move(position.toLatLng(), 10); + } + + final MapController controller; +} + +extension on MapCoord { + LatLng toLatLng() => LatLng(latitude, longitude); +} diff --git a/np_gps_map/pubspec.yaml b/np_gps_map/pubspec.yaml index 3cfcfec5..000a7cb5 100644 --- a/np_gps_map/pubspec.yaml +++ b/np_gps_map/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: flutter: sdk: flutter flutter_map: ^6.1.0 + flutter_map_marker_cluster: ^1.3.6 google_maps_flutter: 2.5.3 google_maps_cluster_manager: 3.1.0 latlong2: any