From b2b8e9a946af8b43537c8f75931308cd7afadf25 Mon Sep 17 00:00:00 2001 From: ddurzo Date: Wed, 13 Oct 2021 17:07:13 +0200 Subject: [PATCH 1/6] - add parameter to block update on set/add items to enable the possibility to make some calculations on clustered markers - expose inflateBounds --- lib/src/cluster_manager.dart | 43 +++++++++++++++++++++++++++--------- pubspec.yaml | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index f7b09d9..240410c 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -43,7 +43,6 @@ class ClusterManager { /// Last known zoom late double _zoom; - final double _maxLng = 180 - pow(10, -10.0) as double; /// Set Google Map Id for the cluster manager @@ -68,33 +67,49 @@ class ClusterManager { } /// Update all cluster items - void setItems(List newItems) { + void setItems(List newItems, { bool update = true }) { _items = newItems; - updateMap(); + if (update) { + updateMap(); + } } /// Add on cluster item - void addItem(ClusterItem newItem) { + void addItem(ClusterItem newItem, { bool update = true }) { _items = List.from([...items, newItem]); - updateMap(); + if (update) { + updateMap(); + } } /// Method called on camera move - void onCameraMove(CameraPosition position, {forceUpdate = false}) { + void onCameraMove(CameraPosition position, { bool forceUpdate = false }) { _zoom = position.zoom; if (forceUpdate) { updateMap(); } } + /// Return the geo-calc inflated bounds + Future getInflateBounds() async { + if (_mapId == null) return null; + final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance + .getVisibleRegion(mapId: _mapId!); + + return _inflateBounds(mapBounds); + } + /// Retrieve cluster markers Future>> getMarkers() async { if (_mapId == null) return List.empty(); - final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance - .getVisibleRegion(mapId: _mapId!); + final LatLngBounds? inflatedBounds = await getInflateBounds(); + if (inflatedBounds == null) return List.empty(); - final LatLngBounds inflatedBounds = _inflateBounds(mapBounds); + // final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance + // .getVisibleRegion(mapId: _mapId!); + // + // final LatLngBounds inflatedBounds = _inflateBounds(mapBounds); List visibleItems = items.where((i) { return inflatedBounds.contains(i.location); @@ -151,7 +166,15 @@ class ClusterManager { {int level = 5}) { if (inputItems.isEmpty) return markerItems; - String nextGeohash = inputItems[0].geohash.substring(0, level); + // String nextGeohash = inputItems[0].geohash.substring(0, level); + String? nextGeohash; + while (nextGeohash == null) { + try { + nextGeohash = inputItems[0].geohash.substring(0, level); + } catch (err) { + level -= 1; + } + } List items = inputItems .where((p) => p.geohash.substring(0, level) == nextGeohash) diff --git a/pubspec.yaml b/pubspec.yaml index c4edf3f..d34e39f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.1.0 + google_maps_flutter_platform_interface: ^2.1.3 dev_dependencies: flutter_test: From c4256f078206bc3edfb23ec659b26ac6f910ffb9 Mon Sep 17 00:00:00 2001 From: ddurzo Date: Fri, 21 Jan 2022 10:11:19 +0100 Subject: [PATCH 2/6] - add max-dist algorithm --- lib/src/cluster.dart | 21 ++++- lib/src/cluster_manager.dart | 99 ++++++++++++++++-------- lib/src/common.dart | 127 +++++++++++++++++++++++++++++++ lib/src/max_dist_clustering.dart | 68 +++++++++++++++++ 4 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 lib/src/common.dart create mode 100644 lib/src/max_dist_clustering.dart diff --git a/lib/src/cluster.dart b/lib/src/cluster.dart index 236fb7f..e00de35 100644 --- a/lib/src/cluster.dart +++ b/lib/src/cluster.dart @@ -5,13 +5,27 @@ class Cluster { final LatLng location; final Iterable items; - Cluster(this.items) - : this.location = LatLng( + Cluster(this.items, this.location); + + Cluster.fromItems(Iterable items) + : this.items = items, + this.location = LatLng( items.fold(0.0, (p, c) => p + c.location.latitude) / items.length, items.fold(0.0, (p, c) => p + c.location.longitude) / items.length); + //location becomes weighted avarage lat lon + Cluster.fromClusters(Cluster cluster1, Cluster cluster2) + : this.items = cluster1.items.toSet()..addAll(cluster2.items.toSet()), + this.location = LatLng( + (cluster1.location.latitude * cluster1.count + + cluster2.location.latitude * cluster2.count) / + (cluster1.count + cluster2.count), + (cluster1.location.longitude * cluster1.count + + cluster2.location.longitude * cluster2.count) / + (cluster1.count + cluster2.count)); + /// Get number of clustered items int get count => items.length; @@ -30,4 +44,7 @@ class Cluster { String toString() { return 'Cluster of $count $T (${location.latitude}, ${location.longitude})'; } + + bool operator ==(o) => o is Cluster && items == o.items; + int get hashCode => items.hashCode; } diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index 240410c..eab3c80 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -5,20 +5,35 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; +import 'package:google_maps_cluster_manager/src/max_dist_clustering.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +enum ClusterAlgorithm { GEOHASH, MAX_DIST } + +class MaxDistParams { + final double epsilon; + + MaxDistParams(this.epsilon); +} + class ClusterManager { ClusterManager(this._items, this.updateMarkers, {Future Function(Cluster)? markerBuilder, - this.levels = const [1, 4.25, 6.75, 8.25, 11.5, 14.5, 16.0, 16.5, 20.0], - this.extraPercent = 0.5, - this.stopClusteringZoom}) + this.levels = const [1, 4.25, 6.75, 8.25, 11.5, 14.5, 16.0, 16.5, 20.0], + this.extraPercent = 0.5, + this.maxItemsForMaxDistAlgo = 200, + this.clusterAlgorithm = ClusterAlgorithm.GEOHASH, + this.maxDistParams, + this.stopClusteringZoom}) : this.markerBuilder = markerBuilder ?? _basicMarkerBuilder, assert(levels.length <= precision); /// Method to build markers final Future Function(Cluster) markerBuilder; + // Num of Items to switch from MAX_DIST algo to GEOHASH + final int maxItemsForMaxDistAlgo; + /// Function to update Markers on Google Map final void Function(Set) updateMarkers; @@ -28,6 +43,11 @@ class ClusterManager { /// Extra percent of markers to be loaded (ex : 0.2 for 20%) final double extraPercent; + // Clusteringalgorithm + final ClusterAlgorithm clusterAlgorithm; + + final MaxDistParams? maxDistParams; + /// Zoom level to stop cluster rendering final double? stopClusteringZoom; @@ -43,6 +63,7 @@ class ClusterManager { /// Last known zoom late double _zoom; + final double _maxLng = 180 - pow(10, -10.0) as double; /// Set Google Map Id for the cluster manager @@ -91,38 +112,44 @@ class ClusterManager { } /// Return the geo-calc inflated bounds - Future getInflateBounds() async { + Future getInflatedBounds() async { if (_mapId == null) return null; final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance .getVisibleRegion(mapId: _mapId!); - return _inflateBounds(mapBounds); + late LatLngBounds inflatedBounds; + if (clusterAlgorithm == ClusterAlgorithm.GEOHASH) { + inflatedBounds = _inflateBounds(mapBounds); + } else { + inflatedBounds = mapBounds; + } + return inflatedBounds; } /// Retrieve cluster markers Future>> getMarkers() async { - if (_mapId == null) return List.empty(); - - final LatLngBounds? inflatedBounds = await getInflateBounds(); + final inflatedBounds = await getInflatedBounds(); if (inflatedBounds == null) return List.empty(); - // final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance - // .getVisibleRegion(mapId: _mapId!); - // - // final LatLngBounds inflatedBounds = _inflateBounds(mapBounds); - List visibleItems = items.where((i) { return inflatedBounds.contains(i.location); }).toList(); if (stopClusteringZoom != null && _zoom >= stopClusteringZoom!) - return visibleItems.map((i) => Cluster([i])).toList(); - - int level = _findLevel(levels); - List> markers = _computeClusters( - visibleItems, List.empty(growable: true), - level: level); - return markers; + return visibleItems.map((i) => Cluster.fromItems([i])).toList(); + + if (clusterAlgorithm == ClusterAlgorithm.GEOHASH || + visibleItems.length >= maxItemsForMaxDistAlgo) { + int level = _findLevel(levels); + List> markers = _computeClusters( + visibleItems, List.empty(growable: true), + level: level); + return markers; + } else { + List> markers = + _computeClustersWithMaxDist(visibleItems, _zoom); + return markers; + } } LatLngBounds _inflateBounds(LatLngBounds bounds) { @@ -161,26 +188,36 @@ class ClusterManager { return 1; } + int _getZoomLevel(double zoom) { + for (int i = levels.length - 1; i >= 0; i--) { + if (levels[i] <= zoom) { + return levels[i].toInt(); + } + } + + return 1; + } + + List> _computeClustersWithMaxDist( + List inputItems, double zoom) { + MaxDistClustering scanner = MaxDistClustering( + epsilon: maxDistParams?.epsilon ?? 20, + ); + + return scanner.run(inputItems, _getZoomLevel(zoom)); + } + List> _computeClusters( List inputItems, List> markerItems, {int level = 5}) { if (inputItems.isEmpty) return markerItems; - - // String nextGeohash = inputItems[0].geohash.substring(0, level); - String? nextGeohash; - while (nextGeohash == null) { - try { - nextGeohash = inputItems[0].geohash.substring(0, level); - } catch (err) { - level -= 1; - } - } + String nextGeohash = inputItems[0].geohash.substring(0, level); List items = inputItems .where((p) => p.geohash.substring(0, level) == nextGeohash) .toList(); - markerItems.add(Cluster(items)); + markerItems.add(Cluster.fromItems(items)); List newInputList = List.from( inputItems.where((i) => i.geohash.substring(0, level) != nextGeohash)); diff --git a/lib/src/common.dart b/lib/src/common.dart new file mode 100644 index 0000000..a2c6533 --- /dev/null +++ b/lib/src/common.dart @@ -0,0 +1,127 @@ +import 'dart:math'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +const int _thd = 1000; +const double _PI = 3.141592653589793238; + +class _Tuple { + final LatLng pos1; + final LatLng pos2; + + _Tuple(this.pos1, this.pos2); + + bool operator ==(o) => o is _Tuple && pos1 == o.pos1 && pos2 == o.pos2; + int get hashCode => pos1.hashCode + pos2.hashCode; +} + +class DistUtils { +// Zoom-Level: +// Level # Tiles Tile width +// (° of longitudes) m / pixel +// (on Equator) ~ Scale +// (on screen) Examples of +// areas to represent +// 0 1 360 156 412 1:500 million whole world +// 1 4 180 78 206 1:250 million +// 2 16 90 39 103 1:150 million subcontinental area +// 3 64 45 19 551 1:70 million largest country +// 4 256 22.5 9 776 1:35 million +// 5 1 024 11.25 4 888 1:15 million large African country +// 6 4 096 5.625 2 444 1:10 million large European country +// 7 16 384 2.813 1 222 1:4 million small country, US state +// 8 65 536 1.406 610.984 1:2 million +// 9 262 144 0.703 305.492 1:1 million wide area, large metropolitan area +// 10 1 048 576 0.352 152.746 1:500 thousand metropolitan area +// 11 4 194 304 0.176 76.373 1:250 thousand city +// 12 16 777 216 0.088 38.187 1:150 thousand town, or city district +// 13 67 108 864 0.044 19.093 1:70 thousand village, or suburb +// 14 268 435 456 0.022 9.547 1:35 thousand +// 15 1 073 741 824 0.011 4.773 1:15 thousand small road +// 16 4 294 967 296 0.005 2.387 1:8 thousand street +// 17 17 179 869 184 0.003 1.193 1:4 thousand block, park, addresses +// 18 68 719 476 736 0.001 0.596 1:2 thousand some buildings, trees +// 19 274 877 906 944 0.0005 0.298 1:1 thousand local highway and crossing details +// 20 1 099 511 627 776 0.00025 0.149 1:5 hundred A mid-sized building + final Map<_Tuple, double> distCache = {}; + + double getLatLonDist(LatLng point1, LatLng point2, int zoomLevel) { + if (distCache[_Tuple(point1, point2)] != null) { + return distCache[_Tuple(point1, point2)]!; + } + double meterPerPixel = _getScalingFactor(zoomLevel); + double dist = getDistanceFromLatLonInKm(point1.latitude, point1.longitude, + point2.latitude, point2.longitude) / + (meterPerPixel / _thd); + // print("dist is $x"); + distCache[_Tuple(point1, point2)] = dist; + return dist; + } + + double getDistanceFromLatLonInKm( + double lat1, double lon1, double lat2, double lon2) { + var R = 6371; // Radius of the earth in km + var dLat = _degreeToRadian(lat2 - lat1); + var dLon = _degreeToRadian(lon2 - lon1); + var a = sin(dLat / 2) * sin(dLat / 2) + + cos(_degreeToRadian(lat1)) * + cos(_degreeToRadian(lat2)) * + sin(dLon / 2) * + sin(dLon / 2); + var c = 2 * atan2(sqrt(a), sqrt(1 - a)); + var d = R * c; // Distance in km + return d; + } + + double _degreeToRadian(double degree) { + return degree * _PI / 180; + } + + double _getScalingFactor(int zoomLevel) { + switch (zoomLevel) { + case 0: + return 156412; + case 1: + return 78206; + case 2: + return 39103; + case 3: + return 19551; + case 4: + return 9776; + case 5: + return 4888; + case 6: + return 2444; + case 7: + return 1222; + case 8: + return 610.984; + case 9: + return 305.492; + case 10: + return 152.746; + case 11: + return 76.373; + case 12: + return 38.187; + case 13: + return 19.093; + case 14: + return 9.547; + case 15: + return 4.773; + case 16: + return 2.387; + case 17: + return 1.193; + case 18: + return 0.596; + case 19: + return 0.298; + case 20: + return 0.149; + default: + return 0.149; + } + } +} \ No newline at end of file diff --git a/lib/src/max_dist_clustering.dart b/lib/src/max_dist_clustering.dart new file mode 100644 index 0000000..2fda17f --- /dev/null +++ b/lib/src/max_dist_clustering.dart @@ -0,0 +1,68 @@ +import '../google_maps_cluster_manager.dart'; +import 'common.dart'; + +class _MinDistCluster { + final Cluster cluster; + final double dist; + + _MinDistCluster(this.cluster, this.dist); +} + +class MaxDistClustering { + ///Complete list of points + late List dataset; + + List> _cluster = []; + + ///Threshold distance for two clusters to be considered as one cluster + final double epsilon; + + final DistUtils distUtils = DistUtils(); + + MaxDistClustering({ + this.epsilon = 1, + }); + + ///Run clustering process, add configs in constructor + List> run(List dataset, int zoomLevel) { + this.dataset = dataset; + + //initial variables + List> distMatrix = []; + for (T entry1 in dataset) { + distMatrix.add([]); + _cluster.add(Cluster.fromItems([entry1])); + } + bool changed = true; + while (changed) { + changed = false; + for (Cluster c in _cluster) { + _MinDistCluster? minDistCluster = getClosestCluster(c, zoomLevel); + // print("mindistcluster ${minDistCluster?.dist}"); + if (minDistCluster == null || minDistCluster.dist > epsilon) continue; + _cluster.add(Cluster.fromClusters(minDistCluster.cluster, c)); + _cluster.remove(c); + _cluster.remove(minDistCluster.cluster); + changed = true; + + break; + } + } + return _cluster; + } + + _MinDistCluster? getClosestCluster(Cluster cluster, int zoomLevel) { + double minDist = 1000000000; + Cluster minDistCluster = Cluster.fromItems([]); + for (Cluster c in _cluster) { + if (c.location == cluster.location) continue; + double tmp = + distUtils.getLatLonDist(c.location, cluster.location, zoomLevel); + if (tmp < minDist) { + minDist = tmp; + minDistCluster = Cluster.fromItems(c.items); + } + } + return _MinDistCluster(minDistCluster, minDist); + } +} \ No newline at end of file From 6459dc0b6c38518702675daaefb7b6c8efd92a4e Mon Sep 17 00:00:00 2001 From: ddurzo Date: Fri, 21 Jan 2022 12:27:04 +0100 Subject: [PATCH 3/6] - add overlapping management (to be improved) --- lib/src/cluster.dart | 15 ++++++++++++--- lib/src/cluster_item.dart | 4 ++++ lib/src/cluster_manager.dart | 19 +++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/src/cluster.dart b/lib/src/cluster.dart index e00de35..b7e60fa 100644 --- a/lib/src/cluster.dart +++ b/lib/src/cluster.dart @@ -4,20 +4,24 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf class Cluster { final LatLng location; final Iterable items; + bool isOverlapped; - Cluster(this.items, this.location); + Cluster(this.items, this.location, { this.isOverlapped = false }); - Cluster.fromItems(Iterable items) + Cluster.fromItems(Iterable items, { bool isOverlapped = false }) : this.items = items, this.location = LatLng( items.fold(0.0, (p, c) => p + c.location.latitude) / items.length, items.fold(0.0, (p, c) => p + c.location.longitude) / - items.length); + items.length), + this.isOverlapped = isOverlapped + ; //location becomes weighted avarage lat lon Cluster.fromClusters(Cluster cluster1, Cluster cluster2) : this.items = cluster1.items.toSet()..addAll(cluster2.items.toSet()), + this.isOverlapped = false, this.location = LatLng( (cluster1.location.latitude * cluster1.count + cluster2.location.latitude * cluster2.count) / @@ -34,6 +38,11 @@ class Cluster { /// Basic cluster marker id String getId() { + final idList = items.where((e) => e.getId() != null).map((e) => e.getId()).toList(); + if (idList.isNotEmpty) { + return idList.join(':'); + } + return location.latitude.toString() + "_" + location.longitude.toString() + diff --git a/lib/src/cluster_item.dart b/lib/src/cluster_item.dart index 1c345c6..96bb4a0 100644 --- a/lib/src/cluster_item.dart +++ b/lib/src/cluster_item.dart @@ -7,4 +7,8 @@ abstract class ClusterItem { String? _geohash; String get geohash => _geohash ??= Geohash.encode(location, codeLength: ClusterManager.precision); + + /// base getId. + /// If you override it, it uses it's value + String? getId() { return null; } } diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index eab3c80..d357f2f 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -126,6 +126,19 @@ class ClusterManager { return inflatedBounds; } + /// Build cluster items in case of overlap + List> buildPlainListWithOverlappingCluster(List items) { + Map> _map = {}; + items.forEach((e) { + if (_map.containsKey(e.location.toString())) { + _map[e.location.toString()]?.add(e); + } else { + _map[e.location.toString()] = [e]; + } + }); + return _map.values.map((i) => Cluster.fromItems(i, isOverlapped: i.length > 1)).toList(); + } + /// Retrieve cluster markers Future>> getMarkers() async { final inflatedBounds = await getInflatedBounds(); @@ -135,8 +148,10 @@ class ClusterManager { return inflatedBounds.contains(i.location); }).toList(); - if (stopClusteringZoom != null && _zoom >= stopClusteringZoom!) - return visibleItems.map((i) => Cluster.fromItems([i])).toList(); + if (stopClusteringZoom != null && _zoom <= stopClusteringZoom!) { + // return visibleItems.map((i) => Cluster.fromItems([i])).toList(); + return buildPlainListWithOverlappingCluster(visibleItems); + } if (clusterAlgorithm == ClusterAlgorithm.GEOHASH || visibleItems.length >= maxItemsForMaxDistAlgo) { From f72dd6508f2ca07044d4962d425f131aa2bb298b Mon Sep 17 00:00:00 2001 From: ddurzo Date: Thu, 10 Feb 2022 13:23:27 +0100 Subject: [PATCH 4/6] - add an overlapping algorithm to manage same-point locations --- example/lib/place.dart | 5 +++ lib/src/cluster_item.dart | 3 ++ lib/src/cluster_manager.dart | 83 ++++++++++++++++++++++++++++++------ lib/src/common.dart | 56 ++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/example/lib/place.dart b/example/lib/place.dart index 07d4738..5a23b57 100644 --- a/example/lib/place.dart +++ b/example/lib/place.dart @@ -15,4 +15,9 @@ class Place with ClusterItem { @override LatLng get location => latLng; + + @override + set location(LatLng newLocation) { + location = newLocation; + } } diff --git a/lib/src/cluster_item.dart b/lib/src/cluster_item.dart index 96bb4a0..78efd4a 100644 --- a/lib/src/cluster_item.dart +++ b/lib/src/cluster_item.dart @@ -2,7 +2,10 @@ import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; abstract class ClusterItem { + /// Getter for location LatLng get location; + /// Setter for location. + set location(LatLng newLocation); String? _geohash; String get geohash => _geohash ??= diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index d357f2f..cdd19de 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -5,10 +5,12 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; +import 'package:google_maps_cluster_manager/src/common.dart'; import 'package:google_maps_cluster_manager/src/max_dist_clustering.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; enum ClusterAlgorithm { GEOHASH, MAX_DIST } +enum ClusterOverlapping { NONE, OVERLAP, DISTRIBUTE } class MaxDistParams { final double epsilon; @@ -16,6 +18,18 @@ class MaxDistParams { MaxDistParams(this.epsilon); } +class ClusterOverlappingParams { + final double bearing; + final double distance; + final double overlappingDistanceLimitInMeters; + + ClusterOverlappingParams({ + this.bearing = 0.3, + this.distance = 0.4, + this.overlappingDistanceLimitInMeters = 20, + }); +} + class ClusterManager { ClusterManager(this._items, this.updateMarkers, {Future Function(Cluster)? markerBuilder, @@ -24,14 +38,17 @@ class ClusterManager { this.maxItemsForMaxDistAlgo = 200, this.clusterAlgorithm = ClusterAlgorithm.GEOHASH, this.maxDistParams, - this.stopClusteringZoom}) + this.stopClusteringZoom, + this.clusterOverlapping = ClusterOverlapping.NONE, + this.clusterOverlappingParams, + }) : this.markerBuilder = markerBuilder ?? _basicMarkerBuilder, assert(levels.length <= precision); /// Method to build markers final Future Function(Cluster) markerBuilder; - // Num of Items to switch from MAX_DIST algo to GEOHASH + /// Num of Items to switch from MAX_DIST algo to GEOHASH final int maxItemsForMaxDistAlgo; /// Function to update Markers on Google Map @@ -43,9 +60,10 @@ class ClusterManager { /// Extra percent of markers to be loaded (ex : 0.2 for 20%) final double extraPercent; - // Clusteringalgorithm + /// Clusteringalgorithm final ClusterAlgorithm clusterAlgorithm; + /// Max dists params final MaxDistParams? maxDistParams; /// Zoom level to stop cluster rendering @@ -54,6 +72,12 @@ class ClusterManager { /// Precision of the geohash static final int precision = kIsWeb ? 12 : 20; + /// Overlapping option + final ClusterOverlapping clusterOverlapping; + + /// Overlapping distance limit + ClusterOverlappingParams? clusterOverlappingParams; + /// Google Maps map id int? _mapId; @@ -128,15 +152,49 @@ class ClusterManager { /// Build cluster items in case of overlap List> buildPlainListWithOverlappingCluster(List items) { - Map> _map = {}; - items.forEach((e) { - if (_map.containsKey(e.location.toString())) { - _map[e.location.toString()]?.add(e); - } else { - _map[e.location.toString()] = [e]; + clusterOverlappingParams ??= ClusterOverlappingParams(); + print('BUILD OVERLAPPED WITH $clusterOverlapping'); + /// Overlapping: if the points are in the same place, create fixed cluster + if (clusterOverlapping == ClusterOverlapping.OVERLAP) { + Map> _map = {}; + items.forEach((e) { + if (_map.containsKey(e.location.toString())) { + _map[e.location.toString()]?.add(e); + } else { + _map[e.location.toString()] = [e]; + } + }); + return _map.values.map((i) => + Cluster.fromItems(i, isOverlapped: i.length > 1)).toList(); + } + + /// Distribute: if the points are in the same place, put aside + if (clusterOverlapping == ClusterOverlapping.DISTRIBUTE) { + final DistUtils distUtils = DistUtils(); + var bearing = 0.8; + for(var i = 0; i < items.length; i++) { + for(var j = 0; j < items.length; j++) { + if (j != i) { + final dist = distUtils.getLatLonDist( + items[i].location, items[j].location, _getZoomLevel(_zoom)) * 1000; + // print('DISTANCE: $dist FROM OVERLAPPING: $overlappingDistanceLimitInMeters'); + if (dist < clusterOverlappingParams!.overlappingDistanceLimitInMeters) { + // print('PREVIOUS LOCATION: ${items[i].location}'); + items[i].location = distUtils.getPointAtDistanceFrom( + items[i].location, + clusterOverlappingParams!.bearing, + clusterOverlappingParams!.distance + ); + // print('NEW LOCATION: ${items[i].location}'); + bearing += 0.3; + } + } + } } - }); - return _map.values.map((i) => Cluster.fromItems(i, isOverlapped: i.length > 1)).toList(); + } + + /// Otherwise, simple list + return items.map((i) => Cluster.fromItems([i])).toList(); } /// Retrieve cluster markers @@ -148,7 +206,8 @@ class ClusterManager { return inflatedBounds.contains(i.location); }).toList(); - if (stopClusteringZoom != null && _zoom <= stopClusteringZoom!) { + print('STOP $stopClusteringZoom AT ZOOM $_zoom'); + if (stopClusteringZoom != null && _zoom >= stopClusteringZoom!) { // return visibleItems.map((i) => Cluster.fromItems([i])).toList(); return buildPlainListWithOverlappingCluster(visibleItems); } diff --git a/lib/src/common.dart b/lib/src/common.dart index a2c6533..4b40d37 100644 --- a/lib/src/common.dart +++ b/lib/src/common.dart @@ -3,6 +3,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf const int _thd = 1000; const double _PI = 3.141592653589793238; +const num _RADIUS = 6371e3; class _Tuple { final LatLng pos1; @@ -76,6 +77,10 @@ class DistUtils { return degree * _PI / 180; } + double _radianToDegree(double rad) { + return rad * (180.0 / _PI); + } + double _getScalingFactor(int zoomLevel) { switch (zoomLevel) { case 0: @@ -124,4 +129,55 @@ class DistUtils { return 0.149; } } + + /// + /// Get distance from point by bearing and km + LatLng getPointAtDistanceFrom(LatLng startPoint, double initialBearingRadians, double distanceKilometres) { + const double radiusEarthKilometres = 6371.01; + var distRatio = distanceKilometres / radiusEarthKilometres; + var distRatioSine = sin(distRatio); + var distRatioCosine = cos(distRatio); + + var startLatRad = _degreeToRadian(startPoint.latitude); + var startLonRad = _degreeToRadian(startPoint.longitude); + + var startLatCos = cos(startLatRad); + var startLatSin = sin(startLatRad); + + var endLatRads = asin((startLatSin * distRatioCosine) + (startLatCos * distRatioSine * cos(initialBearingRadians))); + + var endLonRads = startLonRad + + atan2( + sin(initialBearingRadians) * distRatioSine * startLatCos, + distRatioCosine - startLatSin * sin(endLatRads)); + + return LatLng(_radianToDegree(endLatRads), _radianToDegree(endLonRads)); + } + + /// calculate a destination point given the distance and bearing + LatLng destinationPointByDistanceAndBearing(LatLng l, num distance, num bearing, [num? radius]) { + radius ??= _RADIUS; + + final num angularDistanceRadius = distance / radius; + final num bearingRadians = _degreeToRadian(bearing as double); + + final num latRadians = _degreeToRadian(l.latitude); + final num lngRadians = _degreeToRadian(l.longitude); + + final num sinLatRadians = sin(latRadians); + final num cosLatRadians = cos(latRadians); + final num sinAngularDistanceRadius = sin(angularDistanceRadius); + final num cosAngularDistanceRadius = cos(angularDistanceRadius); + final num sinBearingRadians = sin(bearingRadians); + final num cosBearingRadians = cos(bearingRadians); + + final sinLatRadians2 = sinLatRadians * cosAngularDistanceRadius + + cosLatRadians * sinAngularDistanceRadius * cosBearingRadians; + final num latRadians2 = asin(sinLatRadians2); + final y = sinBearingRadians * sinAngularDistanceRadius * cosLatRadians; + final x = cosAngularDistanceRadius - sinLatRadians * sinLatRadians2; + final num lngRadians2 = lngRadians + atan2(y, x); + return LatLng(_radianToDegree(latRadians2 as double), + (_radianToDegree(lngRadians2 as double) + 540) % 360 - 180); + } } \ No newline at end of file From 8013b63c235fcb8e87f89675d1a3ebd58547a933 Mon Sep 17 00:00:00 2001 From: ddurzo Date: Sat, 19 Feb 2022 11:01:13 +0100 Subject: [PATCH 5/6] - minor improvements to base library to fix some position/view issues --- lib/src/cluster_manager.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index cdd19de..20d1b75 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -153,7 +153,7 @@ class ClusterManager { /// Build cluster items in case of overlap List> buildPlainListWithOverlappingCluster(List items) { clusterOverlappingParams ??= ClusterOverlappingParams(); - print('BUILD OVERLAPPED WITH $clusterOverlapping'); + // print('BUILD OVERLAPPED WITH $clusterOverlapping'); /// Overlapping: if the points are in the same place, create fixed cluster if (clusterOverlapping == ClusterOverlapping.OVERLAP) { Map> _map = {}; @@ -171,22 +171,19 @@ class ClusterManager { /// Distribute: if the points are in the same place, put aside if (clusterOverlapping == ClusterOverlapping.DISTRIBUTE) { final DistUtils distUtils = DistUtils(); - var bearing = 0.8; + var bearing = clusterOverlappingParams!.bearing; for(var i = 0; i < items.length; i++) { for(var j = 0; j < items.length; j++) { if (j != i) { final dist = distUtils.getLatLonDist( items[i].location, items[j].location, _getZoomLevel(_zoom)) * 1000; - // print('DISTANCE: $dist FROM OVERLAPPING: $overlappingDistanceLimitInMeters'); if (dist < clusterOverlappingParams!.overlappingDistanceLimitInMeters) { - // print('PREVIOUS LOCATION: ${items[i].location}'); items[i].location = distUtils.getPointAtDistanceFrom( items[i].location, - clusterOverlappingParams!.bearing, + bearing, clusterOverlappingParams!.distance ); - // print('NEW LOCATION: ${items[i].location}'); - bearing += 0.3; + bearing += clusterOverlappingParams!.bearing; } } } @@ -202,16 +199,22 @@ class ClusterManager { final inflatedBounds = await getInflatedBounds(); if (inflatedBounds == null) return List.empty(); - List visibleItems = items.where((i) { - return inflatedBounds.contains(i.location); - }).toList(); - print('STOP $stopClusteringZoom AT ZOOM $_zoom'); + // in case of stopping zoom clustering and custom overlapping conf, + // clear visible point in bounds after change point positions if (stopClusteringZoom != null && _zoom >= stopClusteringZoom!) { // return visibleItems.map((i) => Cluster.fromItems([i])).toList(); - return buildPlainListWithOverlappingCluster(visibleItems); + final l = buildPlainListWithOverlappingCluster(items.toList()); // visibleItems); + return l.where((i) { + return inflatedBounds.contains(i.location); + }).toList(); } + // otherwise go ahead with simple standard clustering logic + List visibleItems = items.where((i) { + return inflatedBounds.contains(i.location); + }).toList(); + if (clusterAlgorithm == ClusterAlgorithm.GEOHASH || visibleItems.length >= maxItemsForMaxDistAlgo) { int level = _findLevel(levels); From 4177718ceb611cf68af78d8eb260c5e13a258aef Mon Sep 17 00:00:00 2001 From: ddurzo Date: Mon, 21 Feb 2022 14:44:12 +0100 Subject: [PATCH 6/6] - other minor improvements in clustering --- lib/src/cluster_manager.dart | 42 +++++++++++++++++++++--------------- lib/src/common.dart | 1 + 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/src/cluster_manager.dart b/lib/src/cluster_manager.dart index 20d1b75..cf173d6 100644 --- a/lib/src/cluster_manager.dart +++ b/lib/src/cluster_manager.dart @@ -152,16 +152,22 @@ class ClusterManager { /// Build cluster items in case of overlap List> buildPlainListWithOverlappingCluster(List items) { + final DistUtils distUtils = DistUtils(); + // items.forEach((e) { print('***** id: ${e.getId()} ${e.location}'); }); + clusterOverlappingParams ??= ClusterOverlappingParams(); // print('BUILD OVERLAPPED WITH $clusterOverlapping'); /// Overlapping: if the points are in the same place, create fixed cluster if (clusterOverlapping == ClusterOverlapping.OVERLAP) { - Map> _map = {}; + Map> _map = {}; items.forEach((e) { - if (_map.containsKey(e.location.toString())) { - _map[e.location.toString()]?.add(e); + final key = _map.keys.firstWhere( + (k) => distUtils.getLatLonDist(k, e.location, _getZoomLevel(_zoom)) <= (clusterOverlappingParams!.overlappingDistanceLimitInMeters), orElse: () => LatLng(0, 0) + ); + if (key.longitude != 0) { + _map[key]?.add(e); } else { - _map[e.location.toString()] = [e]; + _map[e.location] = [e]; } }); return _map.values.map((i) => @@ -170,26 +176,28 @@ class ClusterManager { /// Distribute: if the points are in the same place, put aside if (clusterOverlapping == ClusterOverlapping.DISTRIBUTE) { - final DistUtils distUtils = DistUtils(); var bearing = clusterOverlappingParams!.bearing; for(var i = 0; i < items.length; i++) { - for(var j = 0; j < items.length; j++) { - if (j != i) { - final dist = distUtils.getLatLonDist( - items[i].location, items[j].location, _getZoomLevel(_zoom)) * 1000; - if (dist < clusterOverlappingParams!.overlappingDistanceLimitInMeters) { - items[i].location = distUtils.getPointAtDistanceFrom( - items[i].location, - bearing, - clusterOverlappingParams!.distance - ); - bearing += clusterOverlappingParams!.bearing; - } + for(var j = i + 1; j < items.length; j++) { + final dist = distUtils.getLatLonDist( + items[i].location, items[j].location, _getZoomLevel(_zoom)); + + // print('parking id: ${items[i].getId()} - ${items[j].getId()} = $dist (of ${clusterOverlappingParams!.overlappingDistanceLimitInMeters})'); + if (dist < (clusterOverlappingParams!.overlappingDistanceLimitInMeters)) { + items[i].location = distUtils.getPointAtDistanceFrom( + items[i].location, + bearing, + clusterOverlappingParams!.distance + ); + bearing += clusterOverlappingParams!.bearing; } } } } + // final l = items.map((i) => Cluster.fromItems([i])).toList(); + // l.forEach((e) { print('@@@@@@ id: ${e.getId()} ${e.location}'); }); + /// Otherwise, simple list return items.map((i) => Cluster.fromItems([i])).toList(); } diff --git a/lib/src/common.dart b/lib/src/common.dart index 4b40d37..1db1436 100644 --- a/lib/src/common.dart +++ b/lib/src/common.dart @@ -55,6 +55,7 @@ class DistUtils { (meterPerPixel / _thd); // print("dist is $x"); distCache[_Tuple(point1, point2)] = dist; + // print('DISTANCE IS: $dist'); return dist; }