diff --git a/README.md b/README.md index 59712f6..c5fffe9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ - - - - Shows an illustrated sun in light mode and a moon with stars in dark mode. - +

+yandex mapkit logo +

[![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 @@ - - - - Shows an illustrated sun in light mode and a moon with stars in dark mode. - +

+yandex mapkit logo +

[![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?> _onUserLocationAdded(dynamic arguments) async {