diff --git a/README.md b/README.md
index 59712f6..c5fffe9 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,6 @@
-
+
+
+
[![Build Status](https://shields.io/github/actions/workflow/status/surfstudio/yandex-mapkit-lite-flutter/main.yml?logo=github&logoColor=white)](https://github.com/surfstudio/yandex-mapkit-lite-flutter)
[![Pub Version](https://img.shields.io/pub/v/yandex_mapkit_lite?logo=dart&logoColor=white)](https://pub.dev/packages/yandex_mapkit_lite)
@@ -145,6 +143,7 @@ For app bundle size optimization purposes, the original package was moved to lit
| Offline maps | :white_check_mark: | :white_check_mark: |
| Location manager | :white_check_mark: | :white_check_mark: |
| User location layer | :white_check_mark: | :white_check_mark: |
+| Custom clusterization | :x: | :white_check_mark: - see the example project |
| Search, hints, geocoding | :white_check_mark: | :x: - consider using [yandex_geocoder](https://pub.dev/packages/yandex_geocoder) |
| Automobile, bicycle, and pedestrian routing | :white_check_mark: | :x: |
| Routing taking into account public transport | :white_check_mark: | :x: |
diff --git a/android/src/main/java/com/unact/yandexmapkit/YandexMapController.java b/android/src/main/java/com/unact/yandexmapkit/YandexMapController.java
index 987a604..c250bd1 100755
--- a/android/src/main/java/com/unact/yandexmapkit/YandexMapController.java
+++ b/android/src/main/java/com/unact/yandexmapkit/YandexMapController.java
@@ -758,7 +758,8 @@ public void onCameraPositionChanged(
arguments.put("cameraPosition", Utils.cameraPositionToJson(cameraPosition));
arguments.put("reason", cameraUpdateReason.ordinal());
arguments.put("finished", finished);
-
+ arguments.put("visibleRegion", Utils.visibleRegionToJson(map.getVisibleRegion()));
+
methodChannel.invokeMethod("onCameraPositionChanged", arguments);
}
diff --git a/assets/yandex_mapkit_lite.dark.png b/assets/yandex_mapkit_lite.dark.png
deleted file mode 100644
index ba4b504..0000000
Binary files a/assets/yandex_mapkit_lite.dark.png and /dev/null differ
diff --git a/example/.markdownlint.json b/example/.markdownlint.json
index fc60389..e34d109 100644
--- a/example/.markdownlint.json
+++ b/example/.markdownlint.json
@@ -1,5 +1,6 @@
{
"MD041": false,
"MD033": false,
- "MD013": false
+ "MD013": false,
+ "MD034": false
}
\ No newline at end of file
diff --git a/example/README.md b/example/README.md
index c95b0b9..fcfd1aa 100644
--- a/example/README.md
+++ b/example/README.md
@@ -1,8 +1,6 @@
-
+
+
+
[![Build Status](https://shields.io/github/actions/workflow/status/surfstudio/yandex-mapkit-lite-flutter/main.yml?logo=github&logoColor=white)](https://github.com/surfstudio/yandex-mapkit-lite-flutter)
[![Coverage Status](https://img.shields.io/codecov/c/github/surfstudio/yandex-mapkit-lite-flutter?logo=codecov&logoColor=white)](https://app.codecov.io/gh/surfstudio/yandex-mapkit-lite-flutter)
diff --git a/example/lib/presentation/screens/root_screen.dart b/example/lib/presentation/screens/root_screen.dart
index fa3cfb8..f5c8fb1 100644
--- a/example/lib/presentation/screens/root_screen.dart
+++ b/example/lib/presentation/screens/root_screen.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:yandex_mapkit_example/assets/res/assets.dart';
import 'package:yandex_mapkit_example/presentation/state/collection_map_strategy.dart';
+import 'package:yandex_mapkit_example/presentation/state/custom_clusterization_map_strategy.dart';
import 'package:yandex_mapkit_example/presentation/state/custom_shape_map_strategy.dart';
import 'package:yandex_mapkit_example/presentation/state/default_map_strategy.dart';
import 'package:yandex_mapkit_example/presentation/state/draggable_placemark_map_strategy.dart';
@@ -85,13 +86,21 @@ class _RootScreenState extends State {
///
/// See [CustomShapeMapStrategyDelegate] for more details.
CustomShapeMapStrategyDelegate(),
+
+ /// Strategy for map feature showcase with custom clusterization.
+ ///
+ /// This strategy is showing how to add a custom clusterization to the map and
+ /// how to handle user interactions with the clusterization.
+ ///
+ /// See [CustomClusterizationMapStrategyDelegate] for more details.
+ CustomClusterizationMapStrategyDelegate(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
- title: const Text('Yandex Mapkit Lite'),
+ title: Text(_mapStrategy.title),
actions: [
DropdownButton(
value: _selectedStrategy,
@@ -120,6 +129,10 @@ class _RootScreenState extends State {
value: 5,
child: Icon(Icons.shape_line),
),
+ DropdownMenuItem(
+ value: 6,
+ child: Icon(Icons.dashboard_customize),
+ ),
],
onChanged: (value) {
setState(() {
@@ -159,6 +172,7 @@ class _RootScreenState extends State {
},
onTrafficChanged: _mapStrategy.onTrafficLevelChanged,
onUserLocationUpdated: _mapStrategy.onUserLayer,
+ onCameraPositionChanged: _mapStrategy.onCameraPositionChanged,
allowUserInteractions: _mapStrategy.allowUserInteractions,
),
),
diff --git a/example/lib/presentation/state/collection_map_strategy.dart b/example/lib/presentation/state/collection_map_strategy.dart
index 80257d4..fbdc9f2 100644
--- a/example/lib/presentation/state/collection_map_strategy.dart
+++ b/example/lib/presentation/state/collection_map_strategy.dart
@@ -190,4 +190,7 @@ class ClusterMapStrategyDelegate extends MapStrategyDelegate {
..._selectedPlaces.map(_createSelectedPlacemark),
};
}
+
+ @override
+ String get title => "Map objects clusterization";
}
diff --git a/example/lib/presentation/state/custom_clusterization_map_strategy.dart b/example/lib/presentation/state/custom_clusterization_map_strategy.dart
new file mode 100644
index 0000000..a73e830
--- /dev/null
+++ b/example/lib/presentation/state/custom_clusterization_map_strategy.dart
@@ -0,0 +1,227 @@
+import 'package:fluster/fluster.dart';
+import 'package:flutter/material.dart';
+import 'package:yandex_mapkit_example/assets/res/assets.dart';
+import 'package:yandex_mapkit_example/presentation/state/map_strategy.dart';
+import 'package:yandex_mapkit_lite/yandex_mapkit_lite.dart';
+
+class CustomClusterizationMapStrategyDelegate extends MapStrategyDelegate {
+ @override
+ Widget buildActions(BuildContext context) => const SizedBox();
+
+ /// There is only one placemark on the map.
+ ///
+ /// The placemark is a circle with a red number inside, which is incremented
+ /// every time the user taps on the placemark.
+ ///
+ /// There is also a action button for incrementing.
+ @override
+ Set get mapObjects {
+ return _clusteredObjects
+ .where((element) => element.location != null)
+ .map((cluster) {
+ if (cluster.isCluster ?? false) {
+ return PlacemarkMapObject(
+ point: cluster.location!,
+ icon: PlacemarkIcon.single(
+ PlacemarkIconStyle(
+ image: BitmapDescriptor.fromAssetImage(Assets.cluster),
+ ),
+ ),
+ consumeTapEvents: true,
+ text: PlacemarkText(
+ text: cluster.pointsSize.toString(),
+ style: const PlacemarkTextStyle(
+ color: Colors.black,
+ size: 10,
+ ),
+ ),
+ opacity: 1,
+ mapId: MapObjectId(cluster.clusterId!.toString()),
+ );
+ }
+
+ final id = int.tryParse(cluster.markerId ?? '');
+
+ return PlacemarkMapObject(
+ point: cluster.location!,
+ icon: PlacemarkIcon.single(
+ PlacemarkIconStyle(
+ image: BitmapDescriptor.fromAssetImage(
+ _selectedIds.contains(id) ? Assets.routeStart : Assets.routeEnd,
+ ),
+ ),
+ ),
+ consumeTapEvents: true,
+ opacity: 1,
+ mapId: MapObjectId(cluster.markerId!),
+ onTap: (_, __) {
+ if (id == null) return;
+
+ if (_selectedIds.contains(id)) {
+ _selectedIds.remove(id);
+ } else {
+ _selectedIds.add(id);
+ }
+
+ notifyListeners();
+ },
+ );
+ }).toSet();
+ }
+
+ @override
+ String get title => "Custom clusterization";
+
+ late final List _clusterables;
+
+ late final Fluster _fluster;
+
+ CustomClusterizationMapStrategyDelegate() {
+ /// Generate clusterables objects,
+ /// which are used for Fluster tp
+ /// generate clusters.
+ _clusterables = _generateClusterables();
+
+ /// Create Fluster instance.
+ ///
+ /// Fluster is a helper class to generate
+ /// clusters for the map.
+ ///
+ /// It uses a quadtree data structure to
+ /// efficiently cluster large amounts of
+ /// points.
+ _fluster = Fluster(
+ /// Any zoom value below minZoom will not generate clusters.
+ minZoom: 0,
+
+ /// Any zoom value above maxZoom will not generate clusters.
+ maxZoom: 20,
+
+ /// Cluster radius in pixels.
+ radius: 150,
+
+ /// Adjust the extent by powers of 2 (e.g. 512. 1024, ... max 8192) to get the
+ /// desired distance between markers where they start to cluster.
+ extent: 2048,
+
+ /// The size of the KD-tree leaf node, which affects performance.
+ nodeSize: 64,
+ points: _clusterables,
+ // ignore: avoid_types_on_closure_parameters
+ createCluster:
+ (BaseCluster? cluster, double? longitude, double? latitude) {
+ return ClusterableWrapper(
+ latitude: latitude,
+ longitude: longitude,
+ isCluster: true,
+ clusterId: cluster?.id,
+ pointsSize: cluster?.pointsSize,
+ childMarkerId: cluster?.childMarkerId,
+ );
+ },
+ );
+ }
+
+ List _generateClusterables() => List.generate(
+ 100,
+ (i) => ClusterableWrapper(
+ latitude: Constants.defaultLocation.latitude + (0.1 * (i - 50)),
+ longitude: Constants.defaultLocation.longitude + (0.1 * (i - 50)),
+ isCluster: false,
+ clusterId: 0,
+ pointsSize: 0,
+ markerId: '$i',
+ ),
+ );
+
+ /// List of clustered objects.
+ ///
+ /// This list is updated every time the camera position changes.
+ List _clusteredObjects = [];
+
+ /// Set of selected marker ids.
+ ///
+ /// This set is updated every time the user taps on a marker.
+ ///
+ /// Demonstrates, how efficient can custom clusterization be
+ /// in comparison with out-of-the-box clusterization.
+ final _selectedIds = {};
+
+ /// View expand factor.
+ ///
+ /// This factor is used to expand the visible region
+ /// to make sure that all the markers are visible
+ /// and clusters are generated with markers
+ /// which are slightly outside the visible region.
+ static const _viewExpandFactor = 0;
+
+ @override
+ void onCameraPositionChanged(
+ CameraPosition cameraPosition,
+ CameraUpdateReason reason,
+ bool finished,
+ VisibleRegion visibleRegion,
+ ) {
+ final zoom = cameraPosition.zoom;
+
+ _clusteredObjects = _fluster.clusters(
+ [
+ visibleRegion.bottomLeft.longitude - _viewExpandFactor,
+ visibleRegion.bottomLeft.latitude - _viewExpandFactor,
+ visibleRegion.topRight.longitude + _viewExpandFactor,
+ visibleRegion.topRight.latitude + _viewExpandFactor,
+ ],
+ zoom.toInt(),
+ );
+
+ notifyListeners();
+ }
+}
+
+/// Wrapper for clusterable object.
+///
+/// This wrapper is used to demonstrate how to
+/// use custom clusterization with Fluster.
+///
+/// See [Fluster] for more details.
+class ClusterableWrapper implements Clusterable {
+ @override
+ String? childMarkerId;
+
+ @override
+ int? clusterId;
+
+ @override
+ bool? isCluster;
+
+ @override
+ double? latitude;
+
+ @override
+ double? longitude;
+
+ @override
+ String? markerId;
+
+ @override
+ int? pointsSize;
+
+ ClusterableWrapper({
+ this.latitude,
+ this.longitude,
+ this.isCluster,
+ this.clusterId,
+ this.pointsSize,
+ this.markerId,
+ this.childMarkerId,
+ });
+
+ Point? get location {
+ if (latitude == null || longitude == null) return null;
+
+ return Point(
+ latitude: latitude!,
+ longitude: longitude!,
+ );
+ }
+}
diff --git a/example/lib/presentation/state/custom_shape_map_strategy.dart b/example/lib/presentation/state/custom_shape_map_strategy.dart
index 028df52..bb2b682 100644
--- a/example/lib/presentation/state/custom_shape_map_strategy.dart
+++ b/example/lib/presentation/state/custom_shape_map_strategy.dart
@@ -91,4 +91,7 @@ class CustomShapeMapStrategyDelegate extends MapStrategyDelegate {
),
),
};
+
+ @override
+ String get title => "Custom map shapess";
}
diff --git a/example/lib/presentation/state/default_map_strategy.dart b/example/lib/presentation/state/default_map_strategy.dart
index 5a44429..3d96458 100644
--- a/example/lib/presentation/state/default_map_strategy.dart
+++ b/example/lib/presentation/state/default_map_strategy.dart
@@ -87,4 +87,7 @@ class DefaultIncrementMapStrategyDelegate extends MapStrategyDelegate {
),
),
};
+
+ @override
+ String get title => "Yandex Map Lite Overview";
}
diff --git a/example/lib/presentation/state/draggable_placemark_map_strategy.dart b/example/lib/presentation/state/draggable_placemark_map_strategy.dart
index 76a2453..8b29d08 100644
--- a/example/lib/presentation/state/draggable_placemark_map_strategy.dart
+++ b/example/lib/presentation/state/draggable_placemark_map_strategy.dart
@@ -88,4 +88,7 @@ class DraggablePlacemarkMapStrategyDelegate extends MapStrategyDelegate {
@override
bool get allowUserInteractions => false;
+
+ @override
+ String get title => "Draggable placemark";
}
diff --git a/example/lib/presentation/state/map_strategy.dart b/example/lib/presentation/state/map_strategy.dart
index ecfcee6..f40d67a 100644
--- a/example/lib/presentation/state/map_strategy.dart
+++ b/example/lib/presentation/state/map_strategy.dart
@@ -12,6 +12,9 @@ abstract class MapStrategyDelegate extends ChangeNotifier {
/// Set of map objects to be displayed on the map.
Set get mapObjects;
+ /// The title of the map strategy.
+ String get title;
+
/// Widget to be displayed as the floating action button on the screen.
///
/// This widget is used to provide user interactions with the map objects.
@@ -36,6 +39,18 @@ abstract class MapStrategyDelegate extends ChangeNotifier {
/// The implementation of this callback is optional.
Future? onUserLayer(UserLocationView view) => null;
+ /// Callback for user camera position change.
+ ///
+ /// Callback is called whenever user camera position on map changes.
+ ///
+ /// Allows us to introduce custom clusters on map.
+ void onCameraPositionChanged(
+ CameraPosition cameraPosition,
+ CameraUpdateReason reason,
+ bool finished,
+ VisibleRegion visibleRegion,
+ ) {}
+
/// Map controller.
///
/// This controller is used to interact with the map.
diff --git a/example/lib/presentation/state/traffic_map_strategy.dart b/example/lib/presentation/state/traffic_map_strategy.dart
index 20cd451..0ced668 100644
--- a/example/lib/presentation/state/traffic_map_strategy.dart
+++ b/example/lib/presentation/state/traffic_map_strategy.dart
@@ -77,4 +77,7 @@ class TrafficMapStrategyDelegate extends MapStrategyDelegate {
@override
bool get allowUserInteractions => false;
+
+ @override
+ String get title => "Traffic level layer";
}
diff --git a/example/lib/presentation/state/user_location_map_strategy.dart b/example/lib/presentation/state/user_location_map_strategy.dart
index e1a891a..b7e1314 100644
--- a/example/lib/presentation/state/user_location_map_strategy.dart
+++ b/example/lib/presentation/state/user_location_map_strategy.dart
@@ -59,4 +59,7 @@ class UserLocationMapStrategyDelegate extends MapStrategyDelegate {
@override
Set get mapObjects => {};
+
+ @override
+ String get title => "User location on the map";
}
diff --git a/example/lib/presentation/widgets/map_widget.dart b/example/lib/presentation/widgets/map_widget.dart
index dec3abb..7a1efbf 100644
--- a/example/lib/presentation/widgets/map_widget.dart
+++ b/example/lib/presentation/widgets/map_widget.dart
@@ -10,6 +10,8 @@ class MapWidget extends StatelessWidget {
final UserLocationCallback? onUserLocationUpdated;
+ final CameraPositionCallback? onCameraPositionChanged;
+
final bool allowUserInteractions;
const MapWidget({
@@ -17,6 +19,7 @@ class MapWidget extends StatelessWidget {
this.onControllerCreated,
this.onTrafficChanged,
this.onUserLocationUpdated,
+ this.onCameraPositionChanged,
this.allowUserInteractions = true,
Key? key,
}) : super(key: key);
@@ -25,7 +28,7 @@ class MapWidget extends StatelessWidget {
Widget build(BuildContext context) {
return YandexMap(
tiltGesturesEnabled: allowUserInteractions,
- rotateGesturesEnabled: allowUserInteractions,
+ rotateGesturesEnabled: false,
scrollGesturesEnabled: allowUserInteractions,
zoomGesturesEnabled: allowUserInteractions,
fastTapEnabled: true,
@@ -33,6 +36,7 @@ class MapWidget extends StatelessWidget {
mapObjects: mapObjects,
onTrafficChanged: onTrafficChanged,
onUserLocationAdded: onUserLocationUpdated,
+ onCameraPositionChanged: onCameraPositionChanged,
nightModeEnabled: Theme.of(context).brightness == Brightness.dark,
logoAlignment: const MapAlignment(
horizontal: HorizontalAlignment.left,
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index b57a73e..ed81fb5 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -14,6 +14,7 @@ dependencies:
permission_handler: ^10.4.0
yandex_mapkit_lite:
path: ../
+ fluster: ^1.2.0
dev_dependencies:
flutter_lints: ^2.0.2
diff --git a/ios/Classes/YandexMapController.swift b/ios/Classes/YandexMapController.swift
index 8b858ad..ddd1506 100755
--- a/ios/Classes/YandexMapController.swift
+++ b/ios/Classes/YandexMapController.swift
@@ -632,7 +632,8 @@ public class YandexMapController:
let arguments: [String: Any?] = [
"cameraPosition": Utils.cameraPositionToJson(cameraPosition),
"reason": cameraUpdateReason.rawValue,
- "finished": finished
+ "finished": finished,
+ "visibleRegion": Utils.visibleRegionToJson(map.visibleRegion(with: cameraPosition)),
]
methodChannel.invokeMethod("onCameraPositionChanged", arguments: arguments)
}
diff --git a/lib/src/types/callbacks.dart b/lib/src/types/callbacks.dart
index ca0e996..e679b3a 100644
--- a/lib/src/types/callbacks.dart
+++ b/lib/src/types/callbacks.dart
@@ -1,7 +1,7 @@
part of yandex_mapkit_lite;
-typedef CameraPositionCallback = void Function(
- CameraPosition cameraPosition, CameraUpdateReason reason, bool finished);
+typedef CameraPositionCallback = void Function(CameraPosition cameraPosition,
+ CameraUpdateReason reason, bool finished, VisibleRegion visibleRegion);
typedef ArgumentCallback = void Function(T argument);
typedef TapCallback = void Function(T mapObject, Point point);
typedef DragStartCallback = void Function(T mapObject);
diff --git a/lib/src/yandex_map_controller.dart b/lib/src/yandex_map_controller.dart
index a31ceac..769cea3 100755
--- a/lib/src/yandex_map_controller.dart
+++ b/lib/src/yandex_map_controller.dart
@@ -260,9 +260,11 @@ class YandexMapController extends ChangeNotifier {
}
_yandexMapState.widget.onCameraPositionChanged!(
- CameraPosition._fromJson(arguments['cameraPosition']),
- CameraUpdateReason.values[arguments['reason']],
- arguments['finished']);
+ CameraPosition._fromJson(arguments['cameraPosition']),
+ CameraUpdateReason.values[arguments['reason']],
+ arguments['finished'],
+ VisibleRegion._fromJson(arguments['visibleRegion']),
+ );
}
Future