├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── app │ │ ├── .classpath │ │ ├── .project │ │ ├── .settings │ │ │ └── org.eclipse.buildship.core.prefs │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── settings_aar.gradle ├── example.gif ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── main.dart │ ├── multiple.dart │ └── place.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── google_maps_cluster_manager.dart └── src │ ├── cluster.dart │ ├── cluster_item.dart │ ├── cluster_manager.dart │ ├── common.dart │ ├── geohash.dart │ └── max_dist_clustering.dart ├── pubspec.yaml └── test └── common_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Api key files 19 | **/example/android/app/src/main/res/values/strings.xml 20 | **/example/ios/Runner/Secrets.swift 21 | 22 | # Visual Studio Code related 23 | .vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | pubspec.lock 32 | .pub-cache/ 33 | .pub/ 34 | build/ 35 | 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # iOS/XCode related 46 | **/ios/**/*.mode1v3 47 | **/ios/**/*.mode2v3 48 | **/ios/**/*.moved-aside 49 | **/ios/**/*.pbxuser 50 | **/ios/**/*.perspectivev3 51 | **/ios/**/*sync/ 52 | **/ios/**/.sconsign.dblite 53 | **/ios/**/.tags* 54 | **/ios/**/.vagrant/ 55 | **/ios/**/DerivedData/ 56 | **/ios/**/Icon? 57 | **/ios/**/Pods/ 58 | **/ios/**/.symlinks/ 59 | **/ios/**/profile 60 | **/ios/**/xcuserdata 61 | **/ios/.generated/ 62 | **/ios/Flutter/App.framework 63 | **/ios/Flutter/Flutter.framework 64 | **/ios/Flutter/Flutter.podspec 65 | **/ios/Flutter/Generated.xcconfig 66 | **/ios/Flutter/app.flx 67 | **/ios/Flutter/app.zip 68 | **/ios/Flutter/flutter_assets/ 69 | **/ios/Flutter/flutter_export_environment.sh 70 | **/ios/ServiceDefinitions.json 71 | **/ios/Runner/GeneratedPluginRegistrant.* 72 | 73 | # Exceptions to above rules. 74 | !**/ios/**/default.mode1v3 75 | !**/ios/**/default.mode2v3 76 | !**/ios/**/default.pbxuser 77 | !**/ios/**/default.perspectivev3 78 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 60bd88df915880d23877bfc1602e8ddcf4c4dd2a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.1.0 2 | 3 | - Bump dependency versions 4 | - Max distance clustering 5 | 6 | ## 3.0.0+1 7 | 8 | - Remove useless log 9 | 10 | ## 3.0.0 11 | 12 | **Breaking changes**: 13 | 14 | - `ClusterItem` is now a mixin (or a class to extends from) instead of a wrapper around items. This way you don't have to map your items to ClusterItems before using them. 15 | - Remove now useless `initialZoom` parameter. 16 | 17 | ## 2.0.0 18 | 19 | **Breaking changes**: 20 | 21 | - Use mapId (with `setMapId` method) to retrieve the map instead of GoogleMapController. This way, the library depends only on `google_maps_flutter_platform_interface` which makes it compatible both with `google_maps_flutter` and `google_maps_flutter_web`. 22 | 23 | ## 1.0.0 24 | 25 | - Migrate to null safety 26 | - Internalising geohash to make it null safety compatible 27 | - Temporary : remove `google_maps_flutter_web` because it needs a reorganization of the project to work correctly (& it's not null safety compatible for the moment) 28 | 29 | ## 0.3.0 30 | 31 | - Add `google_maps_flutter_web` dependency to be compatible with Flutter web 32 | - Update to `google_maps_flutter` version 1.2.0 33 | 34 | ## 0.2.1 35 | 36 | - Improve potential precision of geohash 37 | - Update to `google_maps_flutter` version 1.0.6 38 | 39 | ## 0.2.0 40 | 41 | - Add `stopClusteringZoom` variable 42 | - Update to `google_maps_flutter` version 1.0.2 43 | - Improve `extraPercent` calculation (thanks to @buntagonalprism) 44 | 45 | ## 0.1.0 46 | 47 | - Fix `getMarkers` signature 48 | - Add gif example 49 | 50 | ## 0.0.2 51 | 52 | - Add `setItems` and `addItem` methods 53 | - Add initial zoom 54 | 55 | ## 0.0.1 56 | 57 | - Initial developers preview release. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Baptiste Pillon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/google_maps_cluster_manager.svg)](https://pub.dartlang.org/packages/google_maps_cluster_manager) 2 | 3 | # Flutter Cluster Manager for Google Maps 4 | 5 | ![Screenshot](https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/master/example/example.gif) 6 | 7 | A Flutter package to cluster items on a [Google Maps](https://pub.dev/packages/google_maps_flutter) widget based on Geohash. Highly inspired by [clustering_google_maps](https://pub.dev/packages/clustering_google_maps) 8 | 9 | ## Usage 10 | 11 | To use this package, add `google_maps_cluster_manager` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). 12 | 13 | ### Getting Started 14 | 15 | Your map items has to use `ClusterItem` as a mixin (or extends this class) and implements the `LatLng location` getter. 16 | 17 | ```dart 18 | class Place with ClusterItem { 19 | final String name; 20 | final LatLng latLng; 21 | 22 | Place({required this.name, required this.latLng}); 23 | 24 | @override 25 | LatLng get location => latLng; 26 | } 27 | ``` 28 | 29 | To start with Cluster Manager, you have to initialize a `ClusterManager` instance. 30 | 31 | ```dart 32 | ClusterManager( 33 | _items, // Your items to be clustered on the map (of Place type for this example) 34 | _updateMarkers, // Method to be called when markers are updated 35 | markerBuilder: _markerBuilder, // Optional : Method to implement if you want to customize markers 36 | levels: [1, 4.25, 6.75, 8.25, 11.5, 14.5, 16.0, 16.5, 20.0], // Optional : Configure this if you want to change zoom levels at which the clustering precision change 37 | extraPercent: 0.2, // Optional : This number represents the percentage (0.2 for 20%) of latitude and longitude (in each direction) to be considered on top of the visible map bounds to render clusters. This way, clusters don't "pop out" when you cross the map. 38 | stopClusteringZoom: 17.0 // Optional : The zoom level to stop clustering, so it's only rendering single item "clusters" 39 | ); 40 | ``` 41 | 42 | When your `GoogleMapController` is created, you have to set the `mapId` with the `setMapId` method : 43 | 44 | ```dart 45 | _manager.setMapId(controller.mapId); 46 | ``` 47 | 48 | You are able to add an new item to the map by calling `addItem` method on your `ClusterManager` instance. You can also completely change the items on your maps by calling `setItems` method. 49 | 50 | You can customize the icon of a cluster by using `Future Function(Cluster) markerBuilder` parameter. 51 | 52 | ```dart 53 | static Future Function(Cluster) get markerBuilder => (cluster) async { 54 | return Marker( 55 | markerId: MarkerId(cluster.getId()), 56 | position: cluster.location, 57 | onTap: () { 58 | print(cluster.items); 59 | }, 60 | icon: await getClusterBitmap(cluster.isMultiple ? 125 : 75, 61 | text: cluster.isMultiple? cluster.count.toString() : null), 62 | ); 63 | }; 64 | 65 | static Future getClusterBitmap(int size, {String text?}) async { 66 | final PictureRecorder pictureRecorder = PictureRecorder(); 67 | final Canvas canvas = Canvas(pictureRecorder); 68 | final Paint paint1 = Paint()..color = Colors.red; 69 | 70 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.0, paint1); 71 | 72 | if (text != null) { 73 | TextPainter painter = TextPainter(textDirection: TextDirection.ltr); 74 | painter.text = TextSpan( 75 | text: text, 76 | style: TextStyle( 77 | fontSize: size / 3, 78 | color: Colors.white, 79 | fontWeight: FontWeight.normal), 80 | ); 81 | painter.layout(); 82 | painter.paint( 83 | canvas, 84 | Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2), 85 | ); 86 | } 87 | 88 | final img = await pictureRecorder.endRecording().toImage(size, size); 89 | final data = await img.toByteData(format: ImageByteFormat.png); 90 | 91 | return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); 92 | } 93 | ``` 94 | 95 | Every cluster (even one item clusters) is rendered by the library as a `Cluster` object. You can differentiate single item clusters from multiple ones by using the `isMultiple` variable (or the `count` variable). This way, you can create different markers icon depending on the type of cluster. 96 | 97 | You can create multiple managers for a single map, see the `multiple.dart` example. 98 | 99 | ## Complete Basic Example 100 | 101 | See the `example` directory for a complete sample app. 102 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /example/android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.example" 42 | minSdkVersion 20 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /example/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/example.gif -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - google_maps_flutter (0.0.1): 4 | - Flutter 5 | - GoogleMaps (< 3.10) 6 | - GoogleMaps (2.7.0): 7 | - GoogleMaps/Maps (= 2.7.0) 8 | - GoogleMaps/Base (2.7.0) 9 | - GoogleMaps/Maps (2.7.0): 10 | - GoogleMaps/Base 11 | 12 | DEPENDENCIES: 13 | - Flutter (from `Flutter`) 14 | - google_maps_flutter (from `.symlinks/plugins/google_maps_flutter/ios`) 15 | 16 | SPEC REPOS: 17 | trunk: 18 | - GoogleMaps 19 | 20 | EXTERNAL SOURCES: 21 | Flutter: 22 | :path: Flutter 23 | google_maps_flutter: 24 | :path: ".symlinks/plugins/google_maps_flutter/ios" 25 | 26 | SPEC CHECKSUMS: 27 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 28 | google_maps_flutter: c7f9c73576de1fbe152a227bfd6e6c4ae8088619 29 | GoogleMaps: f79af95cb24d869457b1f961c93d3ce8b2f3b848 30 | 31 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 32 | 33 | COCOAPODS: 1.10.0 34 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 48C35BA7188BCF2902120131 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D132D8F7C926F86637A09B04 /* Pods_Runner.framework */; }; 13 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | AA57C23823EC47B5000F7E32 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA57C23723EC47B5000F7E32 /* Secrets.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 37 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 38 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 40 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 41 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 42 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | A3CCF6DE356FD700C298511D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 48 | AA57C23723EC47B5000F7E32 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; 49 | BE2F43A0E0E3A1537A30CB5B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 50 | D132D8F7C926F86637A09B04 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | F745DC3C915F73FE6220E39A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 48C35BA7188BCF2902120131 /* Pods_Runner.framework in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 28117D5A0F63700697E82652 /* Frameworks */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | D132D8F7C926F86637A09B04 /* Pods_Runner.framework */, 70 | ); 71 | name = Frameworks; 72 | sourceTree = ""; 73 | }; 74 | 53CD689AFA9A35044B2382EE /* Pods */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | BE2F43A0E0E3A1537A30CB5B /* Pods-Runner.debug.xcconfig */, 78 | A3CCF6DE356FD700C298511D /* Pods-Runner.release.xcconfig */, 79 | F745DC3C915F73FE6220E39A /* Pods-Runner.profile.xcconfig */, 80 | ); 81 | path = Pods; 82 | sourceTree = ""; 83 | }; 84 | 9740EEB11CF90186004384FC /* Flutter */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 88 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 89 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 90 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 91 | ); 92 | name = Flutter; 93 | sourceTree = ""; 94 | }; 95 | 97C146E51CF9000F007C117D = { 96 | isa = PBXGroup; 97 | children = ( 98 | 9740EEB11CF90186004384FC /* Flutter */, 99 | 97C146F01CF9000F007C117D /* Runner */, 100 | 97C146EF1CF9000F007C117D /* Products */, 101 | 53CD689AFA9A35044B2382EE /* Pods */, 102 | 28117D5A0F63700697E82652 /* Frameworks */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 97C146EF1CF9000F007C117D /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 97C146EE1CF9000F007C117D /* Runner.app */, 110 | ); 111 | name = Products; 112 | sourceTree = ""; 113 | }; 114 | 97C146F01CF9000F007C117D /* Runner */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 118 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 119 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 120 | 97C147021CF9000F007C117D /* Info.plist */, 121 | 97C146F11CF9000F007C117D /* Supporting Files */, 122 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 123 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 124 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 125 | AA57C23723EC47B5000F7E32 /* Secrets.swift */, 126 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 127 | ); 128 | path = Runner; 129 | sourceTree = ""; 130 | }; 131 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | ); 135 | name = "Supporting Files"; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 97C146ED1CF9000F007C117D /* Runner */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 144 | buildPhases = ( 145 | 6DFBA8899E2B5988168CD5B6 /* [CP] Check Pods Manifest.lock */, 146 | 9740EEB61CF901F6004384FC /* Run Script */, 147 | 97C146EA1CF9000F007C117D /* Sources */, 148 | 97C146EB1CF9000F007C117D /* Frameworks */, 149 | 97C146EC1CF9000F007C117D /* Resources */, 150 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 151 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 152 | 6DB16532DF84B8C1E092BC51 /* [CP] Copy Pods Resources */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | ); 158 | name = Runner; 159 | productName = Runner; 160 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 161 | productType = "com.apple.product-type.application"; 162 | }; 163 | /* End PBXNativeTarget section */ 164 | 165 | /* Begin PBXProject section */ 166 | 97C146E61CF9000F007C117D /* Project object */ = { 167 | isa = PBXProject; 168 | attributes = { 169 | LastUpgradeCheck = 1020; 170 | ORGANIZATIONNAME = "The Chromium Authors"; 171 | TargetAttributes = { 172 | 97C146ED1CF9000F007C117D = { 173 | CreatedOnToolsVersion = 7.3.1; 174 | DevelopmentTeam = Q46ADN6S6V; 175 | LastSwiftMigration = 1100; 176 | }; 177 | }; 178 | }; 179 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 180 | compatibilityVersion = "Xcode 3.2"; 181 | developmentRegion = en; 182 | hasScannedForEncodings = 0; 183 | knownRegions = ( 184 | en, 185 | Base, 186 | ); 187 | mainGroup = 97C146E51CF9000F007C117D; 188 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 189 | projectDirPath = ""; 190 | projectRoot = ""; 191 | targets = ( 192 | 97C146ED1CF9000F007C117D /* Runner */, 193 | ); 194 | }; 195 | /* End PBXProject section */ 196 | 197 | /* Begin PBXResourcesBuildPhase section */ 198 | 97C146EC1CF9000F007C117D /* Resources */ = { 199 | isa = PBXResourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 203 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 204 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 205 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXShellScriptBuildPhase section */ 212 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 213 | isa = PBXShellScriptBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | ); 217 | inputPaths = ( 218 | ); 219 | name = "Thin Binary"; 220 | outputPaths = ( 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | shellPath = /bin/sh; 224 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 225 | }; 226 | 6DB16532DF84B8C1E092BC51 /* [CP] Copy Pods Resources */ = { 227 | isa = PBXShellScriptBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | inputPaths = ( 232 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", 233 | "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", 234 | ); 235 | name = "[CP] Copy Pods Resources"; 236 | outputPaths = ( 237 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | shellPath = /bin/sh; 241 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 242 | showEnvVarsInLog = 0; 243 | }; 244 | 6DFBA8899E2B5988168CD5B6 /* [CP] Check Pods Manifest.lock */ = { 245 | isa = PBXShellScriptBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | inputFileListPaths = ( 250 | ); 251 | inputPaths = ( 252 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 253 | "${PODS_ROOT}/Manifest.lock", 254 | ); 255 | name = "[CP] Check Pods Manifest.lock"; 256 | outputFileListPaths = ( 257 | ); 258 | outputPaths = ( 259 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | shellPath = /bin/sh; 263 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 264 | showEnvVarsInLog = 0; 265 | }; 266 | 9740EEB61CF901F6004384FC /* Run Script */ = { 267 | isa = PBXShellScriptBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | inputPaths = ( 272 | ); 273 | name = "Run Script"; 274 | outputPaths = ( 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | shellPath = /bin/sh; 278 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 279 | }; 280 | /* End PBXShellScriptBuildPhase section */ 281 | 282 | /* Begin PBXSourcesBuildPhase section */ 283 | 97C146EA1CF9000F007C117D /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 288 | AA57C23823EC47B5000F7E32 /* Secrets.swift in Sources */, 289 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | /* End PBXSourcesBuildPhase section */ 294 | 295 | /* Begin PBXVariantGroup section */ 296 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | 97C146FB1CF9000F007C117D /* Base */, 300 | ); 301 | name = Main.storyboard; 302 | sourceTree = ""; 303 | }; 304 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 305 | isa = PBXVariantGroup; 306 | children = ( 307 | 97C147001CF9000F007C117D /* Base */, 308 | ); 309 | name = LaunchScreen.storyboard; 310 | sourceTree = ""; 311 | }; 312 | /* End PBXVariantGroup section */ 313 | 314 | /* Begin XCBuildConfiguration section */ 315 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ALWAYS_SEARCH_USER_PATHS = NO; 319 | CLANG_ANALYZER_NONNULL = YES; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_COMMA = YES; 327 | CLANG_WARN_CONSTANT_CONVERSION = YES; 328 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 329 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 330 | CLANG_WARN_EMPTY_BODY = YES; 331 | CLANG_WARN_ENUM_CONVERSION = YES; 332 | CLANG_WARN_INFINITE_RECURSION = YES; 333 | CLANG_WARN_INT_CONVERSION = YES; 334 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 336 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 337 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 338 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 339 | CLANG_WARN_STRICT_PROTOTYPES = YES; 340 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 346 | ENABLE_NS_ASSERTIONS = NO; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | GCC_C_LANGUAGE_STANDARD = gnu99; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 357 | MTL_ENABLE_DEBUG_INFO = NO; 358 | SDKROOT = iphoneos; 359 | SUPPORTED_PLATFORMS = iphoneos; 360 | TARGETED_DEVICE_FAMILY = "1,2"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Profile; 364 | }; 365 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 366 | isa = XCBuildConfiguration; 367 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 368 | buildSettings = { 369 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 370 | CLANG_ENABLE_MODULES = YES; 371 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 372 | DEVELOPMENT_TEAM = Q46ADN6S6V; 373 | ENABLE_BITCODE = NO; 374 | FRAMEWORK_SEARCH_PATHS = ( 375 | "$(inherited)", 376 | "$(PROJECT_DIR)/Flutter", 377 | ); 378 | INFOPLIST_FILE = Runner/Info.plist; 379 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 380 | LIBRARY_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "$(PROJECT_DIR)/Flutter", 383 | ); 384 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 387 | SWIFT_VERSION = 5.0; 388 | VERSIONING_SYSTEM = "apple-generic"; 389 | }; 390 | name = Profile; 391 | }; 392 | 97C147031CF9000F007C117D /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ALWAYS_SEARCH_USER_PATHS = NO; 396 | CLANG_ANALYZER_NONNULL = YES; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 398 | CLANG_CXX_LIBRARY = "libc++"; 399 | CLANG_ENABLE_MODULES = YES; 400 | CLANG_ENABLE_OBJC_ARC = YES; 401 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_COMMA = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 406 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 407 | CLANG_WARN_EMPTY_BODY = YES; 408 | CLANG_WARN_ENUM_CONVERSION = YES; 409 | CLANG_WARN_INFINITE_RECURSION = YES; 410 | CLANG_WARN_INT_CONVERSION = YES; 411 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 413 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 415 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 416 | CLANG_WARN_STRICT_PROTOTYPES = YES; 417 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 418 | CLANG_WARN_UNREACHABLE_CODE = YES; 419 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 420 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 421 | COPY_PHASE_STRIP = NO; 422 | DEBUG_INFORMATION_FORMAT = dwarf; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | ENABLE_TESTABILITY = YES; 425 | GCC_C_LANGUAGE_STANDARD = gnu99; 426 | GCC_DYNAMIC_NO_PIC = NO; 427 | GCC_NO_COMMON_BLOCKS = YES; 428 | GCC_OPTIMIZATION_LEVEL = 0; 429 | GCC_PREPROCESSOR_DEFINITIONS = ( 430 | "DEBUG=1", 431 | "$(inherited)", 432 | ); 433 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 434 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 435 | GCC_WARN_UNDECLARED_SELECTOR = YES; 436 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 437 | GCC_WARN_UNUSED_FUNCTION = YES; 438 | GCC_WARN_UNUSED_VARIABLE = YES; 439 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 440 | MTL_ENABLE_DEBUG_INFO = YES; 441 | ONLY_ACTIVE_ARCH = YES; 442 | SDKROOT = iphoneos; 443 | TARGETED_DEVICE_FAMILY = "1,2"; 444 | }; 445 | name = Debug; 446 | }; 447 | 97C147041CF9000F007C117D /* Release */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | ALWAYS_SEARCH_USER_PATHS = NO; 451 | CLANG_ANALYZER_NONNULL = YES; 452 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 453 | CLANG_CXX_LIBRARY = "libc++"; 454 | CLANG_ENABLE_MODULES = YES; 455 | CLANG_ENABLE_OBJC_ARC = YES; 456 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 457 | CLANG_WARN_BOOL_CONVERSION = YES; 458 | CLANG_WARN_COMMA = YES; 459 | CLANG_WARN_CONSTANT_CONVERSION = YES; 460 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 461 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 462 | CLANG_WARN_EMPTY_BODY = YES; 463 | CLANG_WARN_ENUM_CONVERSION = YES; 464 | CLANG_WARN_INFINITE_RECURSION = YES; 465 | CLANG_WARN_INT_CONVERSION = YES; 466 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 467 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 468 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 469 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 470 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 471 | CLANG_WARN_STRICT_PROTOTYPES = YES; 472 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 473 | CLANG_WARN_UNREACHABLE_CODE = YES; 474 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 475 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 476 | COPY_PHASE_STRIP = NO; 477 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 478 | ENABLE_NS_ASSERTIONS = NO; 479 | ENABLE_STRICT_OBJC_MSGSEND = YES; 480 | GCC_C_LANGUAGE_STANDARD = gnu99; 481 | GCC_NO_COMMON_BLOCKS = YES; 482 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 483 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 484 | GCC_WARN_UNDECLARED_SELECTOR = YES; 485 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 486 | GCC_WARN_UNUSED_FUNCTION = YES; 487 | GCC_WARN_UNUSED_VARIABLE = YES; 488 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 489 | MTL_ENABLE_DEBUG_INFO = NO; 490 | SDKROOT = iphoneos; 491 | SUPPORTED_PLATFORMS = iphoneos; 492 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 493 | TARGETED_DEVICE_FAMILY = "1,2"; 494 | VALIDATE_PRODUCT = YES; 495 | }; 496 | name = Release; 497 | }; 498 | 97C147061CF9000F007C117D /* Debug */ = { 499 | isa = XCBuildConfiguration; 500 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 501 | buildSettings = { 502 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 503 | CLANG_ENABLE_MODULES = YES; 504 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 505 | DEVELOPMENT_TEAM = Q46ADN6S6V; 506 | ENABLE_BITCODE = NO; 507 | FRAMEWORK_SEARCH_PATHS = ( 508 | "$(inherited)", 509 | "$(PROJECT_DIR)/Flutter", 510 | ); 511 | INFOPLIST_FILE = Runner/Info.plist; 512 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 513 | LIBRARY_SEARCH_PATHS = ( 514 | "$(inherited)", 515 | "$(PROJECT_DIR)/Flutter", 516 | ); 517 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 518 | PRODUCT_NAME = "$(TARGET_NAME)"; 519 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 520 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 521 | SWIFT_VERSION = 5.0; 522 | VERSIONING_SYSTEM = "apple-generic"; 523 | }; 524 | name = Debug; 525 | }; 526 | 97C147071CF9000F007C117D /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 531 | CLANG_ENABLE_MODULES = YES; 532 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 533 | DEVELOPMENT_TEAM = Q46ADN6S6V; 534 | ENABLE_BITCODE = NO; 535 | FRAMEWORK_SEARCH_PATHS = ( 536 | "$(inherited)", 537 | "$(PROJECT_DIR)/Flutter", 538 | ); 539 | INFOPLIST_FILE = Runner/Info.plist; 540 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 541 | LIBRARY_SEARCH_PATHS = ( 542 | "$(inherited)", 543 | "$(PROJECT_DIR)/Flutter", 544 | ); 545 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 548 | SWIFT_VERSION = 5.0; 549 | VERSIONING_SYSTEM = "apple-generic"; 550 | }; 551 | name = Release; 552 | }; 553 | /* End XCBuildConfiguration section */ 554 | 555 | /* Begin XCConfigurationList section */ 556 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 557 | isa = XCConfigurationList; 558 | buildConfigurations = ( 559 | 97C147031CF9000F007C117D /* Debug */, 560 | 97C147041CF9000F007C117D /* Release */, 561 | 249021D3217E4FDB00AE95B9 /* Profile */, 562 | ); 563 | defaultConfigurationIsVisible = 0; 564 | defaultConfigurationName = Release; 565 | }; 566 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 567 | isa = XCConfigurationList; 568 | buildConfigurations = ( 569 | 97C147061CF9000F007C117D /* Debug */, 570 | 97C147071CF9000F007C117D /* Release */, 571 | 249021D4217E4FDB00AE95B9 /* Profile */, 572 | ); 573 | defaultConfigurationIsVisible = 0; 574 | defaultConfigurationName = Release; 575 | }; 576 | /* End XCConfigurationList section */ 577 | }; 578 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 579 | } 580 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import GoogleMaps 4 | 5 | @UIApplicationMain 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | GMSServices.provideAPIKey(Secrets.apiKey) 12 | GeneratedPluginRegistrant.register(with: self) 13 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | io.flutter.embedded_views_preview 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:example/place.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 9 | import 'package:google_maps_flutter/google_maps_flutter.dart'; 10 | 11 | void main() => runApp(MyApp()); 12 | 13 | class MyApp extends StatelessWidget { 14 | // This widget is the root of your application. 15 | @override 16 | Widget build(BuildContext context) { 17 | return MaterialApp( 18 | title: 'Cluster Manager Demo', 19 | home: MapSample(), 20 | ); 21 | } 22 | } 23 | 24 | // Clustering maps 25 | 26 | class MapSample extends StatefulWidget { 27 | @override 28 | State createState() => MapSampleState(); 29 | } 30 | 31 | class MapSampleState extends State { 32 | late ClusterManager _manager; 33 | 34 | Completer _controller = Completer(); 35 | 36 | Set markers = Set(); 37 | 38 | final CameraPosition _parisCameraPosition = 39 | CameraPosition(target: LatLng(48.856613, 2.352222), zoom: 12.0); 40 | 41 | List items = [ 42 | for (int i = 0; i < 10; i++) 43 | Place( 44 | name: 'Place $i', 45 | latLng: LatLng(48.848200 + i * 0.001, 2.319124 + i * 0.001)), 46 | for (int i = 0; i < 10; i++) 47 | Place( 48 | name: 'Restaurant $i', 49 | isClosed: i % 2 == 0, 50 | latLng: LatLng(48.858265 - i * 0.001, 2.350107 + i * 0.001)), 51 | for (int i = 0; i < 10; i++) 52 | Place( 53 | name: 'Bar $i', 54 | latLng: LatLng(48.858265 + i * 0.01, 2.350107 - i * 0.01)), 55 | for (int i = 0; i < 10; i++) 56 | Place( 57 | name: 'Hotel $i', 58 | latLng: LatLng(48.858265 - i * 0.1, 2.350107 - i * 0.01)), 59 | for (int i = 0; i < 10; i++) 60 | Place( 61 | name: 'Test $i', 62 | latLng: LatLng(66.160507 + i * 0.1, -153.369141 + i * 0.1)), 63 | for (int i = 0; i < 10; i++) 64 | Place( 65 | name: 'Test2 $i', 66 | latLng: LatLng(-36.848461 + i * 1, 169.763336 + i * 1)), 67 | ]; 68 | 69 | @override 70 | void initState() { 71 | _manager = _initClusterManager(); 72 | super.initState(); 73 | } 74 | 75 | ClusterManager _initClusterManager() { 76 | return ClusterManager(items, _updateMarkers, 77 | markerBuilder: _markerBuilder); 78 | } 79 | 80 | void _updateMarkers(Set markers) { 81 | print('Updated ${markers.length} markers'); 82 | setState(() { 83 | this.markers = markers; 84 | }); 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | return new Scaffold( 90 | body: GoogleMap( 91 | mapType: MapType.normal, 92 | initialCameraPosition: _parisCameraPosition, 93 | markers: markers, 94 | onMapCreated: (GoogleMapController controller) { 95 | _controller.complete(controller); 96 | _manager.setMapId(controller.mapId); 97 | }, 98 | onCameraMove: _manager.onCameraMove, 99 | onCameraIdle: _manager.updateMap), 100 | floatingActionButton: FloatingActionButton( 101 | onPressed: () { 102 | _manager.setItems([ 103 | for (int i = 0; i < 30; i++) 104 | Place( 105 | name: 'New Place ${DateTime.now()} $i', 106 | latLng: LatLng(48.858265 + i * 0.01, 2.350107)) 107 | ]); 108 | }, 109 | child: Icon(Icons.update), 110 | ), 111 | ); 112 | } 113 | 114 | Future Function(Cluster) get _markerBuilder => 115 | (cluster) async { 116 | return Marker( 117 | markerId: MarkerId(cluster.getId()), 118 | position: cluster.location, 119 | onTap: () { 120 | print('---- $cluster'); 121 | cluster.items.forEach((p) => print(p)); 122 | }, 123 | icon: await _getMarkerBitmap(cluster.isMultiple ? 125 : 75, 124 | text: cluster.isMultiple ? cluster.count.toString() : null), 125 | ); 126 | }; 127 | 128 | Future _getMarkerBitmap(int size, {String? text}) async { 129 | if (kIsWeb) size = (size / 2).floor(); 130 | 131 | final PictureRecorder pictureRecorder = PictureRecorder(); 132 | final Canvas canvas = Canvas(pictureRecorder); 133 | final Paint paint1 = Paint()..color = Colors.orange; 134 | final Paint paint2 = Paint()..color = Colors.white; 135 | 136 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.0, paint1); 137 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.2, paint2); 138 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.8, paint1); 139 | 140 | if (text != null) { 141 | TextPainter painter = TextPainter(textDirection: TextDirection.ltr); 142 | painter.text = TextSpan( 143 | text: text, 144 | style: TextStyle( 145 | fontSize: size / 3, 146 | color: Colors.white, 147 | fontWeight: FontWeight.normal), 148 | ); 149 | painter.layout(); 150 | painter.paint( 151 | canvas, 152 | Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2), 153 | ); 154 | } 155 | 156 | final img = await pictureRecorder.endRecording().toImage(size, size); 157 | final data = await img.toByteData(format: ImageByteFormat.png) as ByteData; 158 | 159 | return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /example/lib/multiple.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:example/place.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 9 | import 'package:google_maps_flutter/google_maps_flutter.dart'; 10 | 11 | void main() => runApp(MyApp()); 12 | 13 | class MyApp extends StatelessWidget { 14 | // This widget is the root of your application. 15 | @override 16 | Widget build(BuildContext context) { 17 | return MaterialApp( 18 | title: 'Cluster Manager Demo', 19 | home: MapSample(), 20 | ); 21 | } 22 | } 23 | 24 | // Clustering maps 25 | 26 | class MapSample extends StatefulWidget { 27 | @override 28 | State createState() => MapSampleState(); 29 | } 30 | 31 | class MapSampleState extends State { 32 | late ClusterManager _manager; 33 | late ClusterManager _manager2; 34 | 35 | Completer _controller = Completer(); 36 | 37 | Set markers = Set(); 38 | Set markers2 = Set(); 39 | 40 | final CameraPosition _parisCameraPosition = 41 | CameraPosition(target: LatLng(48.856613, 2.352222), zoom: 12.0); 42 | 43 | List items = [ 44 | for (int i = 0; i < 10; i++) 45 | Place( 46 | name: 'Restaurant $i', 47 | isClosed: i % 2 == 0, 48 | latLng: LatLng(48.858265 - i * 0.001, 2.350107 + i * 0.001)), 49 | for (int i = 0; i < 10; i++) 50 | Place( 51 | name: 'Bar $i', 52 | latLng: LatLng(48.858265 + i * 0.01, 2.350107 - i * 0.01)), 53 | for (int i = 0; i < 10; i++) 54 | Place( 55 | name: 'Hotel $i', 56 | latLng: LatLng(48.858265 - i * 0.1, 2.350107 - i * 0.01)), 57 | ]; 58 | 59 | List items2 = [ 60 | for (int i = 0; i < 10; i++) 61 | Place( 62 | name: 'Place $i', 63 | latLng: LatLng(48.848200 + i * 0.001, 2.319124 + i * 0.001)), 64 | for (int i = 0; i < 10; i++) 65 | Place( 66 | name: 'Test $i', 67 | latLng: LatLng(48.858265 + i * 0.1, 2.350107 + i * 0.1)), 68 | for (int i = 0; i < 10; i++) 69 | Place( 70 | name: 'Test2 $i', 71 | latLng: LatLng(48.858265 + i * 1, 2.350107 + i * 1)), 72 | ]; 73 | 74 | @override 75 | void initState() { 76 | _manager = ClusterManager(items, _updateMarkers, 77 | markerBuilder: _getMarkerBuilder(Colors.red)); 78 | 79 | _manager2 = ClusterManager(items2, _updateMarkers2, 80 | markerBuilder: _getMarkerBuilder(Colors.blue)); 81 | super.initState(); 82 | } 83 | 84 | void _updateMarkers(Set markers) { 85 | setState(() { 86 | this.markers = markers; 87 | }); 88 | } 89 | 90 | void _updateMarkers2(Set markers) { 91 | setState(() { 92 | this.markers2 = markers; 93 | }); 94 | } 95 | 96 | @override 97 | Widget build(BuildContext context) { 98 | return new Scaffold( 99 | body: GoogleMap( 100 | zoomControlsEnabled: false, 101 | mapType: MapType.normal, 102 | initialCameraPosition: _parisCameraPosition, 103 | markers: markers..addAll(markers2), 104 | onMapCreated: (GoogleMapController controller) { 105 | _controller.complete(controller); 106 | _manager.setMapId(controller.mapId); 107 | _manager2.setMapId(controller.mapId); 108 | }, 109 | onCameraMove: (position) { 110 | _manager.onCameraMove(position); 111 | _manager2.onCameraMove(position); 112 | }, 113 | onCameraIdle: () { 114 | _manager.updateMap(); 115 | _manager2.updateMap(); 116 | }), 117 | floatingActionButton: FloatingActionButton( 118 | onPressed: () { 119 | _manager.setItems([ 120 | for (int i = 0; i < 30; i++) 121 | Place( 122 | name: 'New Place ${DateTime.now()} $i', 123 | latLng: LatLng(48.858265 + i * 0.01, 2.350107)) 124 | ]); 125 | }, 126 | child: Icon(Icons.update), 127 | ), 128 | ); 129 | } 130 | 131 | Future Function(Cluster) _getMarkerBuilder(Color color) => 132 | (cluster) async { 133 | return Marker( 134 | markerId: MarkerId(cluster.getId()), 135 | position: cluster.location, 136 | onTap: () { 137 | print('---- $cluster'); 138 | cluster.items.forEach((p) => print(p)); 139 | }, 140 | icon: await _getMarkerBitmap(cluster.isMultiple ? 125 : 75, color, 141 | text: cluster.isMultiple ? cluster.count.toString() : null), 142 | ); 143 | }; 144 | 145 | Future _getMarkerBitmap(int size, Color color, 146 | {String? text}) async { 147 | if (kIsWeb) size = (size / 2).floor(); 148 | 149 | final PictureRecorder pictureRecorder = PictureRecorder(); 150 | final Canvas canvas = Canvas(pictureRecorder); 151 | final Paint paint1 = Paint()..color = color; 152 | final Paint paint2 = Paint()..color = Colors.white; 153 | 154 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.0, paint1); 155 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.2, paint2); 156 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.8, paint1); 157 | 158 | if (text != null) { 159 | TextPainter painter = TextPainter(textDirection: TextDirection.ltr); 160 | painter.text = TextSpan( 161 | text: text, 162 | style: TextStyle( 163 | fontSize: size / 3, 164 | color: Colors.white, 165 | fontWeight: FontWeight.normal), 166 | ); 167 | painter.layout(); 168 | painter.paint( 169 | canvas, 170 | Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2), 171 | ); 172 | } 173 | 174 | final img = await pictureRecorder.endRecording().toImage(size, size); 175 | final data = await img.toByteData(format: ImageByteFormat.png) as ByteData; 176 | 177 | return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /example/lib/place.dart: -------------------------------------------------------------------------------- 1 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 2 | import 'package:google_maps_flutter/google_maps_flutter.dart'; 3 | 4 | class Place with ClusterItem { 5 | final String name; 6 | final bool isClosed; 7 | final LatLng latLng; 8 | 9 | Place({required this.name, required this.latLng, this.isClosed = false}); 10 | 11 | @override 12 | String toString() { 13 | return 'Place $name (closed : $isClosed)'; 14 | } 15 | 16 | @override 17 | LatLng get location => latLng; 18 | } 19 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=3.0.0 <4.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | google_maps_flutter: ^2.5.2 24 | google_maps_flutter_web: ^0.5.4+3 25 | google_maps_cluster_manager: 26 | path: ../ 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://dart.dev/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | # To add assets to your application, add an assets section, like this: 42 | # assets: 43 | # - images/a_dot_burr.jpeg 44 | # - images/a_dot_ham.jpeg 45 | # An image asset can refer to one or more resolution-specific "variants", see 46 | # https://flutter.dev/assets-and-images/#resolution-aware. 47 | # For details regarding adding assets from package dependencies, see 48 | # https://flutter.dev/assets-and-images/#from-packages 49 | # To add custom fonts to your application, add a fonts section here, 50 | # in this "flutter" section. Each entry in this list should have a 51 | # "family" key with the font family name, and a "fonts" key with a 52 | # list giving the asset and other descriptors for the font. For 53 | # example: 54 | # fonts: 55 | # - family: Schyler 56 | # fonts: 57 | # - asset: fonts/Schyler-Regular.ttf 58 | # - asset: fonts/Schyler-Italic.ttf 59 | # style: italic 60 | # - family: Trajan Pro 61 | # fonts: 62 | # - asset: fonts/TrajanPro.ttf 63 | # - asset: fonts/TrajanPro_Bold.ttf 64 | # weight: 700 65 | # 66 | # For details regarding fonts from package dependencies, 67 | # see https://flutter.dev/custom-fonts/#from-packages 68 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // // Build our app and trigger a frame. 16 | // await tester.pumpWidget(MyApp()); 17 | 18 | // // Verify that our counter starts at 0. 19 | // expect(find.text('0'), findsOneWidget); 20 | // expect(find.text('1'), findsNothing); 21 | 22 | // // Tap the '+' icon and trigger a frame. 23 | // await tester.tap(find.byIcon(Icons.add)); 24 | // await tester.pump(); 25 | 26 | // // Verify that our counter has incremented. 27 | // expect(find.text('0'), findsNothing); 28 | // expect(find.text('1'), findsOneWidget); 29 | // }); 30 | } 31 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpillon/google_maps_cluster_manager/7ab2a6363f70cbc775a84fb09a0abcf0d546cdae/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | example 27 | 28 | 29 | 30 | 31 | 34 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/google_maps_cluster_manager.dart: -------------------------------------------------------------------------------- 1 | library google_maps_cluster_manager; 2 | 3 | export 'src/cluster_manager.dart'; 4 | export 'src/cluster_item.dart'; 5 | export 'src/cluster.dart'; 6 | export 'src/geohash.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/cluster.dart: -------------------------------------------------------------------------------- 1 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 2 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; 3 | 4 | class Cluster { 5 | final LatLng location; 6 | final Iterable items; 7 | 8 | Cluster(this.items, this.location); 9 | 10 | Cluster.fromItems(Iterable items) 11 | : this.items = items, 12 | this.location = LatLng( 13 | items.fold(0.0, (p, c) => p + c.location.latitude) / 14 | items.length, 15 | items.fold(0.0, (p, c) => p + c.location.longitude) / 16 | items.length); 17 | 18 | //location becomes weighted avarage lat lon 19 | Cluster.fromClusters(Cluster cluster1, Cluster cluster2) 20 | : this.items = cluster1.items.toSet()..addAll(cluster2.items.toSet()), 21 | this.location = LatLng( 22 | (cluster1.location.latitude * cluster1.count + 23 | cluster2.location.latitude * cluster2.count) / 24 | (cluster1.count + cluster2.count), 25 | (cluster1.location.longitude * cluster1.count + 26 | cluster2.location.longitude * cluster2.count) / 27 | (cluster1.count + cluster2.count)); 28 | 29 | /// Get number of clustered items 30 | int get count => items.length; 31 | 32 | /// True if cluster is not a single item cluster 33 | bool get isMultiple => items.length > 1; 34 | 35 | /// Basic cluster marker id 36 | String getId() { 37 | return location.latitude.toString() + 38 | "_" + 39 | location.longitude.toString() + 40 | "_$count"; 41 | } 42 | 43 | @override 44 | String toString() { 45 | return 'Cluster of $count $T (${location.latitude}, ${location.longitude})'; 46 | } 47 | 48 | bool operator ==(o) => o is Cluster && items == o.items; 49 | int get hashCode => items.hashCode; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/cluster_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 2 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' 3 | hide ClusterManager; 4 | 5 | mixin ClusterItem { 6 | LatLng get location; 7 | 8 | String? _geohash; 9 | String get geohash => _geohash ??= 10 | Geohash.encode(location, codeLength: ClusterManager.precision); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/cluster_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; 7 | import 'package:google_maps_cluster_manager/src/max_dist_clustering.dart'; 8 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' 9 | hide Cluster; 10 | 11 | enum ClusterAlgorithm { GEOHASH, MAX_DIST } 12 | 13 | class MaxDistParams { 14 | final double epsilon; 15 | 16 | MaxDistParams(this.epsilon); 17 | } 18 | 19 | class ClusterManager { 20 | ClusterManager(this._items, this.updateMarkers, 21 | {Future Function(Cluster)? markerBuilder, 22 | this.levels = const [1, 4.25, 6.75, 8.25, 11.5, 14.5, 16.0, 16.5, 20.0], 23 | this.extraPercent = 0.5, 24 | this.maxItemsForMaxDistAlgo = 200, 25 | this.clusterAlgorithm = ClusterAlgorithm.GEOHASH, 26 | this.maxDistParams, 27 | this.stopClusteringZoom}) 28 | : this.markerBuilder = markerBuilder ?? _basicMarkerBuilder, 29 | assert(levels.length <= precision); 30 | 31 | /// Method to build markers 32 | final Future Function(Cluster) markerBuilder; 33 | 34 | // Num of Items to switch from MAX_DIST algo to GEOHASH 35 | final int maxItemsForMaxDistAlgo; 36 | 37 | /// Function to update Markers on Google Map 38 | final void Function(Set) updateMarkers; 39 | 40 | /// Zoom levels configuration 41 | final List levels; 42 | 43 | /// Extra percent of markers to be loaded (ex : 0.2 for 20%) 44 | final double extraPercent; 45 | 46 | // Clusteringalgorithm 47 | final ClusterAlgorithm clusterAlgorithm; 48 | 49 | final MaxDistParams? maxDistParams; 50 | 51 | /// Zoom level to stop cluster rendering 52 | final double? stopClusteringZoom; 53 | 54 | /// Precision of the geohash 55 | static final int precision = kIsWeb ? 12 : 20; 56 | 57 | /// Google Maps map id 58 | int? _mapId; 59 | 60 | /// List of items 61 | Iterable get items => _items; 62 | Iterable _items; 63 | 64 | /// Last known zoom 65 | late double _zoom; 66 | 67 | final double _maxLng = 180 - pow(10, -10.0) as double; 68 | 69 | /// Set Google Map Id for the cluster manager 70 | void setMapId(int mapId, {bool withUpdate = true}) async { 71 | _mapId = mapId; 72 | _zoom = await GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); 73 | if (withUpdate) updateMap(); 74 | } 75 | 76 | /// Method called on map update to update cluster. Can also be manually called to force update. 77 | void updateMap() { 78 | _updateClusters(); 79 | } 80 | 81 | void _updateClusters() async { 82 | List> mapMarkers = await getMarkers(); 83 | 84 | final Set markers = 85 | Set.from(await Future.wait(mapMarkers.map((m) => markerBuilder(m)))); 86 | 87 | updateMarkers(markers); 88 | } 89 | 90 | /// Update all cluster items 91 | void setItems(List newItems) { 92 | _items = newItems; 93 | updateMap(); 94 | } 95 | 96 | /// Add on cluster item 97 | void addItem(ClusterItem newItem) { 98 | _items = List.from([...items, newItem]); 99 | updateMap(); 100 | } 101 | 102 | /// Method called on camera move 103 | void onCameraMove(CameraPosition position, {forceUpdate = false}) { 104 | _zoom = position.zoom; 105 | if (forceUpdate) { 106 | updateMap(); 107 | } 108 | } 109 | 110 | /// Retrieve cluster markers 111 | Future>> getMarkers() async { 112 | if (_mapId == null) return List.empty(); 113 | 114 | final LatLngBounds mapBounds = await GoogleMapsFlutterPlatform.instance 115 | .getVisibleRegion(mapId: _mapId!); 116 | 117 | late LatLngBounds inflatedBounds; 118 | if (clusterAlgorithm == ClusterAlgorithm.GEOHASH) { 119 | inflatedBounds = _inflateBounds(mapBounds); 120 | } else { 121 | inflatedBounds = mapBounds; 122 | } 123 | 124 | List visibleItems = items.where((i) { 125 | return inflatedBounds.contains(i.location); 126 | }).toList(); 127 | 128 | if (stopClusteringZoom != null && _zoom >= stopClusteringZoom!) 129 | return visibleItems.map((i) => Cluster.fromItems([i])).toList(); 130 | 131 | List> markers; 132 | 133 | if (clusterAlgorithm == ClusterAlgorithm.GEOHASH || 134 | visibleItems.length >= maxItemsForMaxDistAlgo) { 135 | int level = _findLevel(levels); 136 | markers = _computeClusters(visibleItems, List.empty(growable: true), 137 | level: level); 138 | } else { 139 | markers = _computeClustersWithMaxDist(visibleItems, _zoom); 140 | } 141 | 142 | return markers; 143 | } 144 | 145 | LatLngBounds _inflateBounds(LatLngBounds bounds) { 146 | // Bounds that cross the date line expand compared to their difference with the date line 147 | double lng = 0; 148 | if (bounds.northeast.longitude < bounds.southwest.longitude) { 149 | lng = extraPercent * 150 | ((180.0 - bounds.southwest.longitude) + 151 | (bounds.northeast.longitude + 180)); 152 | } else { 153 | lng = extraPercent * 154 | (bounds.northeast.longitude - bounds.southwest.longitude); 155 | } 156 | 157 | // Latitudes expanded beyond +/- 90 are automatically clamped by LatLng 158 | double lat = 159 | extraPercent * (bounds.northeast.latitude - bounds.southwest.latitude); 160 | 161 | double eLng = (bounds.northeast.longitude + lng).clamp(-_maxLng, _maxLng); 162 | double wLng = (bounds.southwest.longitude - lng).clamp(-_maxLng, _maxLng); 163 | 164 | return LatLngBounds( 165 | southwest: LatLng(bounds.southwest.latitude - lat, wLng), 166 | northeast: 167 | LatLng(bounds.northeast.latitude + lat, lng != 0 ? eLng : _maxLng), 168 | ); 169 | } 170 | 171 | int _findLevel(List levels) { 172 | for (int i = levels.length - 1; i >= 0; i--) { 173 | if (levels[i] <= _zoom) { 174 | return i + 1; 175 | } 176 | } 177 | 178 | return 1; 179 | } 180 | 181 | int _getZoomLevel(double zoom) { 182 | for (int i = levels.length - 1; i >= 0; i--) { 183 | if (levels[i] <= zoom) { 184 | return levels[i].toInt(); 185 | } 186 | } 187 | 188 | return 1; 189 | } 190 | 191 | List> _computeClustersWithMaxDist( 192 | List inputItems, double zoom) { 193 | MaxDistClustering scanner = MaxDistClustering( 194 | epsilon: maxDistParams?.epsilon ?? 20, 195 | ); 196 | 197 | return scanner.run(inputItems, _getZoomLevel(zoom)); 198 | } 199 | 200 | List> _computeClusters( 201 | List inputItems, List> markerItems, 202 | {int level = 5}) { 203 | if (inputItems.isEmpty) return markerItems; 204 | String nextGeohash = inputItems[0].geohash.substring(0, level); 205 | 206 | List items = inputItems 207 | .where((p) => p.geohash.substring(0, level) == nextGeohash) 208 | .toList(); 209 | 210 | markerItems.add(Cluster.fromItems(items)); 211 | 212 | List newInputList = List.from( 213 | inputItems.where((i) => i.geohash.substring(0, level) != nextGeohash)); 214 | 215 | return _computeClusters(newInputList, markerItems, level: level); 216 | } 217 | 218 | static Future Function(Cluster) get _basicMarkerBuilder => 219 | (cluster) async { 220 | return Marker( 221 | markerId: MarkerId(cluster.getId()), 222 | position: cluster.location, 223 | onTap: () { 224 | print(cluster); 225 | }, 226 | icon: await _getBasicClusterBitmap(cluster.isMultiple ? 125 : 75, 227 | text: cluster.isMultiple ? cluster.count.toString() : null), 228 | ); 229 | }; 230 | 231 | static Future _getBasicClusterBitmap(int size, 232 | {String? text}) async { 233 | final PictureRecorder pictureRecorder = PictureRecorder(); 234 | final Canvas canvas = Canvas(pictureRecorder); 235 | final Paint paint1 = Paint()..color = Colors.red; 236 | 237 | canvas.drawCircle(Offset(size / 2, size / 2), size / 2.0, paint1); 238 | 239 | if (text != null) { 240 | TextPainter painter = TextPainter(textDirection: TextDirection.ltr); 241 | painter.text = TextSpan( 242 | text: text, 243 | style: TextStyle( 244 | fontSize: size / 3, 245 | color: Colors.white, 246 | fontWeight: FontWeight.normal), 247 | ); 248 | painter.layout(); 249 | painter.paint( 250 | canvas, 251 | Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2), 252 | ); 253 | } 254 | 255 | final img = await pictureRecorder.endRecording().toImage(size, size); 256 | final data = await img.toByteData(format: ImageByteFormat.png) as ByteData; 257 | 258 | return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lib/src/common.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; 4 | 5 | class DistUtils { 6 | // Zoom-Level: 7 | // Level # Tiles Tile width 8 | // (° of longitudes) m / pixel 9 | // (on Equator) ~ Scale 10 | // (on screen) Examples of 11 | // areas to represent 12 | // 0 1 360 156 412 1:500 million whole world 13 | // 1 4 180 78 206 1:250 million 14 | // 2 16 90 39 103 1:150 million subcontinental area 15 | // 3 64 45 19 551 1:70 million largest country 16 | // 4 256 22.5 9 776 1:35 million 17 | // 5 1 024 11.25 4 888 1:15 million large African country 18 | // 6 4 096 5.625 2 444 1:10 million large European country 19 | // 7 16 384 2.813 1 222 1:4 million small country, US state 20 | // 8 65 536 1.406 610.984 1:2 million 21 | // 9 262 144 0.703 305.492 1:1 million wide area, large metropolitan area 22 | // 10 1 048 576 0.352 152.746 1:500 thousand metropolitan area 23 | // 11 4 194 304 0.176 76.373 1:250 thousand city 24 | // 12 16 777 216 0.088 38.187 1:150 thousand town, or city district 25 | // 13 67 108 864 0.044 19.093 1:70 thousand village, or suburb 26 | // 14 268 435 456 0.022 9.547 1:35 thousand 27 | // 15 1 073 741 824 0.011 4.773 1:15 thousand small road 28 | // 16 4 294 967 296 0.005 2.387 1:8 thousand street 29 | // 17 17 179 869 184 0.003 1.193 1:4 thousand block, park, addresses 30 | // 18 68 719 476 736 0.001 0.596 1:2 thousand some buildings, trees 31 | // 19 274 877 906 944 0.0005 0.298 1:1 thousand local highway and crossing details 32 | // 20 1 099 511 627 776 0.00025 0.149 1:5 hundred A mid-sized building 33 | final Map<(LatLng, LatLng), double> distCache = {}; 34 | 35 | double getLatLonDist(LatLng point1, LatLng point2, int zoomLevel) { 36 | if (distCache[(point1, point2)] != null) { 37 | return distCache[(point1, point2)]!; 38 | } 39 | double meterPerPixel = _getScalingFactor(zoomLevel); 40 | double dist = getDistanceFromLatLonInKm(point1.latitude, point1.longitude, 41 | point2.latitude, point2.longitude) / 42 | (meterPerPixel / 1000); 43 | // print("dist is $x"); 44 | distCache[(point1, point2)] = dist; 45 | return dist; 46 | } 47 | 48 | double getDistanceFromLatLonInKm( 49 | double lat1, double lon1, double lat2, double lon2) { 50 | var R = 6371; // Radius of the earth in km 51 | var dLat = _degreeToRadian(lat2 - lat1); 52 | var dLon = _degreeToRadian(lon2 - lon1); 53 | var a = sin(dLat / 2) * sin(dLat / 2) + 54 | cos(_degreeToRadian(lat1)) * 55 | cos(_degreeToRadian(lat2)) * 56 | sin(dLon / 2) * 57 | sin(dLon / 2); 58 | var c = 2 * atan2(sqrt(a), sqrt(1 - a)); 59 | var d = R * c; // Distance in km 60 | return d; 61 | } 62 | 63 | double _degreeToRadian(double degree) { 64 | return degree * pi / 180; 65 | } 66 | 67 | double _getScalingFactor(int zoomLevel) { 68 | switch (zoomLevel) { 69 | case 0: 70 | return 156412; 71 | case 1: 72 | return 78206; 73 | case 2: 74 | return 39103; 75 | case 3: 76 | return 19551; 77 | case 4: 78 | return 9776; 79 | case 5: 80 | return 4888; 81 | case 6: 82 | return 2444; 83 | case 7: 84 | return 1222; 85 | case 8: 86 | return 610.984; 87 | case 9: 88 | return 305.492; 89 | case 10: 90 | return 152.746; 91 | case 11: 92 | return 76.373; 93 | case 12: 94 | return 38.187; 95 | case 13: 96 | return 19.093; 97 | case 14: 98 | return 9.547; 99 | case 15: 100 | return 4.773; 101 | case 16: 102 | return 2.387; 103 | case 17: 104 | return 1.193; 105 | case 18: 106 | return 0.596; 107 | case 19: 108 | return 0.298; 109 | case 20: 110 | return 0.149; 111 | default: 112 | return 0.149; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/geohash.dart: -------------------------------------------------------------------------------- 1 | // Null-safety version of llamadonica library (https://github.com/llamadonica/dart-geohash) 2 | 3 | // Copyright (c) 2015-2018, llamadonica. All rights reserved. Use of this source code 4 | // is governed by a BSD-style license that can be found in the LICENSE file. 5 | 6 | import 'dart:math'; 7 | 8 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; 9 | 10 | /// A collection of static functions to work with geohashes, as exlpained 11 | /// [here](https://en.wikipedia.org/wiki/Geohash) 12 | class Geohash { 13 | static const Map _base32CharToNumber = const { 14 | '0': 0, 15 | '1': 1, 16 | '2': 2, 17 | '3': 3, 18 | '4': 4, 19 | '5': 5, 20 | '6': 6, 21 | '7': 7, 22 | '8': 8, 23 | '9': 9, 24 | 'b': 10, 25 | 'c': 11, 26 | 'd': 12, 27 | 'e': 13, 28 | 'f': 14, 29 | 'g': 15, 30 | 'h': 16, 31 | 'j': 17, 32 | 'k': 18, 33 | 'm': 19, 34 | 'n': 20, 35 | 'p': 21, 36 | 'q': 22, 37 | 'r': 23, 38 | 's': 24, 39 | 't': 25, 40 | 'u': 26, 41 | 'v': 27, 42 | 'w': 28, 43 | 'x': 29, 44 | 'y': 30, 45 | 'z': 31 46 | }; 47 | static const List _base32NumberToChar = const [ 48 | '0', 49 | '1', 50 | '2', 51 | '3', 52 | '4', 53 | '5', 54 | '6', 55 | '7', 56 | '8', 57 | '9', 58 | 'b', 59 | 'c', 60 | 'd', 61 | 'e', 62 | 'f', 63 | 'g', 64 | 'h', 65 | 'j', 66 | 'k', 67 | 'm', 68 | 'n', 69 | 'p', 70 | 'q', 71 | 'r', 72 | 's', 73 | 't', 74 | 'u', 75 | 'v', 76 | 'w', 77 | 'x', 78 | 'y', 79 | 'z' 80 | ]; 81 | 82 | /// Encode a latitude and longitude pair into a geohash string. 83 | static String encode(final LatLng latLng, {final int codeLength = 12}) { 84 | if (codeLength > 20 || (identical(1.0, 1) && codeLength > 12)) { 85 | //Javascript can only handle 32 bit ints reliably. 86 | throw ArgumentError( 87 | 'latitude and longitude are not precise enough to encode $codeLength characters'); 88 | } 89 | final latitudeBase2 = (latLng.latitude + 90) * (pow(2.0, 52) / 180); 90 | final longitudeBase2 = (latLng.longitude + 180) * (pow(2.0, 52) / 360); 91 | final longitudeBits = (codeLength ~/ 2) * 5 + (codeLength % 2) * 3; 92 | final latitudeBits = codeLength * 5 - longitudeBits; 93 | var longitudeCode = (identical(1.0, 1)) //Test for javascript. 94 | ? (longitudeBase2 / (pow(2.0, 52 - longitudeBits))).floor() 95 | : longitudeBase2.floor() >> (52 - longitudeBits); 96 | var latitudeCode = (identical(1.0, 1)) //Test for javascript. 97 | ? (latitudeBase2 / (pow(2.0, 52 - latitudeBits))).floor() 98 | : latitudeBase2.floor() >> (52 - latitudeBits); 99 | 100 | final stringBuffer = []; 101 | for (var localCodeLength = codeLength; 102 | localCodeLength > 0; 103 | localCodeLength--) { 104 | int bigEndCode, littleEndCode; 105 | if (localCodeLength % 2 == 0) { 106 | //Even slot. Latitude is more significant. 107 | bigEndCode = latitudeCode; 108 | littleEndCode = longitudeCode; 109 | latitudeCode >>= 3; 110 | longitudeCode >>= 2; 111 | } else { 112 | bigEndCode = longitudeCode; 113 | littleEndCode = latitudeCode; 114 | latitudeCode >>= 2; 115 | longitudeCode >>= 3; 116 | } 117 | final code = ((bigEndCode & 4) << 2) | 118 | ((bigEndCode & 2) << 1) | 119 | (bigEndCode & 1) | 120 | ((littleEndCode & 2) << 2) | 121 | ((littleEndCode & 1) << 1); 122 | stringBuffer.add(_base32NumberToChar[code]); 123 | } 124 | final buffer = StringBuffer()..writeAll(stringBuffer.reversed); 125 | return buffer.toString(); 126 | } 127 | 128 | /// Get the rectangle that covers the entire area of a geohash string. 129 | static Rectangle getExtents(String geohash) { 130 | final codeLength = geohash.length; 131 | if (codeLength > 20 || (identical(1.0, 1) && codeLength > 12)) { 132 | //Javascript can only handle 32 bit ints reliably. 133 | throw ArgumentError( 134 | 'latitude and longitude are not precise enough to encode $codeLength characters'); 135 | } 136 | var latitudeInt = 0; 137 | var longitudeInt = 0; 138 | var longitudeFirst = true; 139 | for (var character 140 | in geohash.codeUnits.map((r) => String.fromCharCode(r))) { 141 | int? thisSequence; 142 | try { 143 | thisSequence = _base32CharToNumber[character]; 144 | } on Exception catch (_) { 145 | throw ArgumentError('$geohash was not a geohash string'); 146 | } 147 | final bigBits = ((thisSequence! & 16) >> 2) | 148 | ((thisSequence & 4) >> 1) | 149 | (thisSequence & 1); 150 | final smallBits = ((thisSequence & 8) >> 2) | ((thisSequence & 2) >> 1); 151 | if (longitudeFirst) { 152 | longitudeInt = (longitudeInt << 3) | bigBits; 153 | latitudeInt = (latitudeInt << 2) | smallBits; 154 | } else { 155 | longitudeInt = (longitudeInt << 2) | smallBits; 156 | latitudeInt = (latitudeInt << 3) | bigBits; 157 | } 158 | longitudeFirst = !longitudeFirst; 159 | } 160 | final longitudeBits = (codeLength ~/ 2) * 5 + (codeLength % 2) * 3; 161 | final latitudeBits = codeLength * 5 - longitudeBits; 162 | if (identical(1.0, 1)) { 163 | // Some of our intermediate numbers are STILL too big for javascript, 164 | // so we use floating point math... 165 | final longitudeDiff = pow(2.0, 52 - longitudeBits); 166 | final latitudeDiff = pow(2.0, 52 - latitudeBits); 167 | final latitudeFloat = latitudeInt.toDouble() * latitudeDiff; 168 | final longitudeFloat = longitudeInt.toDouble() * longitudeDiff; 169 | final latitude = latitudeFloat * (180 / pow(2.0, 52)) - 90; 170 | final longitude = longitudeFloat * (360 / pow(2.0, 52)) - 180; 171 | final num height = latitudeDiff * (180 / pow(2.0, 52)); 172 | final num width = longitudeDiff * (360 / pow(2.0, 52)); 173 | return Rectangle( 174 | latitude, longitude, height.toDouble(), width.toDouble()); 175 | } 176 | 177 | longitudeInt = longitudeInt << (52 - longitudeBits); 178 | latitudeInt = latitudeInt << (52 - latitudeBits); 179 | final longitudeDiff = 1 << (52 - longitudeBits); 180 | final latitudeDiff = 1 << (52 - latitudeBits); 181 | final latitude = latitudeInt.toDouble() * (180 / pow(2.0, 52)) - 90; 182 | final longitude = longitudeInt.toDouble() * (360 / pow(2.0, 52)) - 180; 183 | final height = latitudeDiff.toDouble() * (180 / pow(2.0, 52)); 184 | final width = longitudeDiff.toDouble() * (360 / pow(2.0, 52)); 185 | return Rectangle(latitude, longitude, height, width); 186 | } 187 | 188 | /// Get a single number that is the center of a specific geohash rectangle. 189 | static LatLng decode(String geohash) { 190 | final extents = getExtents(geohash); 191 | final x = extents.left + extents.width / 2; 192 | final y = extents.top + extents.height / 2; 193 | return LatLng(x, y); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/src/max_dist_clustering.dart: -------------------------------------------------------------------------------- 1 | import '../google_maps_cluster_manager.dart'; 2 | import 'common.dart'; 3 | 4 | class _MinDistCluster { 5 | final Cluster cluster; 6 | final double dist; 7 | 8 | _MinDistCluster(this.cluster, this.dist); 9 | } 10 | 11 | class MaxDistClustering { 12 | ///Complete list of points 13 | late List dataset; 14 | 15 | List> _cluster = []; 16 | 17 | ///Threshold distance for two clusters to be considered as one cluster 18 | final double epsilon; 19 | 20 | final DistUtils distUtils = DistUtils(); 21 | 22 | MaxDistClustering({ 23 | this.epsilon = 1, 24 | }); 25 | 26 | ///Run clustering process, add configs in constructor 27 | List> run(List dataset, int zoomLevel) { 28 | this.dataset = dataset; 29 | 30 | //initial variables 31 | List> distMatrix = []; 32 | for (T entry1 in dataset) { 33 | distMatrix.add([]); 34 | _cluster.add(Cluster.fromItems([entry1])); 35 | } 36 | bool changed = true; 37 | while (changed) { 38 | changed = false; 39 | for (Cluster c in _cluster) { 40 | _MinDistCluster? minDistCluster = getClosestCluster(c, zoomLevel); 41 | if (minDistCluster == null || minDistCluster.dist > epsilon) continue; 42 | _cluster.add(Cluster.fromClusters(minDistCluster.cluster, c)); 43 | _cluster.remove(c); 44 | _cluster.remove(minDistCluster.cluster); 45 | changed = true; 46 | 47 | break; 48 | } 49 | } 50 | return _cluster; 51 | } 52 | 53 | _MinDistCluster? getClosestCluster(Cluster cluster, int zoomLevel) { 54 | double minDist = 1000000000; 55 | Cluster minDistCluster = Cluster.fromItems([]); 56 | for (Cluster c in _cluster) { 57 | if (c.location == cluster.location) continue; 58 | double tmp = 59 | distUtils.getLatLonDist(c.location, cluster.location, zoomLevel); 60 | if (tmp < minDist) { 61 | minDist = tmp; 62 | minDistCluster = Cluster.fromItems(c.items); 63 | } 64 | } 65 | return _MinDistCluster(minDistCluster, minDist); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: google_maps_cluster_manager 2 | description: Simple Flutter clustering library for Google Maps based on Geohash. 3 | version: 3.1.0 4 | homepage: https://github.com/bpillon/google_maps_cluster_manager 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | google_maps_flutter_platform_interface: ^2.4.3 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | topics: 20 | - google-maps 21 | - google-maps-flutter 22 | - map 23 | - cluster 24 | -------------------------------------------------------------------------------- /test/common_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:google_maps_cluster_manager/src/common.dart'; 3 | import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; 4 | 5 | void main() { 6 | group("test get_distance of coordinates", () { 7 | test( 8 | "should get dist of between 600m and 800m on call with close coordinates", 9 | () { 10 | LatLng start = LatLng(52.421327, 10.623056); 11 | LatLng end = LatLng(52.42748887594039, 10.623379056822062); 12 | final DistUtils utils = DistUtils(); 13 | double dist = utils.getDistanceFromLatLonInKm( 14 | start.latitude, start.longitude, end.latitude, end.longitude); 15 | print("dist is $dist"); 16 | expect(dist >= 0.6 && dist <= 0.8, true); 17 | }); 18 | 19 | test( 20 | "should get dist of between 75km and 80km on call with wider coordinates", 21 | () { 22 | LatLng start = LatLng(52.45175365359977, 10.679139941065786); 23 | LatLng end = LatLng(51.7578902763405, 10.74257578002594); 24 | final DistUtils utils = DistUtils(); 25 | double dist = utils.getDistanceFromLatLonInKm( 26 | start.latitude, start.longitude, end.latitude, end.longitude); 27 | print("dist is $dist"); 28 | expect(dist >= 75 && dist <= 80, true); 29 | }); 30 | 31 | test("should map distance of 77km with zoomLevel to ", () { 32 | LatLng start = LatLng(52.45175365359977, 10.679139941065786); 33 | LatLng end = LatLng(51.7578902763405, 10.74257578002594); 34 | final DistUtils utils = DistUtils(); 35 | 36 | double dist = utils.getLatLonDist(start, end, 16); 37 | print("dist is $dist ${75 / 2.387 * 1000} ${80 / 2.387 * 1000}"); 38 | expect(dist >= 75 / 2.387 * 1000 && dist <= 80 / 2.387 * 1000, true); 39 | }); 40 | }); 41 | } 42 | --------------------------------------------------------------------------------