diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ac1586d..87852bd 100755 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -35,7 +35,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.flutter_map_supercluster_example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/example/lib/basic_example.dart b/example/lib/basic_example.dart new file mode 100644 index 0000000..d085fc2 --- /dev/null +++ b/example/lib/basic_example.dart @@ -0,0 +1,69 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; +import 'package:flutter_map_supercluster_example/drawer.dart'; +import 'package:flutter_map_supercluster_example/main.dart'; +import 'package:latlong2/latlong.dart'; + +class BasicExamplePage extends StatelessWidget { + static const String route = 'basicExamplePage'; + + static const _totalMarkers = 3000; + static final _random = Random(42); + static const _initialCenter = LatLng(42.0, 10.0); + static final _markers = List.generate( + _totalMarkers, + (_) => Marker( + point: LatLng( + _random.nextDouble() * 3 - 1.5 + _initialCenter.latitude, + _random.nextDouble() * 3 - 1.5 + _initialCenter.longitude, + ), + child: const Icon(Icons.location_on), + ), + ); + + const BasicExamplePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Basic Example')), + drawer: buildDrawer(context, route), + body: FlutterMap( + options: const MapOptions( + initialCenter: _initialCenter, + initialZoom: 8, + maxZoom: 19, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: tileLayerPackageName, + ), + SuperclusterLayer.immutable( + initialMarkers: _markers, + indexBuilder: IndexBuilders.computeWithOriginalMarkers, + clusterWidgetSize: const Size(40, 40), + maxClusterRadius: 120, + clusterAlignment: Alignment.center, + builder: (context, position, markerCount, extraClusterData) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: Colors.blue), + child: Center( + child: Text( + markerCount.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/example/lib/too_close_to_uncluster_page.dart b/example/lib/cluster_splaying_page.dart similarity index 69% rename from example/lib/too_close_to_uncluster_page.dart rename to example/lib/cluster_splaying_page.dart index e9d3e6d..3ceebd2 100644 --- a/example/lib/too_close_to_uncluster_page.dart +++ b/example/lib/cluster_splaying_page.dart @@ -1,41 +1,52 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; import 'package:flutter_map_supercluster_example/drawer.dart'; +import 'package:flutter_map_supercluster_example/main.dart'; import 'package:latlong2/latlong.dart'; -class TooCloseToUnclusterPage extends StatefulWidget { - static const String route = 'tooCloseToUnclusterPage'; +class ClusterSplayingPage extends StatefulWidget { + static const String route = 'clusterSplayingPage'; - const TooCloseToUnclusterPage({Key? key}) : super(key: key); + const ClusterSplayingPage({super.key}); @override - State createState() => - _TooCloseToUnclusterPageState(); + State createState() => _ClusterSplayingPageState(); } -class _TooCloseToUnclusterPageState extends State +class _ClusterSplayingPageState extends State with TickerProviderStateMixin { late final SuperclusterImmutableController _superclusterController; late final AnimatedMapController _animatedMapController; - static final points = [ - const LatLng(51.4001, -0.08001), - const LatLng(51.4003, -0.08003), - const LatLng(51.4005, -0.08005), - const LatLng(51.4006, -0.08006), - const LatLng(51.4009, -0.08009), - const LatLng(51.5, -0.09), - const LatLng(51.5, -0.09), - const LatLng(51.5, -0.09), - const LatLng(51.5, -0.09), - const LatLng(51.5, -0.09), - const LatLng(51.59, -0.099), + static const points = [ + LatLng(51.4001, -0.08001), + LatLng(51.4003, -0.08003), + LatLng(51.4005, -0.08005), + LatLng(51.4006, -0.08006), + LatLng(51.4009, -0.08009), + LatLng(51.5, -0.09), + LatLng(51.5, -0.09), + LatLng(51.5, -0.09), + LatLng(51.5, -0.09), + LatLng(51.5, -0.09), + LatLng(51.59, -0.099), ]; - late List markers; + static final List markers = points + .map( + (point) => Marker( + alignment: Alignment.topCenter, + height: 30, + width: 30, + point: point, + rotate: true, + child: const Icon(Icons.pin_drop), + ), + ) + .toList(); @override void initState() { @@ -43,20 +54,6 @@ class _TooCloseToUnclusterPageState extends State _superclusterController = SuperclusterImmutableController(); _animatedMapController = AnimatedMapController(vsync: this); - - markers = points - .map( - (point) => Marker( - anchorPos: AnchorPos.align(AnchorAlign.top), - rotateAlignment: AnchorAlign.top.rotationAlignment, - height: 30, - width: 30, - point: point, - rotate: true, - builder: (ctx) => const Icon(Icons.pin_drop), - ), - ) - .toList(); } @override @@ -70,7 +67,7 @@ class _TooCloseToUnclusterPageState extends State Widget build(BuildContext context) { return PopupScope( child: Scaffold( - appBar: AppBar(title: const Text('Too close to uncluster page')), + appBar: AppBar(title: const Text('Cluster Splaying')), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -87,21 +84,21 @@ class _TooCloseToUnclusterPageState extends State ), ], ), - drawer: buildDrawer(context, TooCloseToUnclusterPage.route), + drawer: buildDrawer(context, ClusterSplayingPage.route), body: FlutterMap( mapController: _animatedMapController.mapController, options: MapOptions( - center: const LatLng(51.4931, -0.1003), - zoom: 10, - maxZoom: 15, + initialCenter: const LatLng(51.4931, -0.1003), + initialZoom: 10, + maxZoom: 16, onTap: (_, __) { _superclusterController.collapseSplayedClusters(); }, ), children: [ TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: tileLayerPackageName, ), SuperclusterLayer.immutable( initialMarkers: markers, @@ -112,7 +109,7 @@ class _TooCloseToUnclusterPageState extends State zoom: zoom, ), clusterWidgetSize: const Size(40, 40), - anchor: AnchorPos.align(AnchorAlign.center), + clusterAlignment: Alignment.center, popupOptions: PopupOptions( selectedMarkerBuilder: (context, marker) => const Icon( Icons.pin_drop, diff --git a/example/lib/custom_cluster_marker_page.dart b/example/lib/custom_cluster_marker_page.dart index 8e52242..ed20b94 100644 --- a/example/lib/custom_cluster_marker_page.dart +++ b/example/lib/custom_cluster_marker_page.dart @@ -1,15 +1,16 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; import 'package:flutter_map_supercluster_example/drawer.dart'; +import 'package:flutter_map_supercluster_example/main.dart'; import 'package:latlong2/latlong.dart'; class CustomClusterMarkerPage extends StatelessWidget { static const String route = 'customClusterMarkerPage'; - const CustomClusterMarkerPage({Key? key}) : super(key: key); + const CustomClusterMarkerPage({super.key}); // Initialise randomly generated Markers static final _random = Random(42); @@ -17,7 +18,6 @@ class CustomClusterMarkerPage extends StatelessWidget { static final _markers = List.generate( 3000, (_) => CustomMarker( - builder: (context) => const Icon(Icons.location_on), point: LatLng( _random.nextDouble() * 3 - 1.5 + _initialCenter.latitude, _random.nextDouble() * 3 - 1.5 + _initialCenter.longitude, @@ -25,6 +25,7 @@ class CustomClusterMarkerPage extends StatelessWidget { greenCount: _random.nextInt(10), blueCount: _random.nextInt(10), purpleCount: _random.nextInt(10), + child: const Icon(Icons.location_on), ), ); @@ -34,13 +35,14 @@ class CustomClusterMarkerPage extends StatelessWidget { appBar: AppBar(title: const Text('Custom Cluster Marker')), drawer: buildDrawer(context, CustomClusterMarkerPage.route), body: FlutterMap( - options: MapOptions( - center: _initialCenter, - zoom: 8, + options: const MapOptions( + initialCenter: _initialCenter, + initialZoom: 8, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: tileLayerPackageName, ), SuperclusterLayer.immutable( // Replaces MarkerLayer @@ -91,9 +93,9 @@ class CustomMarker extends Marker { final int blueCount; final int purpleCount; - CustomMarker({ + const CustomMarker({ required super.point, - required super.builder, + required super.child, required this.greenCount, required this.blueCount, required this.purpleCount, diff --git a/example/lib/drawer.dart b/example/lib/drawer.dart index 530f189..5f86479 100644 --- a/example/lib/drawer.dart +++ b/example/lib/drawer.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map_supercluster_example/cluster_splaying_page.dart'; import 'package:flutter_map_supercluster_example/custom_cluster_marker_page.dart'; -import 'package:flutter_map_supercluster_example/immutable_clustering_page.dart'; -import 'package:flutter_map_supercluster_example/mutable_clustering_page.dart'; import 'package:flutter_map_supercluster_example/normal_and_clustered_markers_with_popups_page.dart'; -import 'package:flutter_map_supercluster_example/too_close_to_uncluster_page.dart'; + +import 'basic_example.dart'; +import 'mutable_clustering_page.dart'; Widget _buildMenuItem( BuildContext context, Widget title, String routeName, String currentRoute) { @@ -33,26 +34,26 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { ), _buildMenuItem( context, - const Text('Clustering (mutable)'), - MutableClusteringPage.route, + const Text('Basic Example'), + BasicExamplePage.route, currentRoute, ), _buildMenuItem( context, - const Text('Clustering Many Markers (Immutable)'), - ClusteringManyMarkersPage.route, + const Text('Mutable Clustering'), + MutableClustersPage.route, currentRoute, ), _buildMenuItem( context, - const Text('Too close to uncluster'), - TooCloseToUnclusterPage.route, + const Text('Cluster Splaying'), + ClusterSplayingPage.route, currentRoute, ), _buildMenuItem( context, const Text('Normal and Clustered Markers With Popups'), - NormalAndClusteredMarkersWithPopups.route, + NormalAndClusteredMarkersWithPopupsPage.route, currentRoute, ), _buildMenuItem( diff --git a/example/lib/font/accurate_map_icons.dart b/example/lib/font/accurate_map_icons.dart index a8fad44..c20d788 100644 --- a/example/lib/font/accurate_map_icons.dart +++ b/example/lib/font/accurate_map_icons.dart @@ -1,18 +1,3 @@ -/// Flutter icons AccurateMapIcon -/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com -/// This font was generated by FlutterIcon.com, which is derived from Fontello. -/// -/// To use this font, place it in your fonts/ directory and include the -/// following in your pubspec.yaml -/// -/// flutter: -/// fonts: -/// - family: AccurateMapIcon -/// fonts: -/// - asset: fonts/AccurateMapIcon.ttf -/// -/// -/// import 'package:flutter/widgets.dart'; class AccurateMapIcons { diff --git a/example/lib/immutable_clustering_page.dart b/example/lib/immutable_clustering_page.dart deleted file mode 100644 index 828bbf1..0000000 --- a/example/lib/immutable_clustering_page.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_animations/flutter_map_animations.dart'; -import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; -import 'package:flutter_map_supercluster_example/drawer.dart'; -import 'package:latlong2/latlong.dart'; - -class ClusteringManyMarkersPage extends StatefulWidget { - static const String route = 'clusteringManyMarkersPage'; - - const ClusteringManyMarkersPage({Key? key}) : super(key: key); - - @override - State createState() => - _ClusteringManyMarkersPageState(); -} - -class _ClusteringManyMarkersPageState extends State - with TickerProviderStateMixin { - static const totalMarkers = 2000.0; - final minLatLng = const LatLng(49.8566, 1.3522); - final maxLatLng = const LatLng(58.3498, -10.2603); - - late final SuperclusterImmutableController _superclusterController; - late final AnimatedMapController _animatedMapController; - - late final List markers; - - @override - void initState() { - _superclusterController = SuperclusterImmutableController(); - _animatedMapController = AnimatedMapController(vsync: this); - - final latitudeRange = maxLatLng.latitude - minLatLng.latitude; - final longitudeRange = maxLatLng.longitude - minLatLng.longitude; - - final stepsInEachDirection = sqrt(totalMarkers).floor(); - final latStep = latitudeRange / stepsInEachDirection; - final lonStep = longitudeRange / stepsInEachDirection; - - markers = []; - for (var i = 0; i < stepsInEachDirection; i++) { - for (var j = 0; j < stepsInEachDirection; j++) { - final latLng = LatLng( - minLatLng.latitude + i * latStep, - minLatLng.longitude + j * lonStep, - ); - - markers.add( - Marker( - anchorPos: AnchorPos.align(AnchorAlign.top), - rotateAlignment: AnchorAlign.top.rotationAlignment, - height: 30, - width: 30, - point: latLng, - builder: (ctx) => const Icon(Icons.pin_drop), - ), - ); - } - } - - super.initState(); - } - - @override - void dispose() { - _superclusterController.dispose(); - _animatedMapController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Clustering Many Markers Page'), - actions: [ - StreamBuilder( - stream: _superclusterController.stateStream, - builder: (context, snapshot) { - final data = snapshot.data; - final String markerCountLabel; - if (data == null || - data.loading || - data.aggregatedClusterData == null) { - markerCountLabel = '...'; - } else { - markerCountLabel = - data.aggregatedClusterData!.markerCount.toString(); - } - - return Center( - child: Padding( - padding: const EdgeInsets.only(right: 20), - child: Text('Total markers: $markerCountLabel'), - ), - ); - }), - ], - ), - drawer: buildDrawer(context, ClusteringManyMarkersPage.route), - body: FlutterMap( - mapController: _animatedMapController.mapController, - options: MapOptions( - center: LatLng((maxLatLng.latitude + minLatLng.latitude) / 2, - (maxLatLng.longitude + minLatLng.longitude) / 2), - zoom: 6, - maxZoom: 15, - ), - nonRotatedChildren: [ - Builder( - builder: (context) => - Text(FlutterMapState.maybeOf(context)!.zoom.toString()), - ) - ], - children: [ - TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], - ), - SuperclusterLayer.immutable( - initialMarkers: markers, - indexBuilder: IndexBuilders.computeWithOriginalMarkers, - controller: _superclusterController, - moveMap: (center, zoom) => _animatedMapController.animateTo( - dest: center, - zoom: zoom, - ), - calculateAggregatedClusterData: true, - clusterWidgetSize: const Size(40, 40), - anchor: AnchorPos.align(AnchorAlign.center), - popupOptions: PopupOptions( - selectedMarkerBuilder: (context, marker) => const Icon( - Icons.pin_drop, - color: Colors.red, - ), - popupDisplayOptions: PopupDisplayOptions( - builder: (BuildContext context, Marker marker) => Container( - color: Colors.white, - child: Text(marker.point.toString()), - ), - ), - ), - builder: (context, position, markerCount, extraClusterData) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.0), - color: Colors.blue), - child: Center( - child: Text( - markerCount.toString(), - style: const TextStyle(color: Colors.white), - ), - ), - ); - }, - ), - ], - ), - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a02a2f..090b7ba 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map_supercluster_example/cluster_splaying_page.dart'; import 'package:flutter_map_supercluster_example/custom_cluster_marker_page.dart'; -import 'package:flutter_map_supercluster_example/immutable_clustering_page.dart'; -import 'package:flutter_map_supercluster_example/mutable_clustering_page.dart'; import 'package:flutter_map_supercluster_example/normal_and_clustered_markers_with_popups_page.dart'; -import 'package:flutter_map_supercluster_example/too_close_to_uncluster_page.dart'; + +import 'basic_example.dart'; +import 'mutable_clustering_page.dart'; void main() => runApp(const MyApp()); +const tileLayerPackageName = 'ng.balanci.flutter_map_supercluster.example'; + class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -17,15 +20,13 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: const MutableClusteringPage(), + home: const BasicExamplePage(), routes: { - MutableClusteringPage.route: (context) => const MutableClusteringPage(), - ClusteringManyMarkersPage.route: (context) => - const ClusteringManyMarkersPage(), - TooCloseToUnclusterPage.route: (context) => - const TooCloseToUnclusterPage(), - NormalAndClusteredMarkersWithPopups.route: (context) => - const NormalAndClusteredMarkersWithPopups(), + BasicExamplePage.route: (context) => const BasicExamplePage(), + MutableClustersPage.route: (context) => const MutableClustersPage(), + ClusterSplayingPage.route: (context) => const ClusterSplayingPage(), + NormalAndClusteredMarkersWithPopupsPage.route: (context) => + const NormalAndClusteredMarkersWithPopupsPage(), CustomClusterMarkerPage.route: (context) => const CustomClusterMarkerPage(), }, diff --git a/example/lib/mutable_clustering_page.dart b/example/lib/mutable_clustering_page.dart index f7da667..cc76db9 100644 --- a/example/lib/mutable_clustering_page.dart +++ b/example/lib/mutable_clustering_page.dart @@ -1,21 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; import 'package:flutter_map_supercluster_example/drawer.dart'; import 'package:flutter_map_supercluster_example/font/accurate_map_icons.dart'; +import 'package:flutter_map_supercluster_example/main.dart'; import 'package:latlong2/latlong.dart'; -class MutableClusteringPage extends StatefulWidget { - static const String route = 'mutableClusteringPage'; +class MutableClustersPage extends StatefulWidget { + static const String route = 'mutableClustersPage'; - const MutableClusteringPage({Key? key}) : super(key: key); + const MutableClustersPage({super.key}); @override - State createState() => _MutableClusteringPageState(); + State createState() => _MutableClustersPageState(); } -class _MutableClusteringPageState extends State +class _MutableClustersPageState extends State with TickerProviderStateMixin { late final SuperclusterMutableController _superclusterController; late final AnimatedMapController _animatedMapController; @@ -43,100 +44,147 @@ class _MutableClusteringPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Clustering Page (Mutable)'), - actions: [ - StreamBuilder( - stream: _superclusterController.stateStream, - builder: (context, snapshot) { - final data = snapshot.data; - final String markerCountLabel; - if (data == null || - data.loading || - data.aggregatedClusterData == null) { - markerCountLabel = '...'; - } else { - markerCountLabel = - data.aggregatedClusterData!.markerCount.toString(); - } + return SuperclusterScope( + child: Scaffold( + appBar: AppBar( + title: const Text('Mutable Clusters'), + actions: [ + Builder(builder: (context) { + final data = SuperclusterState.of(context); + final String markerCountLabel; + if (data.loading) { + markerCountLabel = '...'; + } else { + markerCountLabel = + (data.aggregatedClusterData?.markerCount ?? 0).toString(); + } - return Center( - child: Padding( - padding: const EdgeInsets.only(right: 20), - child: Text('Total markers: $markerCountLabel'), - ), - ); - }), - ], - ), - drawer: buildDrawer(context, MutableClusteringPage.route), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - _superclusterController.replaceAll(_initialMarkers); - }); - }, - child: const Icon(Icons.refresh), - ), - body: FlutterMap( - mapController: _animatedMapController.mapController, - options: MapOptions( - center: _initialMarkers[0].point, - zoom: 5, - maxZoom: 15, - onTap: (_, latLng) { - debugPrint(latLng.toString()); - _superclusterController.add(_createMarker(latLng, Colors.blue)); - }, + return Center( + child: Padding( + padding: const EdgeInsets.only(right: 20), + child: Text('Total:\n$markerCountLabel'), + ), + ); + }), + ], ), - children: [ - TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], - ), - SuperclusterLayer.mutable( - initialMarkers: _initialMarkers, - indexBuilder: IndexBuilders.rootIsolate, - controller: _superclusterController, - moveMap: (center, zoom) => _animatedMapController.animateTo( - dest: center, - zoom: zoom, + drawer: buildDrawer(context, MutableClustersPage.route), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: 'clear', + onPressed: () { + setState(() { + _superclusterController.clear(); + }); + }, + child: const Icon(Icons.clear_all), + ), + const SizedBox(height: 12), + FloatingActionButton( + heroTag: 'reset', + onPressed: () { + setState(() { + _superclusterController.replaceAll(_initialMarkers); + }); + }, + child: const Icon(Icons.refresh), ), - onMarkerTap: (marker) { - _superclusterController.remove(marker); + ], + ), + body: FlutterMap( + mapController: _animatedMapController.mapController, + options: MapOptions( + initialCenter: _initialMarkers[0].point, + initialZoom: 5, + maxZoom: 15, + onTap: (_, latLng) { + debugPrint(latLng.toString()); + _superclusterController.add(_createMarker(latLng, Colors.blue)); }, - clusterWidgetSize: const Size(40, 40), - anchor: AnchorPos.align(AnchorAlign.center), - calculateAggregatedClusterData: true, - builder: (context, position, markerCount, extraClusterData) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.0), - color: Colors.blue, - ), - child: Center( - child: Text( - markerCount.toString(), - style: const TextStyle(color: Colors.white), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: tileLayerPackageName, + ), + SuperclusterLayer.mutable( + initialMarkers: _initialMarkers, + indexBuilder: IndexBuilders.rootIsolate, + loadingOverlayBuilder: (_) => const SizedBox.shrink(), + controller: _superclusterController, + moveMap: (center, zoom) => _animatedMapController.animateTo( + dest: center, + zoom: zoom, + ), + popupOptions: PopupOptions( + popupDisplayOptions: PopupDisplayOptions( + builder: (context, marker) => GestureDetector( + onTap: () => _superclusterController.remove(marker), + child: Card( + child: SizedBox( + width: 250, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: Icon( + Icons.delete, + color: Colors.grey.shade600, + )), + const Expanded( + child: Text( + 'Tap this popup to remove the marker. Tap the marker again to close this popup.', + softWrap: true, + ), + ), + ], + ), + ), + ), + ), ), ), - ); - }, - ), - ], + ), + clusterWidgetSize: const Size(40, 40), + clusterAlignment: Alignment.center, + calculateAggregatedClusterData: true, + builder: (context, position, markerCount, extraClusterData) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: Colors.blue, + ), + child: Center( + child: Text( + markerCount.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ); + }, + ), + ], + ), ), ); } static Marker _createMarker(LatLng point, Color color) => Marker( - anchorPos: AnchorPos.align(AnchorAlign.top), + alignment: Alignment.topCenter, rotate: true, - rotateAlignment: AnchorAlign.top.rotationAlignment, height: 30, width: 30, point: point, - builder: (ctx) => Icon( + child: Icon( AccurateMapIcons.locationOnBottomAligned, color: color, size: 30, diff --git a/example/lib/normal_and_clustered_markers_with_popups_page.dart b/example/lib/normal_and_clustered_markers_with_popups_page.dart index e0f010b..027e296 100644 --- a/example/lib/normal_and_clustered_markers_with_popups_page.dart +++ b/example/lib/normal_and_clustered_markers_with_popups_page.dart @@ -1,28 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; import 'package:flutter_map_supercluster_example/drawer.dart'; +import 'package:flutter_map_supercluster_example/main.dart'; import 'package:latlong2/latlong.dart'; -class NormalAndClusteredMarkersWithPopups extends StatefulWidget { - static const String route = 'normalAndClusteredMarkersWithPopupPage'; +class NormalAndClusteredMarkersWithPopupsPage extends StatefulWidget { + static const String route = 'normalAndClusteredMarkersWithPopupsPage'; - const NormalAndClusteredMarkersWithPopups({Key? key}) : super(key: key); + const NormalAndClusteredMarkersWithPopupsPage({super.key}); @override - State createState() => - _NormalAndClusteredMarkersWithPopupsState(); + State createState() => + _NormalAndClusteredMarkersWithPopupsPageState(); } -class _NormalAndClusteredMarkersWithPopupsState - extends State { +class _NormalAndClusteredMarkersWithPopupsPageState + extends State { late final SuperclusterImmutableController _superclusterController; late final PopupController _popupController; - static final points = [ - const LatLng(51.5, 0), - const LatLng(51.0, 0.5), + static const points = [ + LatLng(51.5, 0), + LatLng(51.0, 0.5), ]; late List markersA; late List markersB; @@ -42,13 +43,12 @@ class _NormalAndClusteredMarkersWithPopupsState } Marker _createMarker(LatLng point, Color color) => Marker( - anchorPos: AnchorPos.align(AnchorAlign.top), - rotateAlignment: AnchorAlign.top.rotationAlignment, + alignment: Alignment.topCenter, height: 30, width: 30, point: point, rotate: true, - builder: (ctx) => Icon(Icons.pin_drop, color: color), + child: Icon(Icons.pin_drop, color: color), ); @override @@ -63,7 +63,8 @@ class _NormalAndClusteredMarkersWithPopupsState return Scaffold( appBar: AppBar(title: const Text('Normal and Clustered Markers With Popups')), - drawer: buildDrawer(context, NormalAndClusteredMarkersWithPopups.route), + drawer: + buildDrawer(context, NormalAndClusteredMarkersWithPopupsPage.route), body: PopupScope( popupController: _popupController, onPopupEvent: (event, selectedMarkers) => debugPrint( @@ -71,8 +72,8 @@ class _NormalAndClusteredMarkersWithPopupsState ), child: FlutterMap( options: MapOptions( - center: points[0], - zoom: 5, + initialCenter: points[0], + initialZoom: 5, maxZoom: 15, onTap: (_, __) { _popupController.hideAllPopups(); @@ -80,15 +81,15 @@ class _NormalAndClusteredMarkersWithPopupsState ), children: [ TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: tileLayerPackageName, ), SuperclusterLayer.immutable( initialMarkers: markersA, indexBuilder: IndexBuilders.rootIsolate, controller: _superclusterController, clusterWidgetSize: const Size(40, 40), - anchor: AnchorPos.align(AnchorAlign.center), + clusterAlignment: Alignment.center, popupOptions: PopupOptions( selectedMarkerBuilder: (context, marker) => Icon( Icons.pin_drop, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4fb6d06..62902ec 100755 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,16 +11,16 @@ dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.5 - flutter_map: ^5.0.0 + cupertino_icons: ^1.0.6 + flutter_map: ^7.0.2 + flutter_map_animations: ^0.7.1 + flutter_map_marker_popup: ^7.0.0 flutter_map_supercluster: path: ../ - flutter_map_animations: ^0.4.1 - flutter_map_marker_popup: ^5.2.0 latlong2: ^0.9.0 dev_dependencies: - flutter_lints: ^2.0.2 + flutter_lints: ^3.0.1 flutter_test: sdk: flutter @@ -29,4 +29,4 @@ flutter: fonts: - family: AccurateMapIcon fonts: - - asset: fonts/AccurateMapIcon.ttf + - asset: fonts/AccurateMapIcon.ttf \ No newline at end of file diff --git a/lib/flutter_map_supercluster.dart b/lib/flutter_map_supercluster.dart index ffb86a1..0dd6ccd 100755 --- a/lib/flutter_map_supercluster.dart +++ b/lib/flutter_map_supercluster.dart @@ -5,7 +5,6 @@ export 'package:supercluster/supercluster.dart'; export 'src/controller/marker_matcher.dart'; export 'src/controller/supercluster_controller.dart'; -export 'src/controller/supercluster_state.dart'; export 'src/layer/cluster_data.dart'; export 'src/layer/supercluster_layer.dart'; export 'src/options/index_builder.dart'; @@ -14,3 +13,5 @@ export 'src/splay/cluster_splay_delegate.dart'; export 'src/splay/displaced_marker.dart'; export 'src/splay/displaced_marker_offset.dart'; export 'src/splay/spread_cluster_splay_delegate.dart'; +export 'src/state/supercluster_scope.dart'; +export 'src/state/supercluster_state.dart'; diff --git a/lib/src/controller/marker_matcher.dart b/lib/src/controller/marker_matcher.dart index 1eabc34..2b76ed3 100644 --- a/lib/src/controller/marker_matcher.dart +++ b/lib/src/controller/marker_matcher.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; /// Matches a given marker either by equality (MarkerMatcher.equalsMarker) or diff --git a/lib/src/controller/supercluster_controller.dart b/lib/src/controller/supercluster_controller.dart index c0766b5..62b22b6 100644 --- a/lib/src/controller/supercluster_controller.dart +++ b/lib/src/controller/supercluster_controller.dart @@ -1,27 +1,15 @@ import 'dart:async'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/controller/marker_matcher.dart'; import 'package:latlong2/latlong.dart'; import 'supercluster_controller_impl.dart'; -import 'supercluster_state.dart'; abstract class SuperclusterController { /// Clear all of the existing Markers. void clear(); - /// A Future that completes with an Iterable of all Markers in the order that - /// the inner cluster store holds them. The Future will complete when the - /// controller is associated with a layer and the loading of the supercluster - /// finishes. - Future> all(); - - /// A Stream of the [SuperclusterState]. Note that the [SuperclusterState]'s - /// aggregatedClusterData will not be calculated unless [SuperclusterLayer]'s - /// [calculateAggregatedClusterData] is true. - Stream get stateStream; - /// Collapses any splayed clusters. See SuperclusterLayer's /// [clusterSplayDelegate] for more information on splaying. void collapseSplayedClusters(); @@ -107,9 +95,7 @@ abstract class SuperclusterController { } abstract class SuperclusterImmutableController extends SuperclusterController { - factory SuperclusterImmutableController() => SuperclusterControllerImpl( - createdInternally: false, - ); + factory SuperclusterImmutableController() => SuperclusterControllerImpl(); /// Remove all of the existing Markers and replace them with [markers]. Note /// that this requires completely rebuilding the clusters and may be a slow @@ -120,17 +106,23 @@ abstract class SuperclusterImmutableController extends SuperclusterController { } abstract class SuperclusterMutableController extends SuperclusterController { - factory SuperclusterMutableController() => SuperclusterControllerImpl( - createdInternally: false, - ); + factory SuperclusterMutableController() => SuperclusterControllerImpl(); /// Add a single [Marker]. This [Marker] will be clustered if possible. void add(Marker marker); + /// Add multiple [markers]. The markers will be clustered if possible. This + /// method is faster than [add] for multple markers. + void addAll(List markers); + /// Remove a single [Marker]. This may cause some clusters to be split and /// rebuilt. void remove(Marker marker); + /// Remove multiple [markers]. The markers will be clustered if possible. This + /// method is faster than [remove] for multple markers. + void removeAll(List markers); + /// Modify a Marker. Note that [oldMarker] must have the same [pos] as /// [newMarker]. This is an optimised function that skips re-clustering. void modifyMarker( diff --git a/lib/src/controller/supercluster_controller_impl.dart b/lib/src/controller/supercluster_controller_impl.dart index 19682ae..3294368 100644 --- a/lib/src/controller/supercluster_controller_impl.dart +++ b/lib/src/controller/supercluster_controller_impl.dart @@ -1,39 +1,29 @@ import 'dart:async'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/controller/marker_matcher.dart'; import 'package:flutter_map_supercluster/src/controller/supercluster_event.dart'; import 'package:latlong2/latlong.dart'; -import 'package:supercluster/supercluster.dart'; import 'supercluster_controller.dart'; -import 'supercluster_state.dart'; class SuperclusterControllerImpl implements SuperclusterImmutableController, SuperclusterMutableController { - final bool createdInternally; final StreamController _superclusterEventController; - final StreamController _stateStreamController; - Future> _supercluster = Future.any([]); - SuperclusterControllerImpl({required this.createdInternally}) - : _superclusterEventController = StreamController.broadcast(), - _stateStreamController = - StreamController.broadcast(); + SuperclusterControllerImpl() + : _superclusterEventController = StreamController.broadcast(); Stream get stream => _superclusterEventController.stream; @override - Stream get stateStream => - _stateStreamController.stream.distinct(); - - void setSupercluster(Future> supercluster) { - _supercluster = supercluster; + void add(Marker marker) { + _superclusterEventController.add(AddMarkerEvent(marker)); } @override - void add(Marker marker) { - _superclusterEventController.add(AddMarkerEvent(marker)); + void addAll(List markers) { + _superclusterEventController.add(AddAllMarkerEvent(markers)); } @override @@ -41,6 +31,11 @@ class SuperclusterControllerImpl _superclusterEventController.add(RemoveMarkerEvent(marker)); } + @override + void removeAll(List markers) { + _superclusterEventController.add(RemoveAllMarkerEvent(markers)); + } + @override void modifyMarker( Marker oldMarker, @@ -55,10 +50,6 @@ class SuperclusterControllerImpl )); } - void removeSupercluster() { - _supercluster = Future.any([]); - } - @override void replaceAll(List markers) { _superclusterEventController.add(ReplaceAllMarkerEvent(markers)); @@ -69,11 +60,6 @@ class SuperclusterControllerImpl _superclusterEventController.add(const ReplaceAllMarkerEvent([])); } - @override - Future> all() { - return _supercluster.then((supercluster) => supercluster.getLeaves()); - } - @override void collapseSplayedClusters() { _superclusterEventController.add(const CollapseSplayedClustersEvent()); @@ -94,10 +80,6 @@ class SuperclusterControllerImpl ); } - void updateState(SuperclusterState newState) { - _stateStreamController.add(newState); - } - @override void showPopupsAlsoFor( List markers, { @@ -155,6 +137,5 @@ class SuperclusterControllerImpl @override void dispose() { _superclusterEventController.close(); - _stateStreamController.close(); } } diff --git a/lib/src/controller/supercluster_event.dart b/lib/src/controller/supercluster_event.dart index 8bd610d..12fcff5 100644 --- a/lib/src/controller/supercluster_event.dart +++ b/lib/src/controller/supercluster_event.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/controller/marker_matcher.dart'; import 'package:latlong2/latlong.dart'; @@ -14,12 +14,24 @@ class AddMarkerEvent extends SuperclusterEvent { const AddMarkerEvent(this.marker); } +class AddAllMarkerEvent extends SuperclusterEvent { + final List markers; + + const AddAllMarkerEvent(this.markers); +} + class RemoveMarkerEvent extends SuperclusterEvent { final Marker marker; const RemoveMarkerEvent(this.marker); } +class RemoveAllMarkerEvent extends SuperclusterEvent { + final List markers; + + const RemoveAllMarkerEvent(this.markers); +} + class ReplaceAllMarkerEvent extends SuperclusterEvent { final List markers; diff --git a/lib/src/controller/supercluster_state.dart b/lib/src/controller/supercluster_state.dart deleted file mode 100644 index d61e981..0000000 --- a/lib/src/controller/supercluster_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; - -class SuperclusterState extends Equatable { - final bool loading; - - final ClusterData? aggregatedClusterData; - - const SuperclusterState({ - required this.loading, - required this.aggregatedClusterData, - }); - - @override - List get props => [loading, aggregatedClusterData]; -} diff --git a/lib/src/layer/alignment_util.dart b/lib/src/layer/alignment_util.dart new file mode 100644 index 0000000..7f9898c --- /dev/null +++ b/lib/src/layer/alignment_util.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class AlignmentUtil { + static Point applyAlignment( + Point pos, + double width, + double height, + Alignment alignment, + ) { + final x = (pos.x - (width / 2) + ((width / 2) * alignment.x)).toDouble(); + final y = (pos.y - (height / 2) + ((height / 2) * alignment.y)).toDouble(); + return Point(x, y); + } +} diff --git a/lib/src/layer/anchor_util.dart b/lib/src/layer/anchor_util.dart deleted file mode 100644 index 3e594c1..0000000 --- a/lib/src/layer/anchor_util.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:math'; -import 'dart:ui'; - -import 'package:flutter_map/plugin_api.dart'; -import 'package:supercluster/supercluster.dart'; - -class AnchorUtil { - static Point removeClusterAnchor( - CustomPoint pos, - LayerCluster cluster, - AnchorPos? clusterAnchorPos, - Size clusterWidgetSize, - ) { - final anchor = Anchor.fromPos( - clusterAnchorPos ?? AnchorPos.align(AnchorAlign.center), - clusterWidgetSize.width, - clusterWidgetSize.height, - ); - - return removeAnchor( - pos, - clusterWidgetSize.width, - clusterWidgetSize.height, - anchor, - ); - } - - static Point removeAnchor( - Point pos, - double width, - double height, - Anchor anchor, - ) { - final x = (pos.x - (width - anchor.left)).toDouble(); - final y = (pos.y - (height - anchor.top)).toDouble(); - return Point(x, y); - } -} diff --git a/lib/src/layer/cluster_data.dart b/lib/src/layer/cluster_data.dart index 13423f8..beb3baf 100644 --- a/lib/src/layer/cluster_data.dart +++ b/lib/src/layer/cluster_data.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:supercluster/supercluster.dart'; class ClusterData extends ClusterDataBase { diff --git a/lib/src/layer/create_supercluster.dart b/lib/src/layer/create_supercluster.dart index 5cf1ddb..5968a26 100644 --- a/lib/src/layer/create_supercluster.dart +++ b/lib/src/layer/create_supercluster.dart @@ -1,40 +1,41 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/layer/cluster_data.dart'; -import 'package:flutter_map_supercluster/src/layer/supercluster_config.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_parameters.dart'; import 'package:supercluster/supercluster.dart'; -Supercluster createSupercluster(SuperclusterConfig config) { - return config.isMutableSupercluster - ? _loadMutable(config) - : _loadImmutable(config); +Supercluster createSupercluster(SuperclusterParameters parameters) { + return parameters.isMutableSupercluster + ? _loadMutable(parameters) + : _loadImmutable(parameters); } -SuperclusterMutable _loadMutable(SuperclusterConfig config) { +SuperclusterMutable _loadMutable(SuperclusterParameters parameters) { return SuperclusterMutable( getX: (m) => m.point.longitude, getY: (m) => m.point.latitude, - minZoom: config.minZoom, - maxZoom: config.maxZoom, - minPoints: config.minimumClusterSize, + minZoom: parameters.minZoom, + maxZoom: parameters.maxZoom, + minPoints: parameters.minimumClusterSize, extractClusterData: (marker) => ClusterData( marker, - innerExtractor: config.innerClusterDataExtractor, + innerExtractor: parameters.innerClusterDataExtractor, ), - radius: config.maxClusterRadius, - )..load(config.markers); + radius: parameters.maxClusterRadius, + )..load(parameters.markers); } -SuperclusterImmutable _loadImmutable(SuperclusterConfig config) { +SuperclusterImmutable _loadImmutable( + SuperclusterParameters parameters) { return SuperclusterImmutable( getX: (m) => m.point.longitude, getY: (m) => m.point.latitude, - minZoom: config.minZoom, - maxZoom: config.maxZoom, + minZoom: parameters.minZoom, + maxZoom: parameters.maxZoom, extractClusterData: (marker) => ClusterData( marker, - innerExtractor: config.innerClusterDataExtractor, + innerExtractor: parameters.innerClusterDataExtractor, ), - radius: config.maxClusterRadius, - minPoints: config.minimumClusterSize, - )..load(config.markers); + radius: parameters.maxClusterRadius, + minPoints: parameters.minimumClusterSize, + )..load(parameters.markers); } diff --git a/lib/src/layer/expanded_cluster_manager.dart b/lib/src/layer/expanded_cluster_manager.dart index 9c466ae..5166bd7 100644 --- a/lib/src/layer/expanded_cluster_manager.dart +++ b/lib/src/layer/expanded_cluster_manager.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/widget/expanded_cluster.dart'; import 'package:supercluster/supercluster.dart'; @@ -102,7 +102,45 @@ class ExpandedClusterManager { } } - /// Removes all without triggering onRemove callback. + void removeImmediately(ExpandedCluster expandedCluster) { + final removed = _expandedClusters.remove(expandedCluster.layerCluster.uuid); + if (removed == null) return; + + // Dispose after removal so that ExpandedClusterManager never contains + // disposed ExpandedClusters. + expandedCluster.dispose(); + + onRemoveStart([removed]); + onRemoved([removed]); + } + + void removeAllImmediately(Iterable expandedClusters) { + final removalIds = expandedClusters.map((e) => e.layerCluster.uuid).toSet(); + final removed = []; + + _expandedClusters.removeWhere((uuid, expandedCluster) { + if (removalIds.contains(uuid)) { + removed.add(expandedCluster); + return true; + } + return false; + }); + + // Dispose after removal so that ExpandedClusterManager never contains + // disposed ExpandedClusters. + for (var expandedCluster in removed) { + expandedCluster.dispose(); + } + + if (removed.isNotEmpty) { + onRemoveStart(removed); + onRemoved(removed); + } + } + + /// Removes all without triggering onRemove callback. The removal callback is + /// used for hiding popups but this method should only be called after + /// re-initializing the supercluster which already hides popups. void clear() { final removed = List.from(_expandedClusters.values); _expandedClusters.clear(); diff --git a/lib/src/layer/loading_overlay.dart b/lib/src/layer/loading_overlay.dart index 1eb65af..1f6f735 100644 --- a/lib/src/layer/loading_overlay.dart +++ b/lib/src/layer/loading_overlay.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:supercluster/supercluster.dart'; class LoadingOverlay extends StatelessWidget { diff --git a/lib/src/layer/flutter_map_state_extension.dart b/lib/src/layer/map_camera_extension.dart similarity index 60% rename from lib/src/layer/flutter_map_state_extension.dart rename to lib/src/layer/map_camera_extension.dart index 7a2560b..832d237 100644 --- a/lib/src/layer/flutter_map_state_extension.dart +++ b/lib/src/layer/map_camera_extension.dart @@ -1,13 +1,15 @@ +import 'dart:math'; import 'dart:ui'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; -extension FlutterMapStateExtension on FlutterMapState { - CustomPoint getPixelOffset(LatLng point) => project(point) - pixelOrigin; +extension MapCameraExtension on MapCamera { + Point getPixelOffset(LatLng point) => + project(point) - pixelOrigin.toDoublePoint(); LatLngBounds paddedMapBounds(Size clusterWidgetSize) { - final boundsPixelPadding = CustomPoint( + final boundsPixelPadding = Point( clusterWidgetSize.width / 2, clusterWidgetSize.height / 2, ); diff --git a/lib/src/layer/supercluster_config.dart b/lib/src/layer/supercluster_config.dart index d982bfa..3e48781 100644 --- a/lib/src/layer/supercluster_config.dart +++ b/lib/src/layer/supercluster_config.dart @@ -1,25 +1,77 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:supercluster/supercluster.dart'; -class SuperclusterConfig { - final bool isMutableSupercluster; +/// This describes the options needed to create a Supercluster but does not +/// contain the Markers. See SuperclusterParameters for the options and the +/// Markers in a single class. +/// +/// This is defined as abstract to allow SuperclusterParameters to implement it +/// without needing to implement Equatable methods since Equatable is used on +/// SuperclusterConfigImpl. +abstract class SuperclusterConfig { + static const defaultMinZoom = 1; + static const defaultMaxZoom = 20; - final List markers; + bool get isMutableSupercluster; + int get maxClusterRadius; + ClusterDataBase Function(Marker marker)? get innerClusterDataExtractor; + int? get minimumClusterSize; - final int minZoom; - final int maxZoom; + int get minZoom; + int get maxZoom; - final ClusterDataBase Function(Marker marker)? innerClusterDataExtractor; + const SuperclusterConfig(); +} + +class SuperclusterConfigImpl extends Equatable implements SuperclusterConfig { + @override + final bool isMutableSupercluster; + @override final int maxClusterRadius; + @override + final ClusterDataBase Function(Marker marker)? innerClusterDataExtractor; + @override final int? minimumClusterSize; - SuperclusterConfig({ + final int? _maxClusterZoom; + final int? _mapStateMinZoom; + final int? _mapStateMaxZoom; + + SuperclusterConfigImpl({ + required MapCamera mapCamera, required this.isMutableSupercluster, - required this.markers, - required this.minZoom, - required this.maxZoom, + required int? maxClusterZoom, required this.maxClusterRadius, required this.innerClusterDataExtractor, - this.minimumClusterSize, - }); + required this.minimumClusterSize, + }) : _maxClusterZoom = maxClusterZoom, + _mapStateMinZoom = _mapStateMinZoomFrom(mapCamera), + _mapStateMaxZoom = _mapStateMaxZoomFrom(mapCamera); + + @override + int get minZoom => _mapStateMinZoom ?? SuperclusterConfig.defaultMinZoom; + @override + int get maxZoom => + _maxClusterZoom ?? _mapStateMaxZoom ?? SuperclusterConfig.defaultMaxZoom; + + bool mapStateZoomLimitsHaveChanged(MapCamera mapCamera) => + _mapStateMaxZoom != _mapStateMaxZoomFrom(mapCamera) || + _mapStateMinZoom != _mapStateMinZoomFrom(mapCamera); + + static int? _mapStateMinZoomFrom(MapCamera mapCamera) => + mapCamera.minZoom?.ceil(); + + static int? _mapStateMaxZoomFrom(MapCamera mapCamera) => + mapCamera.maxZoom?.ceil(); + + @override + List get props => [ + isMutableSupercluster, + minZoom, + maxZoom, + maxClusterRadius, + innerClusterDataExtractor, + minimumClusterSize, + ]; } diff --git a/lib/src/layer/supercluster_layer.dart b/lib/src/layer/supercluster_layer.dart index cb29351..769d709 100755 --- a/lib/src/layer/supercluster_layer.dart +++ b/lib/src/layer/supercluster_layer.dart @@ -1,37 +1,13 @@ import 'dart:async'; -import 'dart:math'; -import 'package:async/async.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_marker_popup/extension_api.dart'; -import 'package:flutter_map_supercluster/src/controller/marker_matcher.dart'; -import 'package:flutter_map_supercluster/src/controller/supercluster_event.dart'; -import 'package:flutter_map_supercluster/src/controller/supercluster_state.dart'; -import 'package:flutter_map_supercluster/src/layer/create_supercluster.dart'; -import 'package:flutter_map_supercluster/src/layer/expanded_cluster_manager.dart'; -import 'package:flutter_map_supercluster/src/layer/flutter_map_state_extension.dart'; -import 'package:flutter_map_supercluster/src/layer/loading_overlay.dart'; -import 'package:flutter_map_supercluster/src/layer/supercluster_config.dart'; -import 'package:flutter_map_supercluster/src/layer_element_extension.dart'; -import 'package:flutter_map_supercluster/src/options/index_builder.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; +import 'package:flutter_map_supercluster/src/controller/supercluster_controller_impl.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_layer_impl.dart'; import 'package:flutter_map_supercluster/src/options/popup_options_impl.dart'; -import 'package:flutter_map_supercluster/src/splay/cluster_splay_delegate.dart'; -import 'package:flutter_map_supercluster/src/splay/popup_spec_builder.dart'; -import 'package:flutter_map_supercluster/src/splay/spread_cluster_splay_delegate.dart'; -import 'package:flutter_map_supercluster/src/supercluster_extension.dart'; -import 'package:flutter_map_supercluster/src/widget/expandable_cluster_widget.dart'; -import 'package:flutter_map_supercluster/src/widget/expanded_cluster.dart'; +import 'package:flutter_map_supercluster/src/state/inherit_or_create_supercluster_scope.dart'; import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; -import 'package:supercluster/supercluster.dart'; - -import '../controller/supercluster_controller.dart'; -import '../controller/supercluster_controller_impl.dart'; -import '../options/popup_options.dart'; -import '../widget/cluster_widget.dart'; -import '../widget/marker_widget.dart'; -import 'cluster_data.dart'; /// Builder for the cluster widget. typedef ClusterWidgetBuilder = Widget Function( @@ -44,16 +20,16 @@ typedef ClusterWidgetBuilder = Widget Function( /// See [SuperclusterLayer.moveMap]. typedef MoveMapCallback = FutureOr Function(LatLng center, double zoom); -class SuperclusterLayer extends StatefulWidget { +class SuperclusterLayer extends StatelessWidget { static const popupNamespace = 'flutter_map_supercluster'; - final bool _isMutableSupercluster; + final bool isMutableSupercluster; /// Cluster builder final ClusterWidgetBuilder builder; /// Controller for managing [Marker]s and listening to changes. - final SuperclusterControllerImpl controller; + final SuperclusterController? controller; /// Initial list of markers, additions/removals must be made using the /// [controller]. @@ -61,7 +37,19 @@ class SuperclusterLayer extends StatefulWidget { /// Builder used to create the supercluster index. See [IndexBuilders] for /// predefined builders and guidelines on which one to use. - final IndexBuilder indexBuilder; + /// + /// Defaults to IndexBuilder.rootIsolate which may be slow for large amounts + /// of markers. + final IndexBuilder? indexBuilder; + + /// The maximum zoom at which clusters will be formed, at higher zooms all + /// points will be visible. Defaults to the FlutterMap maxZoom or, if it is + /// not set, 20. + /// + /// This value must not be higher than the FlutterMap's maxZoom if it is set + /// as this will break splay cluster functionality and causes unnecesssary + /// calculations since not all clusters/points will be able to be viewed. + final int? maxClusterZoom; /// The minimum number of points required to form a cluster, if there is less /// than this number of points within the [maxClusterRadius] the markers will @@ -106,13 +94,13 @@ class SuperclusterLayer extends StatefulWidget { /// Cluster size final Size clusterWidgetSize; - /// Cluster anchor - final AnchorPos? anchor; + /// Cluster anchor position. + final Alignment clusterAlignment; /// If true then whenever the aggregated cluster data changes (that is, the /// combined cluster data of all Markers as calculated by - /// [clusterDataExtractor]) then the new value will be added to the - /// [controller]'s [aggregatedClusterDataStream]. + /// [clusterDataExtractor]) dependents of SuperclusterState will be notified + /// with the updated value. final bool calculateAggregatedClusterData; /// Splaying occurs when it is not possible to open a cluster because its @@ -120,14 +108,15 @@ class SuperclusterLayer extends StatefulWidget { /// controls the animation and style of the cluster splaying. final ClusterSplayDelegate clusterSplayDelegate; - SuperclusterLayer.immutable({ - Key? key, - SuperclusterImmutableController? controller, + const SuperclusterLayer.immutable({ + super.key, + SuperclusterImmutableController? this.controller, required this.builder, required this.indexBuilder, this.initialMarkers = const [], this.moveMap, this.onMarkerTap, + this.maxClusterZoom, this.minimumClusterSize, this.maxClusterRadius = 80, this.clusterDataExtractor, @@ -135,27 +124,24 @@ class SuperclusterLayer extends StatefulWidget { this.clusterWidgetSize = const Size(30, 30), this.loadingOverlayBuilder, PopupOptions? popupOptions, - this.anchor, + this.clusterAlignment = Alignment.center, this.clusterSplayDelegate = const SpreadClusterSplayDelegate( duration: Duration(milliseconds: 300), splayLineOptions: SplayLineOptions(), ), - }) : _isMutableSupercluster = false, - controller = controller != null - ? controller as SuperclusterControllerImpl - : SuperclusterControllerImpl(createdInternally: true), + }) : isMutableSupercluster = false, popupOptions = - popupOptions == null ? null : popupOptions as PopupOptionsImpl, - super(key: key); + popupOptions == null ? null : popupOptions as PopupOptionsImpl; - SuperclusterLayer.mutable({ - Key? key, - SuperclusterMutableController? controller, + const SuperclusterLayer.mutable({ + super.key, + SuperclusterMutableController? this.controller, required this.builder, - required this.indexBuilder, + this.indexBuilder, this.initialMarkers = const [], this.moveMap, this.onMarkerTap, + this.maxClusterZoom, this.minimumClusterSize, this.maxClusterRadius = 80, this.clusterDataExtractor, @@ -163,659 +149,38 @@ class SuperclusterLayer extends StatefulWidget { this.clusterWidgetSize = const Size(30, 30), this.loadingOverlayBuilder, PopupOptions? popupOptions, - this.anchor, + this.clusterAlignment = Alignment.center, this.clusterSplayDelegate = const SpreadClusterSplayDelegate( duration: Duration(milliseconds: 400), ), - }) : _isMutableSupercluster = true, - controller = controller != null - ? controller as SuperclusterControllerImpl - : SuperclusterControllerImpl(createdInternally: true), + }) : isMutableSupercluster = true, popupOptions = - popupOptions == null ? null : popupOptions as PopupOptionsImpl, - super(key: key); - - @override - State createState() => _SuperclusterLayerState(); -} - -class _SuperclusterLayerState extends State - with TickerProviderStateMixin { - static const defaultMinZoom = 1; - static const defaultMaxZoom = 20; - - bool _initialized = false; - - late int minZoom; - late int maxZoom; - - late FlutterMapState _mapState; - late final ExpandedClusterManager _expandedClusterManager; - - StreamSubscription? _controllerSubscription; - StreamSubscription? _movementStreamSubscription; - - int? _lastMovementZoom; - - PopupState? _popupState; - - CancelableCompleter> _superclusterCompleter = - CancelableCompleter(); - - @override - void initState() { - super.initState(); - _expandedClusterManager = ExpandedClusterManager( - onRemoveStart: (expandedClusters) { - // The flutter_map_marker_popup package takes care of hiding popups - // when zooming out but when an ExpandedCluster removal is triggered by - // SuperclusterController.collapseSplayedClusters we need to remove the - // popups ourselves. - widget.popupOptions?.popupController.hidePopupsOnlyFor( - expandedClusters - .expand((expandedCluster) => expandedCluster.markers) - .toList(), - ); - }, - onRemoved: (expandedClusters) => setState(() {}), - ); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - _mapState = FlutterMapState.maybeOf(context)!; - - final oldMinZoom = !_initialized ? null : minZoom; - final oldMaxZoom = !_initialized ? null : maxZoom; - minZoom = _mapState.options.minZoom?.ceil() ?? defaultMinZoom; - maxZoom = _mapState.options.maxZoom?.ceil() ?? defaultMaxZoom; - - bool zoomsChanged = - _initialized && oldMinZoom != minZoom || oldMaxZoom != maxZoom; - - if (!_initialized) { - _lastMovementZoom = _mapState.zoom.ceil(); - _controllerSubscription = widget.controller.stream.listen( - (superclusterEvent) => _onSuperclusterEvent(superclusterEvent)); - - _movementStreamSubscription = _mapState.mapController.mapEventStream - .listen((_) => _onMove(_mapState)); - } - - if (!_initialized || zoomsChanged) { - if (_initialized) { - debugPrint( - 'WARNING: Changes to the FlutterMapState have caused a rebuild of ' - 'the Supercluster clusters. This can be a slow operation and ' - 'should be avoided whenever possible.'); - } - _initializeClusterManager( - !_initialized - ? Future.value(widget.initialMarkers) - : _superclusterCompleter.operation.value - .then((supercluster) => supercluster.getLeaves().toList()), - ); - } - _initialized = true; - } - - @override - void didUpdateWidget(SuperclusterLayer oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget._isMutableSupercluster != widget._isMutableSupercluster || - oldWidget.controller != widget.controller) { - _onControllerChange(oldWidget.controller, widget.controller); - } - - if (oldWidget._isMutableSupercluster != widget._isMutableSupercluster || - oldWidget.maxClusterRadius != widget.maxClusterRadius || - oldWidget.minimumClusterSize != widget.minimumClusterSize || - oldWidget.calculateAggregatedClusterData != - widget.calculateAggregatedClusterData) { - debugPrint( - 'WARNING: Changes to the Supercluster options have caused a rebuild ' - 'of the Supercluster clusters. This can be a slow operation and ' - 'should be avoided whenever possible.'); - _initializeClusterManager(_superclusterCompleter.operation - .valueOrCancellation() - .then((supercluster) => supercluster?.getLeaves().toList() ?? [])); - } - - if (widget.popupOptions != oldWidget.popupOptions) { - oldWidget.popupOptions?.popupController.dispose(); - _movementStreamSubscription?.cancel(); - _movementStreamSubscription = _mapState.mapController.mapEventStream - .listen((_) => _onMove(_mapState)); - } - } + popupOptions == null ? null : popupOptions as PopupOptionsImpl; @override - void dispose() { - widget.popupOptions?.popupController.dispose(); - if (widget.controller.createdInternally) widget.controller.dispose(); - _movementStreamSubscription?.cancel(); - _controllerSubscription?.cancel(); - super.dispose(); - } - - void _initializeClusterManager(Future> markersFuture) { - widget.controller.updateState( - const SuperclusterState(loading: true, aggregatedClusterData: null), - ); - - final supercluster = - markersFuture.catchError((_) => []).then((markers) { - final superclusterConfig = SuperclusterConfig( - isMutableSupercluster: widget._isMutableSupercluster, - markers: markers, - minZoom: minZoom, - maxZoom: maxZoom, - maxClusterRadius: widget.maxClusterRadius, - minimumClusterSize: widget.minimumClusterSize, - innerClusterDataExtractor: widget.clusterDataExtractor, - ); - - if (_superclusterCompleter.isCompleted || - _superclusterCompleter.isCanceled) { - setState(() { - _superclusterCompleter = CancelableCompleter(); - }); - } - - final newSupercluster = - widget.indexBuilder.call(createSupercluster, superclusterConfig); - - _superclusterCompleter.complete(newSupercluster); - _superclusterCompleter.operation.value.then((supercluster) { - _onMarkersChange(); - _expandedClusterManager.clear(); - widget.popupOptions?.popupController.hidePopupsWhereSpec( - (popupSpec) => - popupSpec.namespace == SuperclusterLayer.popupNamespace, - ); - }); - - return newSupercluster; - }); - - widget.controller.setSupercluster(supercluster); - } - - @override - Widget build(BuildContext context) { - final mapState = FlutterMapState.maybeOf(context)!; - - return _wrapWithPopupStateIfPopupsEnabled( - (popupState) => Stack( - children: [ - _clustersAndMarkers(mapState), - if (widget.popupOptions?.popupDisplayOptions != null) - PopupLayer( - popupDisplayOptions: widget.popupOptions!.popupDisplayOptions!, - ), - LoadingOverlay( - superclusterFuture: _superclusterCompleter.operation.value, - loadingOverlayBuilder: widget.loadingOverlayBuilder, - ), - ], - ), - ); - } - - Widget _wrapWithPopupStateIfPopupsEnabled( - Widget Function(PopupState? popupState) builder) { - if (widget.popupOptions == null) return builder(null); - - return InheritOrCreatePopupScope( - popupController: widget.popupOptions!.popupController, - builder: (context, popupState) { - _popupState = popupState; - if (widget.popupOptions!.selectedMarkerBuilder != null) { - context.watch(); - } - return builder(popupState); - }, - ); - } - - Widget _clustersAndMarkers(FlutterMapState mapState) { - final paddedBounds = mapState.paddedMapBounds(widget.clusterWidgetSize); - - return FutureBuilder>( - future: _superclusterCompleter.operation.value, - builder: (context, snapshot) { - final supercluster = snapshot.data; - if (supercluster == null) return const SizedBox.shrink(); - - return Stack(children: [ - ..._buildClustersAndMarkers(mapState, supercluster, paddedBounds) - ]); - }, - ); - } - - Iterable _buildClustersAndMarkers( - FlutterMapState mapState, - Supercluster supercluster, - LatLngBounds paddedBounds, - ) sync* { - final selectedMarkerBuilder = - widget.popupOptions != null && _popupState!.selectedMarkers.isNotEmpty - ? widget.popupOptions!.selectedMarkerBuilder - : null; - final List> selectedLayerPoints = []; - final List> clusters = []; - - // Build non-selected markers first, queue the rest to build afterwards - // so they appear above these markers. - for (final layerElement in supercluster.search( - paddedBounds.west, - paddedBounds.south, - paddedBounds.east, - paddedBounds.north, - mapState.zoom.ceil(), - )) { - if (layerElement is LayerCluster) { - clusters.add(layerElement); - continue; - } - layerElement as LayerPoint; - if (selectedMarkerBuilder != null && - _popupState!.selectedMarkers.contains(layerElement.originalPoint)) { - selectedLayerPoints.add(layerElement); - continue; - } - yield _buildMarker(mapState, layerElement); - } - - // Build selected markers. - for (final selectedLayerPoint in selectedLayerPoints) { - yield _buildMarker(mapState, selectedLayerPoint, selected: true); - } - - // Build non expanded clusters. - for (final cluster in clusters) { - if (_expandedClusterManager.contains(cluster)) continue; - yield _buildCluster(mapState, supercluster, cluster); - } - - // Build expanded clusters. - for (final expandedCluster in _expandedClusterManager.all) { - yield _buildExpandedCluster(mapState, expandedCluster); - } - } - - Widget _buildMarker( - FlutterMapState mapState, - LayerPoint mapPoint, { - bool selected = false, - }) { - final marker = mapPoint.originalPoint; - - final markerBuilder = !selected - ? marker.builder - : (context) => - widget.popupOptions!.selectedMarkerBuilder!(context, marker); - - return MarkerWidget( - mapState: _mapState, - marker: marker, - markerBuilder: markerBuilder, - onTap: () => _onMarkerTap(PopupSpecBuilder.forLayerPoint(mapPoint)), - ); - } - - Widget _buildCluster( - FlutterMapState mapState, - Supercluster supercluster, - LayerCluster cluster, - ) { - return ClusterWidget( - mapState: _mapState, - cluster: cluster, - builder: widget.builder, - onTap: () => _onClusterTap(supercluster, cluster), - size: widget.clusterWidgetSize, - anchorPos: widget.anchor, - ); - } - - Widget _buildExpandedCluster( - FlutterMapState mapState, - ExpandedCluster expandedCluster, - ) { - final selectedMarkerBuilder = widget.popupOptions?.selectedMarkerBuilder; - final Widget Function(BuildContext context, Marker marker) markerBuilder = - selectedMarkerBuilder == null - ? ((context, marker) => marker.builder(context)) - : ((context, marker) => - _popupState?.selectedMarkers.contains(marker) == true - ? selectedMarkerBuilder(context, marker) - : marker.builder(context)); - - return ExpandableClusterWidget( - mapState: mapState, - expandedCluster: expandedCluster, - builder: widget.builder, - size: widget.clusterWidgetSize, - anchorPos: widget.anchor, - markerBuilder: markerBuilder, - onCollapse: () { - widget.popupOptions?.popupController - .hidePopupsOnlyFor(expandedCluster.markers.toList()); - _expandedClusterManager - .collapseThenRemove(expandedCluster.layerCluster); - }, - onMarkerTap: _onMarkerTap, - ); - } - - void _onClusterTap( - Supercluster supercluster, - LayerCluster layerCluster, - ) async { - if (layerCluster.highestZoom >= maxZoom) { - await _moveMapIfNotAt( - layerCluster.latLng, - layerCluster.highestZoom.toDouble(), - ); - - final splayAnimation = _expandedClusterManager.putIfAbsent( - layerCluster, - () => ExpandedCluster( - vsync: this, - mapState: _mapState, - layerPoints: - supercluster.childrenOf(layerCluster).cast>(), - layerCluster: layerCluster, - clusterSplayDelegate: widget.clusterSplayDelegate, + Widget build(BuildContext context) => InheritOrCreateSuperclusterScope( + child: SuperclusterLayerImpl( + isMutableSupercluster: isMutableSupercluster, + mapCamera: MapCamera.of(context), + mapController: MapController.of(context), + controller: controller == null + ? null + : controller as SuperclusterControllerImpl, + builder: builder, + indexBuilder: indexBuilder ?? IndexBuilders.rootIsolate, + initialMarkers: initialMarkers, + moveMap: moveMap, + onMarkerTap: onMarkerTap, + maxClusterZoom: maxClusterZoom, + minimumClusterSize: minimumClusterSize, + maxClusterRadius: maxClusterRadius, + clusterDataExtractor: clusterDataExtractor, + calculateAggregatedClusterData: calculateAggregatedClusterData, + clusterWidgetSize: clusterWidgetSize, + loadingOverlayBuilder: loadingOverlayBuilder, + popupOptions: popupOptions, + clusterAlignment: clusterAlignment, + clusterSplayDelegate: clusterSplayDelegate, ), ); - if (splayAnimation != null) setState(() {}); - } else { - await _moveMapIfNotAt( - layerCluster.latLng, - layerCluster.highestZoom + 0.5, - ); - } - } - - FutureOr _moveMapIfNotAt( - LatLng center, - double zoom, { - FutureOr Function(LatLng center, double zoom)? moveMapOverride, - }) { - if (center == _mapState.center && zoom == _mapState.zoom) { - return Future.value(); - } - - final moveMap = moveMapOverride ?? - widget.moveMap ?? - (center, zoom) => _mapState.move( - center, - zoom, - source: MapEventSource.custom, - ); - - return moveMap.call(center, zoom); - } - - void _onMarkerTap(PopupSpec popupSpec) { - _selectMarker(popupSpec); - widget.onMarkerTap?.call(popupSpec.marker); - } - - void _selectMarker(PopupSpec popupSpec) { - if (widget.popupOptions != null) { - assert(_popupState != null); - - final popupOptions = widget.popupOptions!; - popupOptions.markerTapBehavior.apply( - popupSpec, - _popupState!, - popupOptions.popupController, - ); - - if (popupOptions.selectedMarkerBuilder != null) setState(() {}); - } - } - - void _onMove(FlutterMapState mapState) { - final zoom = mapState.zoom.ceil(); - - if (_lastMovementZoom == null || zoom < _lastMovementZoom!) { - _expandedClusterManager.removeIfZoomGreaterThan(zoom); - } - - _lastMovementZoom = zoom; - } - - void _onMarkersChange() { - if (!widget.controller.createdInternally) { - _superclusterCompleter.operation.value.then((supercluster) { - final aggregatedClusterData = widget.calculateAggregatedClusterData - ? supercluster.aggregatedClusterData() - : null; - final clusterData = aggregatedClusterData == null - ? null - : (aggregatedClusterData as ClusterData); - widget.controller.updateState( - SuperclusterState( - loading: false, - aggregatedClusterData: clusterData, - ), - ); - }); - } - } - - void _onControllerChange( - SuperclusterControllerImpl oldController, - SuperclusterControllerImpl newController, - ) { - _controllerSubscription?.cancel(); - _controllerSubscription = - newController.stream.listen((event) => _onSuperclusterEvent(event)); - - oldController.removeSupercluster(); - if (oldController.createdInternally) oldController.dispose(); - newController.setSupercluster(_superclusterCompleter.operation.value); - } - - void _moveToMarker({ - required MarkerMatcher markerMatcher, - required bool showPopup, - required FutureOr Function(LatLng center, double zoom)? moveMap, - }) async { - // Create a shorthand for the map movement function. - move(center, zoom) => - _moveMapIfNotAt(center, zoom, moveMapOverride: moveMap); - - /// Find the Marker's LayerPoint. - final supercluster = await _superclusterCompleter.operation.value; - LayerPoint? foundLayerPoint = - supercluster.layerPointMatching(markerMatcher); - if (foundLayerPoint == null) return; - - final markerInSplayCluster = maxZoom < foundLayerPoint.lowestZoom; - if (markerInSplayCluster) { - await _moveToSplayClusterMarker( - supercluster: supercluster, - layerPoint: foundLayerPoint, - move: move, - showPopup: showPopup, - ); - } else { - await move( - foundLayerPoint.latLng, - max(foundLayerPoint.lowestZoom.toDouble(), _mapState.zoom), - ); - if (showPopup) { - _selectMarker(PopupSpecBuilder.forLayerPoint(foundLayerPoint)); - } - } - } - - /// Move to Marker inside splay cluster. There are three possibilities: - /// 1. There is already an ExpandedCluster containing the Marker and it - /// remains expanded during movement. - /// 2. There is already an ExpandedCluster and it closes during movement so - /// we must create a new one once movement finishes. - /// 3. There is NOT already an ExpandedCluster, we should create one and add - /// it once movement finishes. - Future _moveToSplayClusterMarker({ - required Supercluster supercluster, - required LayerPoint layerPoint, - required FutureOr Function(LatLng center, double zoom) move, - required bool showPopup, - }) async { - // Find the parent. - final layerCluster = supercluster.parentOf(layerPoint)!; - - // Shorthand for creating an ExpandedCluster. - createExpandedCluster() => ExpandedCluster( - vsync: this, - mapState: _mapState, - layerPoints: - supercluster.childrenOf(layerCluster).cast>(), - layerCluster: layerCluster, - clusterSplayDelegate: widget.clusterSplayDelegate, - ); - - // Find or create the marker's ExpandedCluster and use it to find the - // DisplacedMarker. - final expandedClusterBeforeMovement = - _expandedClusterManager.forLayerCluster(layerCluster); - final createdExpandedCluster = - expandedClusterBeforeMovement != null ? null : createExpandedCluster(); - final displacedMarker = - (expandedClusterBeforeMovement ?? createdExpandedCluster)! - .markersToDisplacedMarkers[layerPoint.originalPoint]!; - - // Move to the DisplacedMarker. - await move( - displacedMarker.displacedPoint, - max(_mapState.zoom, layerPoint.lowestZoom - 0.99999), - ); - - // Determine the ExpandedCluster after movement, either: - // 1. We created one (without adding it to ExpandedClusterManager) - // because there was none before movement. - // 2. Movement may have caused the ExpandedCluster to be removed in which - // case we create a new one. - final splayAnimation = _expandedClusterManager.putIfAbsent( - layerCluster, - () => createdExpandedCluster ?? createExpandedCluster(), - ); - if (splayAnimation != null) { - if (!mounted) return; - setState(() {}); - await splayAnimation; - } - - if (showPopup) { - final popupSpec = PopupSpecBuilder.forDisplacedMarker( - displacedMarker, - layerCluster.highestZoom, - ); - _selectMarker(popupSpec); - } - } - - void _onSuperclusterEvent(SuperclusterEvent event) async { - switch (event) { - case AddMarkerEvent(): - _superclusterCompleter.operation.then((supercluster) { - (supercluster as SuperclusterMutable).insert(event.marker); - _onMarkersChange(); - }); - case RemoveMarkerEvent(): - _superclusterCompleter.operation.then((supercluster) { - final removed = (supercluster as SuperclusterMutable) - .remove(event.marker); - if (removed) _onMarkersChange(); - }); - case ReplaceAllMarkerEvent(): - _initializeClusterManager(Future.value(event.markers)); - case ModifyMarkerEvent(): - _superclusterCompleter.operation.then((supercluster) { - final modified = - (supercluster as SuperclusterMutable).modifyPointData( - event.oldMarker, - event.newMarker, - updateParentClusters: event.updateParentClusters, - ); - - if (modified) _onMarkersChange(); - }); - case CollapseSplayedClustersEvent(): - _expandedClusterManager.collapseThenRemoveAll(); - case ShowPopupsAlsoForEvent(): - if (widget.popupOptions == null) return; - widget.popupOptions?.popupController.showPopupsAlsoForSpecs( - PopupSpecBuilder.buildList( - supercluster: await _superclusterCompleter.operation.value, - zoom: _mapState.zoom.ceil(), - maxZoom: maxZoom, - markers: event.markers, - expandedClusters: _expandedClusterManager.all, - ), - disableAnimation: event.disableAnimation, - ); - case MoveToMarkerEvent(): - _moveToMarker( - markerMatcher: event.markerMatcher, - showPopup: event.showPopup, - moveMap: event.moveMap, - ); - case ShowPopupsOnlyForEvent(): - if (widget.popupOptions == null) return; - widget.popupOptions?.popupController.showPopupsOnlyForSpecs( - PopupSpecBuilder.buildList( - supercluster: await _superclusterCompleter.operation.value, - zoom: _mapState.zoom.ceil(), - maxZoom: maxZoom, - markers: event.markers, - expandedClusters: _expandedClusterManager.all, - ), - disableAnimation: event.disableAnimation, - ); - case HideAllPopupsEvent(): - if (widget.popupOptions == null) return; - widget.popupOptions?.popupController.hideAllPopups( - disableAnimation: event.disableAnimation, - ); - case HidePopupsWhereEvent(): - if (widget.popupOptions == null) return; - widget.popupOptions?.popupController.hidePopupsWhere( - event.test, - disableAnimation: event.disableAnimation, - ); - case HidePopupsOnlyForEvent(): - if (widget.popupOptions == null) return; - widget.popupOptions?.popupController.hidePopupsOnlyFor( - event.markers, - disableAnimation: event.disableAnimation, - ); - case TogglePopupEvent(): - if (widget.popupOptions == null) return; - final popupSpec = PopupSpecBuilder.build( - supercluster: await _superclusterCompleter.operation.value, - zoom: _mapState.zoom.ceil(), - maxZoom: maxZoom, - marker: event.marker, - expandedClusters: _expandedClusterManager.all, - ); - if (popupSpec == null) return; - widget.popupOptions?.popupController.togglePopupSpec( - popupSpec, - disableAnimation: event.disableAnimation, - ); - } - - setState(() {}); - } } diff --git a/lib/src/layer/supercluster_layer_impl.dart b/lib/src/layer/supercluster_layer_impl.dart new file mode 100644 index 0000000..ca99208 --- /dev/null +++ b/lib/src/layer/supercluster_layer_impl.dart @@ -0,0 +1,754 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; +import 'package:flutter_map_supercluster/src/controller/supercluster_controller_impl.dart'; +import 'package:flutter_map_supercluster/src/controller/supercluster_event.dart'; +import 'package:flutter_map_supercluster/src/layer/create_supercluster.dart'; +import 'package:flutter_map_supercluster/src/layer/expanded_cluster_manager.dart'; +import 'package:flutter_map_supercluster/src/layer/loading_overlay.dart'; +import 'package:flutter_map_supercluster/src/layer/map_camera_extension.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_config.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_parameters.dart'; +import 'package:flutter_map_supercluster/src/layer_element_extension.dart'; +import 'package:flutter_map_supercluster/src/options/popup_options_impl.dart'; +import 'package:flutter_map_supercluster/src/splay/popup_spec_builder.dart'; +import 'package:flutter_map_supercluster/src/state/inherited_supercluster_scope.dart'; +import 'package:flutter_map_supercluster/src/supercluster_extension.dart'; +import 'package:flutter_map_supercluster/src/widget/cluster_widget.dart'; +import 'package:flutter_map_supercluster/src/widget/expandable_cluster_widget.dart'; +import 'package:flutter_map_supercluster/src/widget/expanded_cluster.dart'; +import 'package:flutter_map_supercluster/src/widget/marker_widget.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +class SuperclusterLayerImpl extends StatefulWidget { + final bool isMutableSupercluster; + final MapCamera mapCamera; + final MapController mapController; + final ClusterWidgetBuilder builder; + final SuperclusterControllerImpl? controller; + final List initialMarkers; + final IndexBuilder indexBuilder; + final int? maxClusterZoom; + final int? minimumClusterSize; + final int maxClusterRadius; + final ClusterDataBase Function(Marker marker)? clusterDataExtractor; + final MoveMapCallback? moveMap; + final void Function(Marker)? onMarkerTap; + final WidgetBuilder? loadingOverlayBuilder; + final PopupOptionsImpl? popupOptions; + final Size clusterWidgetSize; + final Alignment clusterAlignment; + final bool calculateAggregatedClusterData; + final ClusterSplayDelegate clusterSplayDelegate; + + const SuperclusterLayerImpl({ + super.key, + required this.isMutableSupercluster, + required this.mapCamera, + required this.mapController, + required this.controller, + required this.builder, + required this.indexBuilder, + required this.initialMarkers, + required this.moveMap, + required this.onMarkerTap, + required this.maxClusterZoom, + required this.minimumClusterSize, + required this.maxClusterRadius, + required this.clusterDataExtractor, + required this.calculateAggregatedClusterData, + required this.clusterWidgetSize, + required this.loadingOverlayBuilder, + required this.popupOptions, + required this.clusterAlignment, + required this.clusterSplayDelegate, + }); + + @override + State createState() => _SuperclusterLayerImplState(); +} + +class _SuperclusterLayerImplState extends State + with TickerProviderStateMixin { + late SuperclusterConfigImpl _superclusterConfig; + + late final ExpandedClusterManager _expandedClusterManager; + + StreamSubscription? _controllerSubscription; + StreamSubscription? _movementStreamSubscription; + + int? _lastMovementZoom; + + PopupState? _popupState; + + CancelableCompleter> _superclusterCompleter = + CancelableCompleter(); + + @override + void initState() { + super.initState(); + + _lastMovementZoom = widget.mapCamera.zoom.ceil(); + _controllerSubscription = widget.controller?.stream + .listen((superclusterEvent) => _onSuperclusterEvent(superclusterEvent)); + + _movementStreamSubscription = + widget.mapController.mapEventStream.listen((_) => _onMove()); + + _superclusterConfig = SuperclusterConfigImpl( + isMutableSupercluster: widget.isMutableSupercluster, + mapCamera: widget.mapCamera, + maxClusterZoom: widget.maxClusterZoom, + maxClusterRadius: widget.maxClusterRadius, + innerClusterDataExtractor: widget.clusterDataExtractor, + minimumClusterSize: widget.minimumClusterSize, + ); + + _expandedClusterManager = ExpandedClusterManager( + onRemoveStart: (expandedClusters) { + // The flutter_map_marker_popup package takes care of hiding popups + // when zooming out but when an ExpandedCluster removal is triggered by + // SuperclusterController.collapseSplayedClusters we need to remove the + // popups ourselves. + + widget.popupOptions?.popupController.hidePopupsOnlyFor( + expandedClusters + .expand((expandedCluster) => expandedCluster.markers) + .toList(), + ); + }, + onRemoved: (expandedClusters) => setState(() {}), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeSupercluster(Future.value(widget.initialMarkers)); + }); + } + + @override + void didUpdateWidget(SuperclusterLayerImpl oldWidget) { + super.didUpdateWidget(oldWidget); + + final oldSuperclusterConfig = _superclusterConfig; + _superclusterConfig = SuperclusterConfigImpl( + isMutableSupercluster: widget.isMutableSupercluster, + mapCamera: widget.mapCamera, + maxClusterZoom: widget.maxClusterZoom, + maxClusterRadius: widget.maxClusterRadius, + innerClusterDataExtractor: widget.clusterDataExtractor, + minimumClusterSize: widget.minimumClusterSize, + ); + + // Change the controller if necessary. + if (oldSuperclusterConfig.isMutableSupercluster != + _superclusterConfig.isMutableSupercluster || + oldWidget.controller != widget.controller) { + _controllerSubscription?.cancel(); + _controllerSubscription = widget.controller?.stream + .listen((event) => _onSuperclusterEvent(event)); + } + + if (oldSuperclusterConfig.mapStateZoomLimitsHaveChanged(widget.mapCamera)) { + debugPrint( + 'WARNING: Changes to the FlutterMapState have caused a rebuild of ' + 'the Supercluster clusters. This can be a slow operation and ' + 'should be avoided whenever possible.'); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeSupercluster(_superclusterCompleter.operation + .valueOrCancellation() + .then((supercluster) => supercluster?.getLeaves().toList() ?? [])); + }); + } else if (oldSuperclusterConfig != _superclusterConfig) { + debugPrint( + 'WARNING: Changes to the Supercluster options have caused a rebuild ' + 'of the Supercluster clusters. This can be a slow operation and ' + 'should be avoided whenever possible.'); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeSupercluster(_superclusterCompleter.operation + .valueOrCancellation() + .then((supercluster) => supercluster?.getLeaves().toList() ?? [])); + }); + } + + if (widget.popupOptions != oldWidget.popupOptions) { + oldWidget.popupOptions?.popupController.dispose(); + } + } + + @override + void dispose() { + widget.popupOptions?.popupController.dispose(); + _movementStreamSubscription?.cancel(); + _controllerSubscription?.cancel(); + super.dispose(); + } + + void _initializeSupercluster(Future> markersFuture) { + InheritedSuperclusterScope.of(context, listen: false).setSuperclusterState( + const SuperclusterStateImpl( + supercluster: null, + aggregatedClusterData: null, + ), + ); + markersFuture.catchError((_) => []).then((markers) { + if (_superclusterCompleter.isCompleted || + _superclusterCompleter.isCanceled) { + setState(() { + _superclusterCompleter = CancelableCompleter(); + }); + } + + final newSupercluster = widget.indexBuilder.call( + createSupercluster, + SuperclusterParameters(config: _superclusterConfig, markers: markers), + ); + + _superclusterCompleter.complete(newSupercluster); + _superclusterCompleter.operation.value.then((supercluster) { + _onMarkersChange(); + _expandedClusterManager.clear(); + widget.popupOptions?.popupController.hidePopupsWhereSpec( + (popupSpec) => + popupSpec.namespace == SuperclusterLayer.popupNamespace, + ); + }); + + return newSupercluster; + }); + } + + @override + Widget build(BuildContext context) { + return _wrapWithPopupStateIfPopupsEnabled( + (popupState) => Stack( + children: [ + MobileLayerTransformer(child: _clustersAndMarkers()), + if (widget.popupOptions?.popupDisplayOptions != null) + PopupLayer( + popupDisplayOptions: widget.popupOptions!.popupDisplayOptions!, + ), + LoadingOverlay( + superclusterFuture: _superclusterCompleter.operation.value, + loadingOverlayBuilder: widget.loadingOverlayBuilder, + ), + ], + ), + ); + } + + Widget _wrapWithPopupStateIfPopupsEnabled( + Widget Function(PopupState? popupState) builder) { + if (widget.popupOptions == null) return builder(null); + + return InheritOrCreatePopupScope( + popupController: widget.popupOptions!.popupController, + builder: (context, popupState) { + _popupState = popupState; + if (widget.popupOptions!.selectedMarkerBuilder != null) { + context.watch(); + } + return builder(popupState); + }, + ); + } + + Widget _clustersAndMarkers() { + final paddedBounds = + widget.mapCamera.paddedMapBounds(widget.clusterWidgetSize); + + return FutureBuilder>( + future: _superclusterCompleter.operation.value, + builder: (context, snapshot) { + final supercluster = snapshot.data; + if (supercluster == null) return const SizedBox.shrink(); + + return Stack(children: [ + ..._buildClustersAndMarkers(supercluster, paddedBounds) + ]); + }, + ); + } + + Iterable _buildClustersAndMarkers( + Supercluster supercluster, + LatLngBounds paddedBounds, + ) sync* { + final selectedMarkerBuilder = + widget.popupOptions != null && _popupState!.selectedMarkers.isNotEmpty + ? widget.popupOptions!.selectedMarkerBuilder + : null; + final List> selectedLayerPoints = []; + final List> clusters = []; + + // Build non-selected markers first, queue the rest to build afterwards + // so they appear above these markers. + for (final layerElement in supercluster.search( + paddedBounds.west, + paddedBounds.south, + paddedBounds.east, + paddedBounds.north, + widget.mapCamera.zoom.ceil(), + )) { + if (layerElement is LayerCluster) { + clusters.add(layerElement); + continue; + } + layerElement as LayerPoint; + if (selectedMarkerBuilder != null && + _popupState!.selectedMarkers.contains(layerElement.originalPoint)) { + selectedLayerPoints.add(layerElement); + continue; + } + yield _buildMarker(layerElement); + } + + // Build selected markers. + for (final selectedLayerPoint in selectedLayerPoints) { + yield _buildMarker(selectedLayerPoint, selected: true); + } + + // Build non expanded clusters. + for (final cluster in clusters) { + if (_expandedClusterManager.contains(cluster)) continue; + yield _buildCluster(supercluster, cluster); + } + + // Build expanded clusters. + for (final expandedCluster in _expandedClusterManager.all) { + yield _buildExpandedCluster(expandedCluster); + } + } + + Widget _buildMarker( + LayerPoint mapPoint, { + bool selected = false, + }) { + final marker = mapPoint.originalPoint; + + final markerChild = !selected + ? marker.child + : widget.popupOptions!.selectedMarkerBuilder!(context, marker); + + return MarkerWidget( + mapCamera: widget.mapCamera, + marker: marker, + markerChild: markerChild, + onTap: () => _onMarkerTap(PopupSpecBuilder.forLayerPoint(mapPoint)), + ); + } + + Widget _buildCluster( + Supercluster supercluster, + LayerCluster cluster, + ) { + return ClusterWidget( + mapCamera: widget.mapCamera, + cluster: cluster, + builder: widget.builder, + onTap: () => _onClusterTap(supercluster, cluster), + size: widget.clusterWidgetSize, + alignment: widget.clusterAlignment, + ); + } + + Widget _buildExpandedCluster( + ExpandedCluster expandedCluster, + ) { + final selectedMarkerBuilder = widget.popupOptions?.selectedMarkerBuilder; + final Widget Function(BuildContext context, Marker marker) markerBuilder = + selectedMarkerBuilder == null + ? ((context, marker) => marker.child) + : ((context, marker) => + _popupState?.selectedMarkers.contains(marker) == true + ? selectedMarkerBuilder(context, marker) + : marker.child); + + return ExpandableClusterWidget( + mapCamera: widget.mapCamera, + expandedCluster: expandedCluster, + builder: widget.builder, + size: widget.clusterWidgetSize, + clusterAlignment: widget.clusterAlignment, + markerBuilder: markerBuilder, + onCollapse: () { + widget.popupOptions?.popupController + .hidePopupsOnlyFor(expandedCluster.markers.toList()); + _expandedClusterManager + .collapseThenRemove(expandedCluster.layerCluster); + }, + onMarkerTap: _onMarkerTap, + ); + } + + void _onClusterTap( + Supercluster supercluster, + LayerCluster layerCluster, + ) async { + if (layerCluster.highestZoom >= _superclusterConfig.maxZoom) { + await _moveMapIfNotAt( + layerCluster.latLng, + layerCluster.highestZoom.toDouble(), + ); + + final splayAnimation = _expandedClusterManager.putIfAbsent( + layerCluster, + () => ExpandedCluster( + vsync: this, + mapCamera: widget.mapCamera, + layerPoints: + supercluster.childrenOf(layerCluster).cast>(), + layerCluster: layerCluster, + clusterSplayDelegate: widget.clusterSplayDelegate, + ), + ); + if (splayAnimation != null) setState(() {}); + } else { + await _moveMapIfNotAt( + layerCluster.latLng, + layerCluster.highestZoom + 0.5, + ); + } + } + + FutureOr _moveMapIfNotAt( + LatLng center, + double zoom, { + FutureOr Function(LatLng center, double zoom)? moveMapOverride, + }) { + if (center == widget.mapCamera.center && zoom == widget.mapCamera.zoom) { + return Future.value(); + } + + final moveMap = moveMapOverride ?? + widget.moveMap ?? + (center, zoom) => widget.mapController.move(center, zoom); + + return moveMap.call(center, zoom); + } + + void _onMarkerTap(PopupSpec popupSpec) { + _selectMarker(popupSpec); + widget.onMarkerTap?.call(popupSpec.marker); + } + + void _selectMarker(PopupSpec popupSpec) { + if (widget.popupOptions != null) { + assert(_popupState != null); + + final popupOptions = widget.popupOptions!; + popupOptions.markerTapBehavior.apply( + popupSpec, + _popupState!, + popupOptions.popupController, + ); + + if (popupOptions.selectedMarkerBuilder != null) setState(() {}); + } + } + + void _onMove() { + final zoom = widget.mapCamera.zoom.ceil(); + + if (_lastMovementZoom == null || zoom < _lastMovementZoom!) { + _expandedClusterManager.removeIfZoomGreaterThan(zoom); + } + + _lastMovementZoom = zoom; + } + + void _onMarkersChange() { + _superclusterCompleter.operation.value.then((supercluster) { + final aggregatedClusterData = widget.calculateAggregatedClusterData + ? supercluster.aggregatedClusterData() + : null; + final clusterData = aggregatedClusterData == null + ? null + : (aggregatedClusterData as ClusterData); + final superclusterState = SuperclusterStateImpl( + aggregatedClusterData: clusterData, + supercluster: supercluster, + ); + InheritedSuperclusterScope.of(context, listen: false) + .setSuperclusterState(superclusterState); + }); + } + + void _moveToMarker({ + required MarkerMatcher markerMatcher, + required bool showPopup, + required FutureOr Function(LatLng center, double zoom)? moveMap, + }) async { + // Create a shorthand for the map movement function. + move(center, zoom) => + _moveMapIfNotAt(center, zoom, moveMapOverride: moveMap); + + /// Find the Marker's LayerPoint. + final supercluster = await _superclusterCompleter.operation.value; + LayerPoint? foundLayerPoint = + supercluster.layerPointMatching(markerMatcher); + if (foundLayerPoint == null) return; + + final markerInSplayCluster = + _superclusterConfig.maxZoom < foundLayerPoint.lowestZoom; + if (markerInSplayCluster) { + await _moveToSplayClusterMarker( + supercluster: supercluster, + layerPoint: foundLayerPoint, + move: move, + showPopup: showPopup, + ); + } else { + await move( + foundLayerPoint.latLng, + max(foundLayerPoint.lowestZoom.toDouble(), widget.mapCamera.zoom), + ); + if (showPopup) { + _selectMarker(PopupSpecBuilder.forLayerPoint(foundLayerPoint)); + } + } + } + + /// Move to Marker inside splay cluster. There are three possibilities: + /// 1. There is already an ExpandedCluster containing the Marker and it + /// remains expanded during movement. + /// 2. There is already an ExpandedCluster and it closes during movement so + /// we must create a new one once movement finishes. + /// 3. There is NOT already an ExpandedCluster, we should create one and add + /// it once movement finishes. + Future _moveToSplayClusterMarker({ + required Supercluster supercluster, + required LayerPoint layerPoint, + required FutureOr Function(LatLng center, double zoom) move, + required bool showPopup, + }) async { + // Find the parent. + final layerCluster = supercluster.parentOf(layerPoint)!; + + // Shorthand for creating an ExpandedCluster. + createExpandedCluster() => ExpandedCluster( + vsync: this, + mapCamera: widget.mapCamera, + layerPoints: + supercluster.childrenOf(layerCluster).cast>(), + layerCluster: layerCluster, + clusterSplayDelegate: widget.clusterSplayDelegate, + ); + + // Find or create the marker's ExpandedCluster and use it to find the + // DisplacedMarker. + final expandedClusterBeforeMovement = + _expandedClusterManager.forLayerCluster(layerCluster); + final createdExpandedCluster = + expandedClusterBeforeMovement != null ? null : createExpandedCluster(); + final displacedMarker = + (expandedClusterBeforeMovement ?? createdExpandedCluster)! + .markersToDisplacedMarkers[layerPoint.originalPoint]!; + + // Move to the DisplacedMarker. + await move( + displacedMarker.displacedPoint, + max(widget.mapCamera.zoom, layerPoint.lowestZoom - 0.99999), + ); + + // Determine the ExpandedCluster after movement, either: + // 1. We created one (without adding it to ExpandedClusterManager) + // because there was none before movement. + // 2. Movement may have caused the ExpandedCluster to be removed in which + // case we create a new one. + final splayAnimation = _expandedClusterManager.putIfAbsent( + layerCluster, + () => createdExpandedCluster ?? createExpandedCluster(), + ); + if (splayAnimation != null) { + if (!mounted) return; + setState(() {}); + await splayAnimation; + } + + if (showPopup) { + final popupSpec = PopupSpecBuilder.forDisplacedMarker( + displacedMarker, + layerCluster.highestZoom, + ); + _selectMarker(popupSpec); + } + } + + void _onSuperclusterEvent(SuperclusterEvent event) async { + switch (event) { + case AddMarkerEvent(): + _superclusterCompleter.operation.then((supercluster) { + (supercluster as SuperclusterMutable).add(event.marker); + _removeExpandedClustersOfRemovedClusters(supercluster); + _onMarkersChange(); + }); + case AddAllMarkerEvent(): + _superclusterCompleter.operation.then((supercluster) { + (supercluster as SuperclusterMutable).addAll(event.markers); + _removeExpandedClustersOfRemovedClusters(supercluster); + _onMarkersChange(); + }); + case RemoveMarkerEvent(): + _superclusterCompleter.operation.then((supercluster) { + final removed = (supercluster as SuperclusterMutable) + .remove(event.marker); + + if (removed) { + _removeExpandedClustersOfRemovedClusters(supercluster); + _hidePopupsInNamespaceFor([event.marker]); + _onMarkersChange(); + } + }); + case RemoveAllMarkerEvent(): + _superclusterCompleter.operation.then((supercluster) { + final removed = (supercluster as SuperclusterMutable) + .removeAll(event.markers); + if (removed) { + _removeExpandedClustersOfRemovedClusters(supercluster); + _hidePopupsInNamespaceFor(event.markers); + _onMarkersChange(); + } + }); + case ReplaceAllMarkerEvent(): + _initializeSupercluster(Future.value(event.markers)); + _expandedClusterManager.clear(); + case ModifyMarkerEvent(): + _superclusterCompleter.operation.then((supercluster) { + final modified = + (supercluster as SuperclusterMutable).modifyPointData( + event.oldMarker, + event.newMarker, + updateParentClusters: event.updateParentClusters, + ); + + if (modified) { + _modifyDisplacedMarker(event.oldMarker, event.newMarker); + _removeExpandedClustersOfRemovedClusters(supercluster); + _hidePopupsInNamespaceFor([event.oldMarker]); + _onMarkersChange(); + } + }); + case CollapseSplayedClustersEvent(): + _expandedClusterManager.collapseThenRemoveAll(); + case ShowPopupsAlsoForEvent(): + if (widget.popupOptions == null) return; + widget.popupOptions?.popupController.showPopupsAlsoForSpecs( + PopupSpecBuilder.buildList( + supercluster: await _superclusterCompleter.operation.value, + zoom: widget.mapCamera.zoom.ceil(), + maxZoom: _superclusterConfig.maxZoom, + markers: event.markers, + expandedClusters: _expandedClusterManager.all, + ), + disableAnimation: event.disableAnimation, + ); + case MoveToMarkerEvent(): + _moveToMarker( + markerMatcher: event.markerMatcher, + showPopup: event.showPopup, + moveMap: event.moveMap, + ); + case ShowPopupsOnlyForEvent(): + if (widget.popupOptions == null) return; + widget.popupOptions?.popupController.showPopupsOnlyForSpecs( + PopupSpecBuilder.buildList( + supercluster: await _superclusterCompleter.operation.value, + zoom: widget.mapCamera.zoom.ceil(), + maxZoom: _superclusterConfig.maxZoom, + markers: event.markers, + expandedClusters: _expandedClusterManager.all, + ), + disableAnimation: event.disableAnimation, + ); + case HideAllPopupsEvent(): + if (widget.popupOptions == null) return; + widget.popupOptions?.popupController.hideAllPopups( + disableAnimation: event.disableAnimation, + ); + case HidePopupsWhereEvent(): + if (widget.popupOptions == null) return; + widget.popupOptions?.popupController.hidePopupsWhere( + event.test, + disableAnimation: event.disableAnimation, + ); + case HidePopupsOnlyForEvent(): + if (widget.popupOptions == null) return; + widget.popupOptions?.popupController.hidePopupsOnlyFor( + event.markers, + disableAnimation: event.disableAnimation, + ); + case TogglePopupEvent(): + if (widget.popupOptions == null) return; + final popupSpec = PopupSpecBuilder.build( + supercluster: await _superclusterCompleter.operation.value, + zoom: widget.mapCamera.zoom.ceil(), + maxZoom: _superclusterConfig.maxZoom, + marker: event.marker, + expandedClusters: _expandedClusterManager.all, + ); + if (popupSpec == null) return; + widget.popupOptions?.popupController.togglePopupSpec( + popupSpec, + disableAnimation: event.disableAnimation, + ); + } + + setState(() {}); + } + + void _removeExpandedClustersOfRemovedClusters( + SuperclusterMutable supercluster) { + final toRemove = _expandedClusterManager.all + .where((e) => !supercluster.containsElement(e.layerCluster)); + if (toRemove.isNotEmpty) { + _expandedClusterManager.removeAllImmediately(toRemove); + } + } + + void _hidePopupsInNamespaceFor(Iterable markers) { + widget.popupOptions?.popupController.hidePopupsWhereSpec((spec) => + spec.namespace == SuperclusterLayer.popupNamespace && + markers.contains(spec.marker)); + } + + void _modifyDisplacedMarker(Marker oldMarker, Marker newMarker) async { + final supercluster = await _superclusterCompleter.operation.value; + LayerPoint? foundLayerPoint = + supercluster.layerPointMatching(MarkerMatcher.equalsMarker(oldMarker)); + if (foundLayerPoint == null) return; + + // Is the marker part of a splay cluster? + if (_superclusterConfig.maxZoom < foundLayerPoint.lowestZoom) { + // Find the parent. + final layerCluster = supercluster.parentOf(foundLayerPoint)!; + + // Find the marker's ExpandedCluster + // and use it to find the DisplacedMarker. + final expandedClusterBeforeMovement = + _expandedClusterManager.forLayerCluster(layerCluster); + + if (expandedClusterBeforeMovement != null) { + List displacedMarkers = + expandedClusterBeforeMovement.displacedMarkers; + for (var i = 0; i < displacedMarkers.length; i++) { + if (displacedMarkers[i].marker == oldMarker) { + // exchange the DisplacedMarker + // and make the new marker visible in the map + _expandedClusterManager + .forLayerCluster(layerCluster)! + .displacedMarkers[i] = + DisplacedMarker( + marker: newMarker, + displacedPoint: displacedMarkers[i].displacedPoint); + } + } + } + } + } +} diff --git a/lib/src/layer/supercluster_parameters.dart b/lib/src/layer/supercluster_parameters.dart new file mode 100644 index 0000000..d648abf --- /dev/null +++ b/lib/src/layer/supercluster_parameters.dart @@ -0,0 +1,32 @@ +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_config.dart'; +import 'package:supercluster/supercluster.dart'; + +class SuperclusterParameters implements SuperclusterConfig { + final SuperclusterConfig config; + final List markers; + + SuperclusterParameters({ + required this.config, + required this.markers, + }); + + @override + ClusterDataBase Function(Marker marker)? get innerClusterDataExtractor => + config.innerClusterDataExtractor; + + @override + bool get isMutableSupercluster => config.isMutableSupercluster; + + @override + int get maxClusterRadius => config.maxClusterRadius; + + @override + int get maxZoom => config.maxZoom; + + @override + int get minZoom => config.minZoom; + + @override + int? get minimumClusterSize => config.minimumClusterSize; +} diff --git a/lib/src/layer_element_extension.dart b/lib/src/layer_element_extension.dart index ea5b6f0..b145153 100644 --- a/lib/src/layer_element_extension.dart +++ b/lib/src/layer_element_extension.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:supercluster/supercluster.dart'; diff --git a/lib/src/marker_extension.dart b/lib/src/marker_extension.dart deleted file mode 100644 index 3074485..0000000 --- a/lib/src/marker_extension.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter_map/plugin_api.dart'; - -extension MarkerExtension on Marker { - Anchor get anchor => Anchor.fromPos( - anchorPos ?? AnchorPos.align(AnchorAlign.center), - width, - height, - ); -} diff --git a/lib/src/options/index_builder.dart b/lib/src/options/index_builder.dart index cd3b056..6d64b4d 100644 --- a/lib/src/options/index_builder.dart +++ b/lib/src/options/index_builder.dart @@ -1,13 +1,15 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; -import 'package:flutter_map_supercluster/src/layer/supercluster_config.dart'; +import 'package:flutter_map_supercluster/src/layer/supercluster_parameters.dart'; /// A callback used to create a supercluster index. See [IndexBuilders] for /// predefined builders and guidelines on which one to use. typedef IndexBuilder = Future> Function( - Supercluster Function(SuperclusterConfig config) createSupercluster, - SuperclusterConfig superclusterConfig, + Supercluster Function( + SuperclusterParameters parameters, + ) createSupercluster, + SuperclusterParameters superclusterParameters, ); /// Predefined builders for creating a supercluster index. The following is a @@ -32,9 +34,9 @@ class IndexBuilders { /// Creates the supercluster in the root isolate. This is the best choice if /// you don't experience jank when creating the index. - static IndexBuilder rootIsolate = - ((createSupercluster, superclusterConfig) async => - createSupercluster(superclusterConfig)); + static final IndexBuilder rootIsolate = + ((createSupercluster, superclusterParameters) async => + createSupercluster(superclusterParameters)); /// Creates the supercluster in a separate isolate using flutter's [compute] /// method and then replaces the copied Marker instances in the supercluster @@ -43,10 +45,10 @@ class IndexBuilders { /// down index creation for large numbers of Markers. This is unnecessary if /// you extend Marker and override its hashCode/== methods, in which case you /// should use [computeWithCopiedMarkers]. - static IndexBuilder computeWithOriginalMarkers = - ((createSupercluster, superclusterConfig) async => - compute(createSupercluster, superclusterConfig).then((supercluster) => - supercluster..replacePoints(superclusterConfig.markers))); + static final IndexBuilder computeWithOriginalMarkers = ((createSupercluster, + superclusterParameters) async => + compute(createSupercluster, superclusterParameters).then((supercluster) => + supercluster..replacePoints(superclusterParameters.markers))); /// Creates the supercluster in a separate isolate using flutter's [compute] /// method. Dart creates copies of objects when running code in a separate @@ -58,9 +60,9 @@ class IndexBuilders { /// /// Failure to override hashCode/== will prevent popups from working properly /// for splayed clusters and may cause other issues. - static IndexBuilder computeWithCopiedMarkers = - ((createSupercluster, superclusterConfig) async => - compute(createSupercluster, superclusterConfig)); + static final IndexBuilder computeWithCopiedMarkers = + ((createSupercluster, superclusterParameters) async => + compute(createSupercluster, superclusterParameters)); /// Calls the provided [indexBuilder] before replacing the resulting index's /// markers with the original markers. This is only necessary when the @@ -69,8 +71,8 @@ class IndexBuilders { /// the index in a separate isolate or the Markers override hashCode/== you /// should just use a plain IndexBuilder instance. static IndexBuilder customWithOriginalMarkers(IndexBuilder indexBuilder) => - ((createSupercluster, superclusterConfig) async => indexBuilder - .call(createSupercluster, superclusterConfig) + ((createSupercluster, superclusterParameters) async => indexBuilder + .call(createSupercluster, superclusterParameters) .then((supercluster) => - supercluster..replacePoints(superclusterConfig.markers))); + supercluster..replacePoints(superclusterParameters.markers))); } diff --git a/lib/src/options/popup_options.dart b/lib/src/options/popup_options.dart index cd3f072..bcf01fc 100644 --- a/lib/src/options/popup_options.dart +++ b/lib/src/options/popup_options.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/extension_api.dart'; import 'package:flutter_map_supercluster/src/options/popup_options_impl.dart'; diff --git a/lib/src/options/popup_options_impl.dart b/lib/src/options/popup_options_impl.dart index 81d45d9..cb49ee2 100644 --- a/lib/src/options/popup_options_impl.dart +++ b/lib/src/options/popup_options_impl.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/extension_api.dart'; import 'package:flutter_map_supercluster/src/options/popup_options.dart'; diff --git a/lib/src/splay/cluster_splay_delegate.dart b/lib/src/splay/cluster_splay_delegate.dart index c2a7e0e..24dbb1c 100644 --- a/lib/src/splay/cluster_splay_delegate.dart +++ b/lib/src/splay/cluster_splay_delegate.dart @@ -1,5 +1,7 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/layer/supercluster_layer.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker_offset.dart'; @@ -28,16 +30,16 @@ abstract class ClusterSplayDelegate { List displaceMarkers( List markers, { required LatLng clusterPosition, - required CustomPoint Function(LatLng latLng) project, - required LatLng Function(CustomPoint point) unproject, + required Point Function(LatLng latLng) project, + required LatLng Function(Point point) unproject, }); /// Calculate the marker offsets at the given [animationProgress]. List displacedMarkerOffsets( List displacedMarkers, double animationProgress, - CustomPoint Function(LatLng point) getPixelOffset, - CustomPoint clusterPosition, + Point Function(LatLng point) getPixelOffset, + Point clusterPosition, ); /// Create an optional decoration such as lines from the markers to the diff --git a/lib/src/splay/displaced_marker.dart b/lib/src/splay/displaced_marker.dart index dec7b8d..7f6eb3d 100644 --- a/lib/src/splay/displaced_marker.dart +++ b/lib/src/splay/displaced_marker.dart @@ -1,9 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; class DisplacedMarker { - static final anchorPos = AnchorPos.align(AnchorAlign.center); + static const alignment = Alignment.center; final Marker marker; final LatLng displacedPoint; @@ -15,10 +15,4 @@ class DisplacedMarker { LatLng get originalPoint => marker.point; static const AlignmentGeometry rotateAlignment = Alignment.center; - - Anchor get anchor => Anchor.fromPos( - anchorPos, - marker.width, - marker.height, - ); } diff --git a/lib/src/splay/displaced_marker_offset.dart b/lib/src/splay/displaced_marker_offset.dart index 4f99366..7915b0a 100644 --- a/lib/src/splay/displaced_marker_offset.dart +++ b/lib/src/splay/displaced_marker_offset.dart @@ -1,12 +1,14 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'dart:math'; + +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker.dart'; /// Pixel positions for a [Marker] which has been displaced from its original /// position. class DisplacedMarkerOffset { final DisplacedMarker displacedMarker; - final CustomPoint displacedOffset; - final CustomPoint originalOffset; + final Point displacedOffset; + final Point originalOffset; const DisplacedMarkerOffset({ required this.displacedMarker, diff --git a/lib/src/splay/popup_spec_builder.dart b/lib/src/splay/popup_spec_builder.dart index 64ef4ee..b77932e 100644 --- a/lib/src/splay/popup_spec_builder.dart +++ b/lib/src/splay/popup_spec_builder.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/extension_api.dart'; import 'package:flutter_map_supercluster/src/layer/supercluster_layer.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker.dart'; @@ -14,9 +14,7 @@ class PopupSpecBuilder { namespace: SuperclusterLayer.popupNamespace, marker: displacedMarker.marker, markerPointOverride: displacedMarker.displacedPoint, - markerRotateAlignmentOveride: DisplacedMarker.rotateAlignment, - removeMarkerRotateOrigin: true, - markerAnchorPosOverride: DisplacedMarker.anchorPos, + markerAlignmentOverride: DisplacedMarker.alignment, removeIfZoomLessThan: lowestZoom, ); diff --git a/lib/src/splay/spread_cluster_splay_delegate.dart b/lib/src/splay/spread_cluster_splay_delegate.dart index 218f820..0b3cf7d 100644 --- a/lib/src/splay/spread_cluster_splay_delegate.dart +++ b/lib/src/splay/spread_cluster_splay_delegate.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/layer/supercluster_layer.dart'; import 'package:flutter_map_supercluster/src/splay/cluster_splay_delegate.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker.dart'; @@ -26,6 +26,7 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { final SplayClusterWidgetBuilder? builder; final double clusterOpacity; final SplayLineOptions? splayLineOptions; + final double distanceIncrement; const SpreadClusterSplayDelegate({ /// Duration of the splay animation. @@ -48,6 +49,9 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { // Optional builder used for the expanded cluster. If provided // [clusterOpacity] has no affect. this.builder, + + // the displacement radius is increased by this number depending on the markers count + this.distanceIncrement = 4.0, }); @override @@ -84,8 +88,8 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { List displaceMarkers( List markers, { required LatLng clusterPosition, - required CustomPoint Function(LatLng latLng) project, - required LatLng Function(CustomPoint point) unproject, + required Point Function(LatLng latLng) project, + required LatLng Function(Point point) unproject, }) { final markersWithAngles = markers .map( @@ -97,7 +101,9 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { .toList() ..sort((a, b) => a.angle.compareTo(b.angle)); - final circleOffsets = _clockwiseCircle(distance, markersWithAngles.length); + final circleOffsets = _clockwiseCircle( + distance + (distanceIncrement * markersWithAngles.length), + markersWithAngles.length); final clusterPointAtMaxZoom = project(clusterPosition); final result = []; @@ -118,8 +124,8 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { List displacedMarkerOffsets( List displacedMarkers, double animationProgress, - CustomPoint Function(LatLng point) getPixelOffset, - CustomPoint clusterPosition, + Point Function(LatLng point) getPixelOffset, + Point clusterPosition, ) { return displacedMarkers .map( @@ -140,8 +146,10 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { splayLineOptions == null ? null : _DisplacedMarkerSplay( - width: distance * 2.0, - height: distance * 2.0, + width: distance + + (distanceIncrement * displacedMarkerOffsets.length) * 2.0, + height: distance + + (distanceIncrement * displacedMarkerOffsets.length) * 2.0, displacedMarkerOffsets: displacedMarkerOffsets, splayLineOptions: splayLineOptions!, ); @@ -156,13 +164,13 @@ class SpreadClusterSplayDelegate extends ClusterSplayDelegate { return atan2(y, x); } - static List _clockwiseCircle(double radius, int count) { + static List _clockwiseCircle(double radius, int count) { final angleStep = pi2 / count; - return List.generate(count, (index) { + return List.generate(count, (index) { final angle = circleStartAngle + index * angleStep; - return CustomPoint( + return Point( radius * cos(angle), radius * sin(angle), ); diff --git a/lib/src/state/inherit_or_create_supercluster_scope.dart b/lib/src/state/inherit_or_create_supercluster_scope.dart new file mode 100644 index 0000000..2f20625 --- /dev/null +++ b/lib/src/state/inherit_or_create_supercluster_scope.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_supercluster/src/state/inherited_supercluster_scope.dart'; +import 'package:flutter_map_supercluster/src/state/supercluster_scope.dart'; + +class InheritOrCreateSuperclusterScope extends StatelessWidget { + final Widget child; + const InheritOrCreateSuperclusterScope({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final superclusterScopeState = + InheritedSuperclusterScope.maybeOf(context, listen: false); + + return superclusterScopeState != null + ? child + : SuperclusterScope(child: child); + } +} diff --git a/lib/src/state/inherited_supercluster_scope.dart b/lib/src/state/inherited_supercluster_scope.dart new file mode 100644 index 0000000..64999b2 --- /dev/null +++ b/lib/src/state/inherited_supercluster_scope.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_supercluster/src/state/supercluster_state.dart'; + +class InheritedSuperclusterScope extends InheritedWidget { + final SuperclusterState superclusterState; + final void Function(SuperclusterStateImpl stateImpl) setSuperclusterState; + + const InheritedSuperclusterScope({ + super.key, + required this.superclusterState, + required this.setSuperclusterState, + required super.child, + }); + + static InheritedSuperclusterScope? maybeOf( + BuildContext context, { + bool listen = true, + }) { + if (listen) { + return context + .dependOnInheritedWidgetOfExactType(); + } else { + return context + .getInheritedWidgetOfExactType(); + } + } + + static InheritedSuperclusterScope of( + BuildContext context, { + bool listen = true, + }) { + final result = maybeOf(context, listen: listen); + assert( + result != null, 'No InheritedSuperclusterScopeState found in context.'); + return result!; + } + + @override + bool updateShouldNotify(InheritedSuperclusterScope oldWidget) => + oldWidget.superclusterState != superclusterState; +} diff --git a/lib/src/state/marker_supercluster.dart b/lib/src/state/marker_supercluster.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/state/supercluster_scope.dart b/lib/src/state/supercluster_scope.dart new file mode 100644 index 0000000..9a395f3 --- /dev/null +++ b/lib/src/state/supercluster_scope.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_supercluster/src/state/inherited_supercluster_scope.dart'; +import 'package:flutter_map_supercluster/src/state/supercluster_state.dart'; + +class SuperclusterScope extends StatefulWidget { + final Widget child; + + const SuperclusterScope({ + super.key, + required this.child, + }); + + @override + State createState() => _SuperclusterScopeState(); +} + +class _SuperclusterScopeState extends State { + late SuperclusterState superclusterState; + + @override + void initState() { + super.initState(); + superclusterState = const SuperclusterStateImpl( + supercluster: null, + aggregatedClusterData: null, + ); + } + + @override + Widget build(BuildContext context) { + return InheritedSuperclusterScope( + superclusterState: superclusterState, + setSuperclusterState: (superclusterState) => setState(() { + this.superclusterState = superclusterState; + }), + child: widget.child, + ); + } +} diff --git a/lib/src/state/supercluster_state.dart b/lib/src/state/supercluster_state.dart new file mode 100644 index 0000000..fbf3a71 --- /dev/null +++ b/lib/src/state/supercluster_state.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/flutter_map_supercluster.dart'; +import 'package:flutter_map_supercluster/src/state/inherited_supercluster_scope.dart'; + +abstract class SuperclusterState { + bool get loading; + + ClusterData? get aggregatedClusterData; + + static SuperclusterState? maybeOf( + BuildContext context, { + bool listen = true, + }) { + return InheritedSuperclusterScope.maybeOf( + context, + listen: listen, + )?.superclusterState; + } + + static SuperclusterState of( + BuildContext context, { + bool listen = true, + }) { + final SuperclusterState? result = maybeOf(context, listen: listen); + assert(result != null, 'No SuperclusterState found in context.'); + return result!; + } +} + +class SuperclusterStateImpl extends Equatable implements SuperclusterState { + @override + final ClusterData? aggregatedClusterData; + + final Supercluster? _supercluster; + + @override + bool get loading => _supercluster == null; + + const SuperclusterStateImpl({ + required this.aggregatedClusterData, + required Supercluster? supercluster, + }) : _supercluster = supercluster; + + @override + List get props => [loading, aggregatedClusterData]; +} diff --git a/lib/src/supercluster_extension.dart b/lib/src/supercluster_extension.dart index b48f09c..97a8e86 100644 --- a/lib/src/supercluster_extension.dart +++ b/lib/src/supercluster_extension.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/controller/marker_matcher.dart'; import 'package:supercluster/supercluster.dart'; diff --git a/lib/src/widget/cluster_widget.dart b/lib/src/widget/cluster_widget.dart index 9a383fd..28620b9 100644 --- a/lib/src/widget/cluster_widget.dart +++ b/lib/src/widget/cluster_widget.dart @@ -1,9 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_supercluster/src/layer/anchor_util.dart'; -import 'package:flutter_map_supercluster/src/layer/flutter_map_state_extension.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/src/layer/alignment_util.dart'; +import 'package:flutter_map_supercluster/src/layer/map_camera_extension.dart'; import 'package:flutter_map_supercluster/src/layer_element_extension.dart'; import 'package:supercluster/supercluster.dart'; @@ -17,22 +17,23 @@ class ClusterWidget extends StatelessWidget { final Size size; final Point position; final double mapRotationRad; + final Alignment alignment; ClusterWidget({ Key? key, - required FlutterMapState mapState, + required MapCamera mapCamera, required this.cluster, required this.builder, required this.onTap, required this.size, - required AnchorPos? anchorPos, + required this.alignment, }) : position = _getClusterPixel( - mapState, + mapCamera, cluster, - anchorPos, + alignment, size, ), - mapRotationRad = mapState.rotationRad, + mapRotationRad = mapCamera.rotationRad, super(key: ValueKey(cluster.uuid)); @override @@ -61,16 +62,16 @@ class ClusterWidget extends StatelessWidget { } static Point _getClusterPixel( - FlutterMapState mapState, + MapCamera mapCamera, LayerCluster cluster, - AnchorPos? anchorPos, + Alignment alignment, Size size, ) { - return AnchorUtil.removeClusterAnchor( - mapState.getPixelOffset(cluster.latLng), - cluster, - anchorPos, - size, + return AlignmentUtil.applyAlignment( + mapCamera.getPixelOffset(cluster.latLng), + size.width, + size.height, + alignment, ); } } diff --git a/lib/src/widget/expandable_cluster_widget.dart b/lib/src/widget/expandable_cluster_widget.dart index b2082fc..64cc09d 100644 --- a/lib/src/widget/expandable_cluster_widget.dart +++ b/lib/src/widget/expandable_cluster_widget.dart @@ -1,7 +1,9 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/extension_api.dart'; -import 'package:flutter_map_supercluster/src/layer/flutter_map_state_extension.dart'; +import 'package:flutter_map_supercluster/src/layer/map_camera_extension.dart'; import 'package:flutter_map_supercluster/src/layer_element_extension.dart'; import 'package:flutter_map_supercluster/src/splay/popup_spec_builder.dart'; import 'package:flutter_map_supercluster/src/widget/cluster_widget.dart'; @@ -11,28 +13,28 @@ import 'package:flutter_map_supercluster/src/widget/marker_widget.dart'; import '../layer/supercluster_layer.dart'; class ExpandableClusterWidget extends StatelessWidget { - final FlutterMapState mapState; + final MapCamera mapCamera; final ExpandedCluster expandedCluster; final ClusterWidgetBuilder builder; final Size size; - final AnchorPos? anchorPos; + final Alignment clusterAlignment; final Widget Function(BuildContext, Marker) markerBuilder; final void Function(PopupSpec popupSpec) onMarkerTap; final VoidCallback onCollapse; - final CustomPoint clusterPixelPosition; + final Point clusterPixelPosition; ExpandableClusterWidget({ Key? key, - required this.mapState, + required this.mapCamera, required this.expandedCluster, required this.builder, required this.size, - required this.anchorPos, + required this.clusterAlignment, required this.markerBuilder, required this.onMarkerTap, required this.onCollapse, }) : clusterPixelPosition = - mapState.getPixelOffset(expandedCluster.layerCluster.latLng), + mapCamera.getPixelOffset(expandedCluster.layerCluster.latLng), super(key: ValueKey('expandable-${expandedCluster.layerCluster.uuid}')); @override @@ -41,7 +43,7 @@ class ExpandableClusterWidget extends StatelessWidget { animation: expandedCluster.animation, builder: (context, _) { final displacedMarkerOffsets = expandedCluster.displacedMarkerOffsets( - mapState, + mapCamera, clusterPixelPosition, ); final splayDecoration = expandedCluster.splayDecoration( @@ -63,7 +65,7 @@ class ExpandableClusterWidget extends StatelessWidget { (offset) => MarkerWidget.displaced( displacedMarker: offset.displacedMarker, position: clusterPixelPosition + offset.displacedOffset, - markerBuilder: (context) => markerBuilder( + markerChild: markerBuilder( context, offset.displacedMarker.marker, ), @@ -73,17 +75,17 @@ class ExpandableClusterWidget extends StatelessWidget { expandedCluster.minimumVisibleZoom, ), ), - mapRotationRad: mapState.rotationRad, + mapRotationRad: mapCamera.rotationRad, ), ), ClusterWidget( - mapState: mapState, + mapCamera: mapCamera, cluster: expandedCluster.layerCluster, builder: (context, latLng, count, data) => expandedCluster.buildCluster(context, builder), onTap: expandedCluster.isExpanded ? onCollapse : () {}, size: size, - anchorPos: anchorPos, + alignment: clusterAlignment, ), ], ), diff --git a/lib/src/widget/expanded_cluster.dart b/lib/src/widget/expanded_cluster.dart index b7f5641..1c2316a 100644 --- a/lib/src/widget/expanded_cluster.dart +++ b/lib/src/widget/expanded_cluster.dart @@ -1,9 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_supercluster/src/layer/cluster_data.dart'; -import 'package:flutter_map_supercluster/src/layer/flutter_map_state_extension.dart'; +import 'package:flutter_map_supercluster/src/layer/map_camera_extension.dart'; import 'package:flutter_map_supercluster/src/layer/supercluster_layer.dart'; import 'package:flutter_map_supercluster/src/layer_element_extension.dart'; import 'package:flutter_map_supercluster/src/splay/cluster_splay_delegate.dart'; @@ -26,7 +26,7 @@ class ExpandedCluster { ExpandedCluster({ required TickerProvider vsync, required this.layerCluster, - required FlutterMapState mapState, + required MapCamera mapCamera, required List> layerPoints, required this.clusterSplayDelegate, }) : clusterData = layerCluster.clusterData as ClusterData, @@ -38,9 +38,9 @@ class ExpandedCluster { layerPoints.map((e) => e.originalPoint).toList(), clusterPosition: layerCluster.latLng, project: (latLng) => - mapState.project(latLng, layerCluster.highestZoom.toDouble()), + mapCamera.project(latLng, layerCluster.highestZoom.toDouble()), unproject: (point) => - mapState.unproject(point, layerCluster.highestZoom.toDouble()), + mapCamera.unproject(point, layerCluster.highestZoom.toDouble()), ), maxMarkerSize = layerPoints.fold( Size.zero, @@ -66,13 +66,13 @@ class ExpandedCluster { int get minimumVisibleZoom => layerCluster.highestZoom; List displacedMarkerOffsets( - FlutterMapState mapState, - CustomPoint clusterPosition, + MapCamera mapCamera, + Point clusterPosition, ) => clusterSplayDelegate.displacedMarkerOffsets( displacedMarkers, animation.value, - mapState.getPixelOffset, + mapCamera.getPixelOffset, clusterPosition, ); diff --git a/lib/src/widget/marker_widget.dart b/lib/src/widget/marker_widget.dart index 3603f1d..64e0b9a 100644 --- a/lib/src/widget/marker_widget.dart +++ b/lib/src/widget/marker_widget.dart @@ -1,57 +1,48 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_supercluster/src/layer/anchor_util.dart'; -import 'package:flutter_map_supercluster/src/layer/flutter_map_state_extension.dart'; -import 'package:flutter_map_supercluster/src/marker_extension.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_supercluster/src/layer/alignment_util.dart'; +import 'package:flutter_map_supercluster/src/layer/map_camera_extension.dart'; import 'package:flutter_map_supercluster/src/splay/displaced_marker.dart'; class MarkerWidget extends StatelessWidget { final Marker marker; - final WidgetBuilder markerBuilder; + final Widget markerChild; final VoidCallback onTap; final Point position; final double mapRotationRad; - final AlignmentGeometry? rotateAlignment; - final bool removeRotateOrigin; - MarkerWidget({ super.key, - required FlutterMapState mapState, + required MapCamera mapCamera, required this.marker, - required this.markerBuilder, + required this.markerChild, required this.onTap, - }) : mapRotationRad = mapState.rotationRad, - position = _getMapPointPixel(mapState, marker), - rotateAlignment = marker.rotateAlignment, - removeRotateOrigin = false; + }) : mapRotationRad = mapCamera.rotationRad, + position = _getMapPointPixel(mapCamera, marker); MarkerWidget.displaced({ - Key? key, + super.key, required DisplacedMarker displacedMarker, - required CustomPoint position, - required this.markerBuilder, + required Point position, + required this.markerChild, required this.onTap, required this.mapRotationRad, }) : marker = displacedMarker.marker, - position = AnchorUtil.removeAnchor( + position = AlignmentUtil.applyAlignment( position, displacedMarker.marker.width, displacedMarker.marker.height, - displacedMarker.anchor, - ), - rotateAlignment = DisplacedMarker.rotateAlignment, - removeRotateOrigin = true, - super(key: key); + DisplacedMarker.alignment, + ); @override Widget build(BuildContext context) { final child = GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, - child: markerBuilder(context), + child: markerChild, ); return Positioned( @@ -64,22 +55,20 @@ class MarkerWidget extends StatelessWidget { ? child : Transform.rotate( angle: -mapRotationRad, - origin: removeRotateOrigin ? null : marker.rotateOrigin, - alignment: rotateAlignment, child: child, ), ); } static Point _getMapPointPixel( - FlutterMapState mapState, + MapCamera mapCamera, Marker marker, ) { - return AnchorUtil.removeAnchor( - mapState.getPixelOffset(marker.point), + return AlignmentUtil.applyAlignment( + mapCamera.getPixelOffset(marker.point), marker.width, marker.height, - marker.anchor, + marker.alignment ?? Alignment.center, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 8c1cca8..8670b02 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,10 @@ name: flutter_map_supercluster description: Very fast Marker clustering for flutter_map. Supports efficient adding/removing of Markers. -version: 4.3.0 - +version: 5.0.0-dev.1 homepage: https://github.com/rorystephenson/flutter_map_supercluster +topics: + - flutter-map + - cluster environment: sdk: ">=3.0.0 <4.0.0" @@ -13,13 +15,13 @@ dependencies: equatable: ^2.0.5 flutter: sdk: flutter - flutter_map: ^5.0.0 - flutter_map_marker_popup: ^5.2.0 + flutter_map: ^7.0.2 + flutter_map_marker_popup: ^7.0.0 latlong2: ^0.9.0 - provider: ^6.0.5 - supercluster: ^2.4.0 + provider: ^6.1.1 + supercluster: ^3.0.1 dev_dependencies: - flutter_lints: ^2.0.2 + flutter_lints: ^3.0.1 flutter_test: - sdk: flutter + sdk: flutter \ No newline at end of file