diff --git a/.github/workflows/flutter_ci.yml b/.github/workflows/flutter_ci.yml index b6707163e..cf1a18f55 100644 --- a/.github/workflows/flutter_ci.yml +++ b/.github/workflows/flutter_ci.yml @@ -8,67 +8,73 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - - run: flutter pub get - - name: Lint analysis - run: cd example && flutter analyze - + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + - uses: subosito/flutter-action@v1 + with: + flutter-version: "2.10.5" + - run: flutter pub get + - name: Lint analysis + run: cd example && flutter analyze + check-dart-formatting: name: "Check Dart formatting" runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v1 - - uses: subosito/flutter-action@v1 - - name: Check Dart formatting - run: flutter format --set-exit-if-changed . + - uses: actions/checkout@v1 + - uses: subosito/flutter-action@v1 + - name: Check Dart formatting + run: dart format --set-exit-if-changed . check-swift-formatting: name: "Check Swift formatting" runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v1 - - name: get SwiftFormat - run: wget https://github.com/nicklockwood/SwiftFormat/releases/download/0.48.18/swiftformat_linux.zip - - run: unzip swiftformat_linux.zip - - run: chmod +x swiftformat_linux - - name: Check Swift formatting - run: ./swiftformat_linux --swiftversion 4.2 --maxwidth 100 --lint ios + - uses: actions/checkout@v1 + - name: get SwiftFormat + run: ./install_formatting_tools.sh + - name: Check Swift formatting + run: ./swiftformat --swiftversion 4.2 --maxwidth 100 --lint ios check-java-formatting: name: "Check Java formatting" runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: get google-java-format - run: wget https://github.com/google/google-java-format/releases/download/v1.13.0/google-java-format-1.13.0-all-deps.jar - - run: java --version - - name: Check Java formatting - run: java -jar google-java-format-1.13.0-all-deps.jar --set-exit-if-changed -n $(find . -type f -name "*.java") + steps: + - uses: actions/checkout@v1 + - name: get google-java-format + run: ./install_formatting_tools.sh + - name: Check Java formatting + run: > + java + --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -jar google-java-format-1.13.0-all-deps.jar --set-exit-if-changed -n $(find . -type f -name "*.java") build-android: environment: ANDROID_CI_DOWNLOADS_TOKEN name: "Build Android apk" runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - - run: flutter pub get - - name: Build example APK - run: cd example && flutter build apk - env: - SDK_REGISTRY_TOKEN: ${{ secrets.SDK_REGISTRY_ANDROID}} + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + - uses: subosito/flutter-action@v2 + with: + flutter-version: "2.10.5" + - run: flutter pub get + - name: Build example APK + run: cd example && flutter build apk + env: + SDK_REGISTRY_TOKEN: ${{ secrets.SDK_REGISTRY_ANDROID}} build-iOS: environment: ANDROID_CI_DOWNLOADS_TOKEN @@ -79,8 +85,10 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-java@v1 with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 + java-version: "12.x" + - uses: subosito/flutter-action@v2 + with: + flutter-version: "2.10.5" - run: flutter pub get - name: build iOS package run: | @@ -98,12 +106,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - - run: flutter config --enable-web - - run: flutter pub get - - name: Build web - run: cd example && flutter build web + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + - uses: subosito/flutter-action@v1 + - run: flutter config --enable-web + - run: flutter pub get + - name: Build web + run: cd example && flutter build web diff --git a/.gitignore b/.gitignore index b7d74c826..e8639a336 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,13 @@ *.log *.pyc *.swp +*.cxx .DS_Store .atom/ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml @@ -98,10 +100,15 @@ unlinked_spec.ds **/macos/Flutter/Flutter-Debug.xcconfig **/macos/Flutter/Flutter-Release.xcconfig **/macos/Flutter/Flutter-Profile.xcconfig +__MACOSX/ # Coverage coverage/ +# Binaries +google-java-format-1.13.0-all-deps.jar +swiftformat + # Symbols app.*.symbols @@ -112,3 +119,4 @@ app.*.symbols !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1163ad7b7..dcc46455e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ +## 0.16.0, May 19, 2022 +* Fix type issues in query rendered features in rect [#862](https://github.com/flutter-mapbox-gl/maps/pull/862) +* Annotation manager moved to dart [#779](https://github.com/flutter-mapbox-gl/maps/pull/779) +* Add java formatting [#863](https://github.com/flutter-mapbox-gl/maps/pull/863) +* Fix urls parsing [#868](https://github.com/flutter-mapbox-gl/maps/pull/868) +* Fix for MissingPluginException when using downloadOfflineRegion [#864](https://github.com/flutter-mapbox-gl/maps/pull/864) +* Fix issue with map disposal on web [#895](https://github.com/flutter-mapbox-gl/maps/pull/895) +* Fix for rescale issues on web [#896](https://github.com/flutter-mapbox-gl/maps/pull/896) +* Fix android build issues [#904](https://github.com/flutter-mapbox-gl/maps/pull/904) +* Upgraded mapbox gl js to 2.7.0 [#889](https://github.com/flutter-mapbox-gl/maps/pull/889) +* Add updateContentInsets on Android [#903](https://github.com/flutter-mapbox-gl/maps/pull/903) +* Fix MapController's queryRendered* methods not considering layerIds [#870](https://github.com/flutter-mapbox-gl/maps/pull/870) +* Adding function in annotation manager to remove multiple annotations [#931](https://github.com/flutter-mapbox-gl/maps/pull/931) +* Update "Setting up" section of README [#918](https://github.com/flutter-mapbox-gl/maps/pull/918) +* Add support for layer zoom limits [#934](https://github.com/flutter-mapbox-gl/maps/pull/934) +* Add locationComponent#getLastLocation [#922](https://github.com/flutter-mapbox-gl/maps/pull/922) +* Add and default to Hybrid composition on Android [#916](https://github.com/flutter-mapbox-gl/maps/pull/916) +* Fix location puck getting hidden [#956](https://github.com/flutter-mapbox-gl/maps/pull/956) +* Don't call locationComponent if it's null [#966](https://github.com/flutter-mapbox-gl/maps/pull/966) +* Do nothing and correctly return if layer and source to remove are not there [](https://github.com/flutter-mapbox-gl/maps/pull/961) +* Fixed LocationTracking issue delayed permissions [#958](https://github.com/flutter-mapbox-gl/maps/pull/958) +* Fix for LocationComponent update issue [#969](https://github.com/flutter-mapbox-gl/maps/pull/969) +* Features with non string ids will not update properly [#970](https://github.com/flutter-mapbox-gl/maps/pull/970) +* Update added shapes by layer when setting a feature [#972](https://github.com/flutter-mapbox-gl/maps/pull/972) +* Allows modifying http headers [#977](https://github.com/flutter-mapbox-gl/maps/pull/977) +* Additional documentation [#986](https://github.com/flutter-mapbox-gl/maps/pull/986) +* Disabled hybrid composition do to various issues [#992](https://github.com/flutter-mapbox-gl/maps/pull/992) +* Implement layer filtering [#997](https://github.com/flutter-mapbox-gl/maps/pull/997) +* Drag event types support added [#987](https://github.com/flutter-mapbox-gl/maps/pull/987) +* Remove duplicated code [#1026](https://github.com/flutter-mapbox-gl/maps/pull/1026) +* Support filtering on addLayer [#1024](https://github.com/flutter-mapbox-gl/maps/pull/1024) +* Document layer zoom limits [#1028](https://github.com/flutter-mapbox-gl/maps/pull/1028) + ## 0.15.0, January 13, 2022 -* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) +* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) * Fixed layer based feature selection [#765](https://github.com/flutter-mapbox-gl/maps/pull/765) * Implement the changePosition function for place_fill example [#778](https://github.com/flutter-mapbox-gl/maps/pull/778) * Invoke onPause method of MapView in onPause lifecycle [#782](https://github.com/flutter-mapbox-gl/maps/pull/782) @@ -24,7 +57,7 @@ * Fixed issue with return type of remove source on web [#854](https://github.com/flutter-mapbox-gl/maps/pull/854) ## 0.14.0, November 13, 2021 -* Remove memory leaks by disposing internal components [#706](https://github.com/tobrun/flutter-mapbox-gl/pull/706) +* Remove memory leaks by disposing internal components [#706](https://github.com/tobrun/flutter-mapbox-gl/pull/706) * Improved annotation click order [#748](https://github.com/tobrun/flutter-mapbox-gl/pull/748) * Add support for Layers, properties and expressions backed by GeoJsonSource [#723](https://github.com/tobrun/flutter-mapbox-gl/pull/723) * Add attribution button gravity, position normally [#731](https://github.com/tobrun/flutter-mapbox-gl/pull/731) diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..71ae0f508 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +format: + java \ + --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ + -jar google-java-format-1.13.0-all-deps.jar -r $(shell find . -type f -name "*.java") + flutter format . + ./swiftformat --swiftversion 4.2 --maxwidth 100 ios + +install_formatting: + ./install_formatting_tools.sh + +codegen: + dart scripts/lib/generate.dart \ No newline at end of file diff --git a/README.md b/README.md index ad6887d49..112b4c2fd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Map Styles](#map-styles) - [Offline Sideloading](#offline-sideloading) - [Downloading Offline Regions](#downloading-offline-regions) + - [Create a static map snapshot](#create-a-static-map-snapshot) - [Location features](#location-features) - [Android](#android) - [iOS](#ios) @@ -70,8 +71,16 @@ curl: (22) The requested URL returned error: 401 Unauthorized Include the JavaScript and CSS files in the `` of your `index.html` file: ``` - - + + + + ``` *Note: Look for latest version in [Mapbox GL JS documentation](https://docs.mapbox.com/mapbox-gl-js/guides/).* @@ -121,8 +130,9 @@ MapboxMap( | Circle Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Line Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Fill Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Fill Extrusion Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Hillshade Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Heatmap Layer | :x: | :x: | :x: | +| Heatmap Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Vector Source | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Raster Source | :white_check_mark: | :white_check_mark: | :white_check_mark: | | GeoJson Source | :white_check_mark: | :white_check_mark: | :white_check_mark: | @@ -198,7 +208,25 @@ An offline region is a defined region of a map that is available for use in cond downloadOfflineRegionStream(offlineRegion, onEvent); ``` +## Create a static map snapshot + +The snapshotManager generates static raster images of the map. +Each snapshot image depicts a portion of a map defined by an SnapshotOptions object you provide. + +* Call `takeSnapshot` with predefined `SnapshotOptions` +``` + final renderBox = mapKey.currentContext?.findRenderObject() as RenderBox; + + final snapshotOptions = SnapshotOptions( + width: renderBox.size.width, + height: renderBox.size.height, + writeToDisk: true, + withLogo: false, + ); + + final uri = await mapController?.takeSnapshot(snapshotOptions); +``` ## Location features ### Android @@ -224,6 +252,9 @@ xml ... [Recommended](https://docs.mapbox.com/help/tutorials/first-steps-ios-sdk/#display-the-users-location) explanation about "Shows your location on the map and helps improve the map". +## Flutter 3.x.x issues and experimental workarounds +Since Flutter 3.x.x was introduced, it exposed some race conditions resulting in occasional crashes upon map disposal. The parameter `useDelayedDisposal` was introduced as a workaround for this issue until Flutter and/or Mapbox fix this issue properly. Use with caution - this is not yet production ready since several users still report crashes after using this workaround. + ## Running the example code See the [documentation about this topic](doc/RUNNING_EXAMPLE_CODE.md) diff --git a/android/build.gradle b/android/build.gradle index 1430207bc..39bacb48e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -36,6 +36,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + namespace "com.mapbox.mapboxgl" compileSdkVersion 31 ndkVersion "20.1.5948944" @@ -57,6 +58,7 @@ android { implementation "com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v9:0.12.0" implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-offline-v9:0.7.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0' + implementation 'com.mapbox.mapboxsdk:mapbox-sdk-turf:5.1.0' } compileOptions { sourceCompatibility 1.8 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e7a6f4b2f..8428c0be2 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/BitmapUtils.java b/android/src/main/java/com/mapbox/mapboxgl/BitmapUtils.java new file mode 100644 index 000000000..c91044e51 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/BitmapUtils.java @@ -0,0 +1,57 @@ +package com.mapbox.mapboxgl; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Base64; +import android.util.Log; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** Created by nickitaliano on 10/9/17. */ +public class BitmapUtils { + private static final String LOG_TAG = "BitmapUtils"; + + public static String createTempFile(Context context, Bitmap bitmap) { + File tempFile = null; + FileOutputStream outputStream = null; + + try { + tempFile = File.createTempFile(LOG_TAG, ".jpeg", context.getCacheDir()); + outputStream = new FileOutputStream(tempFile); + } catch (IOException e) { + Log.w(LOG_TAG, e.getLocalizedMessage()); + } + + if (tempFile == null) { + return null; + } + + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + closeSnapshotOutputStream(outputStream); + return Uri.fromFile(tempFile).toString(); + } + + public static String createBase64(Bitmap bitmap) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + byte[] bitmapBytes = outputStream.toByteArray(); + closeSnapshotOutputStream(outputStream); + String base64Prefix = "data:image/jpeg;base64,"; + return base64Prefix + Base64.encodeToString(bitmapBytes, Base64.NO_WRAP); + } + + private static void closeSnapshotOutputStream(OutputStream outputStream) { + if (outputStream == null) { + return; + } + try { + outputStream.close(); + } catch (IOException e) { + Log.w(LOG_TAG, e.getLocalizedMessage()); + } + } +} diff --git a/android/src/main/java/com/mapbox/mapboxgl/GeoJSONUtils.java b/android/src/main/java/com/mapbox/mapboxgl/GeoJSONUtils.java new file mode 100644 index 000000000..db3b3ed51 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/GeoJSONUtils.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxgl; + +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.FeatureCollection; +import com.mapbox.geojson.Geometry; +import com.mapbox.geojson.GeometryCollection; +import com.mapbox.geojson.Point; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.turf.TurfMeasurement; +import java.util.ArrayList; +import java.util.List; + +public class GeoJSONUtils { + public static LatLng toLatLng(Point point) { + if (point == null) { + return null; + } + return new LatLng(point.latitude(), point.longitude()); + } + + private static GeometryCollection toGeometryCollection(List features) { + ArrayList geometries = new ArrayList<>(); + geometries.ensureCapacity(features.size()); + for (Feature feature : features) { + geometries.add(feature.geometry()); + } + return GeometryCollection.fromGeometries(geometries); + } + + public static LatLngBounds toLatLngBounds(FeatureCollection featureCollection) { + List features = featureCollection.features(); + + double[] bbox = TurfMeasurement.bbox(toGeometryCollection(features)); + + return LatLngBounds.from(bbox[3], bbox[2], bbox[1], bbox[0]); + } +} diff --git a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java index 567f1edaf..771fd8a28 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java +++ b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java @@ -26,15 +26,8 @@ class GlobalMethodHandler implements MethodChannel.MethodCallHandler { private static final int BUFFER_SIZE = 1024 * 2; @NonNull private final Context context; @NonNull private final BinaryMessenger messenger; - @Nullable private PluginRegistry.Registrar registrar; @Nullable private FlutterPlugin.FlutterAssets flutterAssets; - GlobalMethodHandler(@NonNull PluginRegistry.Registrar registrar) { - this.registrar = registrar; - this.context = registrar.activeContext(); - this.messenger = registrar.messenger(); - } - GlobalMethodHandler(@NonNull FlutterPlugin.FlutterPluginBinding binding) { this.context = binding.getApplicationContext(); this.flutterAssets = binding.getFlutterAssets(); @@ -142,9 +135,7 @@ private InputStream openTilesDbFile(String tilesDb) throws IOException { return new FileInputStream(new File(tilesDb)); } else { String assetKey; - if (registrar != null) { - assetKey = registrar.lookupKeyForAsset(tilesDb); - } else if (flutterAssets != null) { + if (flutterAssets != null) { assetKey = flutterAssets.getAssetFilePathByName(tilesDb); } else { throw new IllegalStateException(); diff --git a/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java index 4dcb9b034..b10599dbb 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java +++ b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java @@ -372,6 +372,50 @@ static PropertyValue[] interpretFillLayerProperties(Object o) { return properties.toArray(new PropertyValue[properties.size()]); } + static PropertyValue[] interpretFillExtrusionLayerProperties(Object o) { + final Map data = (Map) toMap(o); + final List properties = new LinkedList(); + final JsonParser parser = new JsonParser(); + + for (Map.Entry entry : data.entrySet()) { + final JsonElement jsonElement = parser.parse(entry.getValue()); + Expression expression = Expression.Converter.convert(jsonElement); + switch (entry.getKey()) { + case "fill-extrusion-opacity": + properties.add(PropertyFactory.fillExtrusionOpacity(expression)); + break; + case "fill-extrusion-color": + properties.add(PropertyFactory.fillExtrusionColor(expression)); + break; + case "fill-extrusion-translate": + properties.add(PropertyFactory.fillExtrusionTranslate(expression)); + break; + case "fill-extrusion-translate-anchor": + properties.add(PropertyFactory.fillExtrusionTranslateAnchor(expression)); + break; + case "fill-extrusion-pattern": + properties.add(PropertyFactory.fillExtrusionPattern(expression)); + break; + case "fill-extrusion-height": + properties.add(PropertyFactory.fillExtrusionHeight(expression)); + break; + case "fill-extrusion-base": + properties.add(PropertyFactory.fillExtrusionBase(expression)); + break; + case "fill-extrusion-vertical-gradient": + properties.add(PropertyFactory.fillExtrusionVerticalGradient(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + static PropertyValue[] interpretRasterLayerProperties(Object o) { final Map data = (Map) toMap(o); final List properties = new LinkedList(); @@ -453,4 +497,39 @@ static PropertyValue[] interpretHillshadeLayerProperties(Object o) { return properties.toArray(new PropertyValue[properties.size()]); } + + static PropertyValue[] interpretHeatmapLayerProperties(Object o) { + final Map data = (Map) toMap(o); + final List properties = new LinkedList(); + final JsonParser parser = new JsonParser(); + + for (Map.Entry entry : data.entrySet()) { + final JsonElement jsonElement = parser.parse(entry.getValue()); + Expression expression = Expression.Converter.convert(jsonElement); + switch (entry.getKey()) { + case "heatmap-radius": + properties.add(PropertyFactory.heatmapRadius(expression)); + break; + case "heatmap-weight": + properties.add(PropertyFactory.heatmapWeight(expression)); + break; + case "heatmap-intensity": + properties.add(PropertyFactory.heatmapIntensity(expression)); + break; + case "heatmap-color": + properties.add(PropertyFactory.heatmapColor(expression)); + break; + case "heatmap-opacity": + properties.add(PropertyFactory.heatmapOpacity(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java index a7a27e866..44121b20c 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java @@ -14,7 +14,8 @@ class MapboxMapBuilder implements MapboxMapOptionsSink { public final String TAG = getClass().getSimpleName(); - private final MapboxMapOptions options = new MapboxMapOptions().attributionEnabled(true); + private final MapboxMapOptions options = + new MapboxMapOptions().textureMode(true).attributionEnabled(true); private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private boolean dragEnabled = true; diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java index f45556c9f..3499d4ef5 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java @@ -4,6 +4,9 @@ package com.mapbox.mapboxgl; +import static com.mapbox.mapboxsdk.style.layers.Property.*; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; + import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; @@ -37,6 +40,7 @@ import com.mapbox.android.telemetry.TelemetryEnabler; import com.mapbox.geojson.Feature; import com.mapbox.geojson.FeatureCollection; +import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdate; @@ -58,6 +62,8 @@ import com.mapbox.mapboxsdk.maps.Style; import com.mapbox.mapboxsdk.offline.OfflineManager; import com.mapbox.mapboxsdk.plugins.localization.LocalizationPlugin; +import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter; +import com.mapbox.mapboxsdk.storage.FileSource; import com.mapbox.mapboxsdk.style.expressions.Expression; import com.mapbox.mapboxsdk.style.layers.CircleLayer; import com.mapbox.mapboxsdk.style.layers.FillExtrusionLayer; @@ -85,6 +91,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; /** Controller of a single MapboxMaps MapView instance. */ @SuppressLint("MissingPermission") @@ -101,6 +108,7 @@ final class MapboxMapController OnMapReadyCallback, OnCameraTrackingChangedListener, PlatformView { + private static final String TAG = "MapboxMapController"; private final int id; private final MethodChannel methodChannel; @@ -108,6 +116,9 @@ final class MapboxMapController private final float density; private final Context context; private final String styleStringInitial; + private final Set interactiveFeatureLayerIds; + private final Map addedFeaturesByLayer; + private final Map mSnapshotterMap; private MapView mapView; private MapboxMap mapboxMap; private boolean trackCameraPosition = false; @@ -124,13 +135,8 @@ final class MapboxMapController private Style style; private Feature draggedFeature; private AndroidGesturesManager androidGesturesManager; - private LatLng dragOrigin; private LatLng dragPrevious; - - private Set interactiveFeatureLayerIds; - private Map addedFeaturesByLayer; - private LatLngBounds bounds = null; Style.OnStyleLoaded onStyleLoadedCallback = new Style.OnStyleLoaded() { @@ -174,7 +180,7 @@ public void onStyleLoaded(@NonNull Style style) { if (dragEnabled) { this.androidGesturesManager = new AndroidGesturesManager(this.mapView.getContext(), false); } - + this.mSnapshotterMap = new HashMap<>(); methodChannel = new MethodChannel(messenger, "plugins.flutter.io/mapbox_maps_" + id); methodChannel.setMethodCallHandler(this); } @@ -393,6 +399,7 @@ private void addSymbolLayer( PropertyValue[] properties, boolean enableInteraction, Expression filter) { + SymbolLayer symbolLayer = new SymbolLayer(layerName, sourceName); symbolLayer.setProperties(properties); if (sourceLayer != null) { @@ -485,6 +492,40 @@ private void addFillLayer( } } + private void addFillExtrusionLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + FillExtrusionLayer fillLayer = new FillExtrusionLayer(layerName, sourceName); + fillLayer.setProperties(properties); + if (sourceLayer != null) { + fillLayer.setSourceLayer(sourceLayer); + } + if (minZoom != null) { + fillLayer.setMinZoom(minZoom); + } + if (maxZoom != null) { + fillLayer.setMaxZoom(maxZoom); + } + if (filter != null) { + fillLayer.setFilter(filter); + } + if (belowLayerId != null) { + style.addLayerBelow(fillLayer, belowLayerId); + } else { + style.addLayer(fillLayer); + } + if (enableInteraction) { + interactiveFeatureLayerIds.add(layerName); + } + } + private void addCircleLayer( String layerName, String sourceName, @@ -571,13 +612,38 @@ private void addHillshadeLayer( } } + private void addHeatmapLayer( + String layerName, + String sourceName, + Float minZoom, + Float maxZoom, + String belowLayerId, + PropertyValue[] properties, + Expression filter) { + HeatmapLayer layer = new HeatmapLayer(layerName, sourceName); + layer.setProperties(properties); + if (minZoom != null) { + layer.setMinZoom(minZoom); + } + if (maxZoom != null) { + layer.setMaxZoom(maxZoom); + } + if (belowLayerId != null) { + style.addLayerBelow(layer, belowLayerId); + } else { + style.addLayer(layer); + } + } + private Feature firstFeatureOnLayers(RectF in) { if (style != null) { final List layers = style.getLayers(); final List layersInOrder = new ArrayList(); for (Layer layer : layers) { String id = layer.getId(); - if (interactiveFeatureLayerIds.contains(id)) layersInOrder.add(id); + if (interactiveFeatureLayerIds.contains(id)) { + layersInOrder.add(id); + } } Collections.reverse(layersInOrder); @@ -849,6 +915,7 @@ public void onError(@NonNull String message) { Expression filterExpression = parseFilter(filter); + removeLayer(layerId); addSymbolLayer( layerId, sourceId, @@ -879,6 +946,7 @@ public void onError(@NonNull String message) { Expression filterExpression = parseFilter(filter); + removeLayer(layerId); addLineLayer( layerId, sourceId, @@ -909,6 +977,7 @@ public void onError(@NonNull String message) { Expression filterExpression = parseFilter(filter); + removeLayer(layerId); addFillLayer( layerId, sourceId, @@ -921,6 +990,38 @@ public void onError(@NonNull String message) { filterExpression); updateLocationComponentLayer(); + result.success(null); + break; + } + case "fillExtrusionLayer#add": + { + final String sourceId = call.argument("sourceId"); + final String layerId = call.argument("layerId"); + final String belowLayerId = call.argument("belowLayerId"); + final String sourceLayer = call.argument("sourceLayer"); + final Double minzoom = call.argument("minzoom"); + final Double maxzoom = call.argument("maxzoom"); + final String filter = call.argument("filter"); + final boolean enableInteraction = call.argument("enableInteraction"); + final PropertyValue[] properties = + LayerPropertyConverter.interpretFillExtrusionLayerProperties( + call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + removeLayer(layerId); + addFillExtrusionLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + result.success(null); break; } @@ -939,6 +1040,7 @@ public void onError(@NonNull String message) { Expression filterExpression = parseFilter(filter); + removeLayer(layerId); addCircleLayer( layerId, sourceId, @@ -963,6 +1065,8 @@ public void onError(@NonNull String message) { final Double maxzoom = call.argument("maxzoom"); final PropertyValue[] properties = LayerPropertyConverter.interpretRasterLayerProperties(call.argument("properties")); + + removeLayer(layerId); addRasterLayer( layerId, sourceId, @@ -995,6 +1099,28 @@ public void onError(@NonNull String message) { null); updateLocationComponentLayer(); + result.success(null); + break; + } + case "heatmapLayer#add": + { + final String sourceId = call.argument("sourceId"); + final String layerId = call.argument("layerId"); + final String belowLayerId = call.argument("belowLayerId"); + final Double minzoom = call.argument("minzoom"); + final Double maxzoom = call.argument("maxzoom"); + final PropertyValue[] properties = + LayerPropertyConverter.interpretHeatmapLayerProperties(call.argument("properties")); + addHeatmapLayer( + layerId, + sourceId, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + belowLayerId, + properties, + null); + updateLocationComponentLayer(); + result.success(null); break; } @@ -1063,6 +1189,32 @@ public void onFailure(@NonNull Exception exception) { result.success(null); break; } + case "style#updateImageSource": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + ImageSource imageSource = style.getSourceAs(call.argument("imageSourceId")); + List coordinates = Convert.toLatLngList(call.argument("coordinates"), false); + if (coordinates != null) { + // https://github.com/mapbox/mapbox-maps-android/issues/302 + imageSource.setCoordinates( + new LatLngQuad( + coordinates.get(0), + coordinates.get(1), + coordinates.get(2), + coordinates.get(3))); + } + byte[] bytes = call.argument("bytes"); + if (bytes != null) { + imageSource.setImage(BitmapFactory.decodeByteArray(bytes, 0, call.argument("length"))); + } + result.success(null); + break; + } case "style#addSource": { final String id = Convert.toString(call.argument("sourceId")); @@ -1139,8 +1291,7 @@ public void onFailure(@NonNull Exception exception) { null); } String layerId = call.argument("layerId"); - style.removeLayer(layerId); - interactiveFeatureLayerIds.remove(layerId); + removeLayer(layerId); result.success(null); break; @@ -1185,6 +1336,99 @@ public void onFailure(@NonNull Exception exception) { result.success(null); break; } + case "style#setVisibility": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + String layerId = call.argument("layerId"); + boolean isVisible = call.argument("isVisible"); + Layer layer = style.getLayer(layerId); + if (layer != null) { + layer.setProperties(isVisible ? visibility(VISIBLE) : visibility(NONE)); + } + + result.success(null); + break; + } + case "snapshot#takeSnapshot": + { + FileSource.getInstance(context).activate(); + MapSnapshotter.Options snapShotOptions = + new MapSnapshotter.Options( + (int) call.argument("width"), (int) call.argument("height")); + + snapShotOptions.withLogo((boolean) call.argument("withLogo")); + Style.Builder styleBuilder = new Style.Builder(); + if (call.hasArgument("styleUri")) { + styleBuilder.fromUri((String) call.argument("styleUri")); + } else if (call.hasArgument("styleJson")) { + styleBuilder.fromJson((String) call.argument("styleJson")); + } else { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + styleBuilder.fromUri(style.getUri()); + } + snapShotOptions.withStyleBuilder(styleBuilder); + if (call.hasArgument("bounds")) { + FeatureCollection bounds = FeatureCollection.fromJson((String) call.argument("bounds")); + snapShotOptions.withRegion(GeoJSONUtils.toLatLngBounds(bounds)); + } else if (call.hasArgument("centerCoordinate")) { + Feature centerPoint = Feature.fromJson((String) call.argument("centerCoordinate")); + CameraPosition cameraPosition = + new CameraPosition.Builder() + .target(GeoJSONUtils.toLatLng((Point) centerPoint.geometry())) + .tilt((double) call.argument("pitch")) + .bearing((double) call.argument("heading")) + .zoom((double) call.argument("zoomLevel")) + .build(); + snapShotOptions.withCameraPosition(cameraPosition); + } else { + snapShotOptions.withRegion(mapboxMap.getProjection().getVisibleRegion().latLngBounds); + } + + final MapSnapshotter snapshotter = new MapSnapshotter(context, snapShotOptions); + final String snapshotterID = UUID.randomUUID().toString(); + mSnapshotterMap.put(snapshotterID, snapshotter); + + snapshotter.start( + snapshot -> { + Bitmap bitmap = snapshot.getBitmap(); + + String result1; + if ((boolean) call.argument("writeToDisk")) { + result1 = BitmapUtils.createTempFile(context, bitmap); + } else { + result1 = BitmapUtils.createBase64(bitmap); + } + + if (result1 == null) { + result.error( + "NO_RESULT", + "Could not generate snapshot, please check Android logs for more info.", + null); + return; + } + + result.success(result1); + mSnapshotterMap.remove(snapshotterID); + }, + new MapSnapshotter.ErrorHandler() { + @Override + public void onError(String error) { + result.error("SNAPSHOT_ERROR", error, null); + mSnapshotterMap.remove(snapshotterID); + } + }); + break; + } default: result.notImplemented(); } @@ -1337,12 +1581,14 @@ private void destroyMapViewIfNecessary() { return; } + mapView.onStop(); + mapView.onDestroy(); + if (locationComponent != null) { locationComponent.setLocationComponentEnabled(false); } stopListeningForLocationUpdates(); - mapView.onDestroy(); mapView = null; } @@ -1739,6 +1985,13 @@ boolean onMove(MoveGestureDetector detector) { return true; } + void removeLayer(String layerId) { + if (style != null && layerId != null) { + style.removeLayer(layerId); + interactiveFeatureLayerIds.remove(layerId); + } + } + void onMoveEnd(MoveGestureDetector detector) { PointF pointf = detector.getFocalPoint(); invokeFeatureDrag(pointf, "end"); @@ -1767,6 +2020,7 @@ void stopDragging() { /** Simple Listener to listen for the status of camera movements. */ public class OnCameraMoveFinishedListener implements MapboxMap.CancelableCallback { + @Override public void onFinish() {} diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java index 3f25d26a6..8eac1fdfe 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java @@ -17,7 +17,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** * Plugin for controlling a set of MapboxMap views to be shown as overlays on top of the Flutter @@ -86,41 +85,6 @@ public void onDetachedFromActivity() { lifecycle = null; } - // Old Plugin APIs - - public static void registerWith(Registrar registrar) { - final Activity activity = registrar.activity(); - if (activity == null) { - // When a background flutter view tries to register the plugin, the registrar has no activity. - // We stop the registration process as this plugin is foreground only. - return; - } - if (activity instanceof LifecycleOwner) { - registrar - .platformViewRegistry() - .registerViewFactory( - VIEW_TYPE, - new MapboxMapFactory( - registrar.messenger(), - new LifecycleProvider() { - @Override - public Lifecycle getLifecycle() { - return ((LifecycleOwner) activity).getLifecycle(); - } - })); - } else { - registrar - .platformViewRegistry() - .registerViewFactory( - VIEW_TYPE, - new MapboxMapFactory(registrar.messenger(), new ProxyLifecycleProvider(activity))); - } - - MethodChannel methodChannel = - new MethodChannel(registrar.messenger(), "plugins.flutter.io/mapbox_gl"); - methodChannel.setMethodCallHandler(new GlobalMethodHandler(registrar)); - } - private static final class ProxyLifecycleProvider implements Application.ActivityLifecycleCallbacks, LifecycleOwner, LifecycleProvider { diff --git a/example/.gitignore b/example/.gitignore index 47e0b4d62..92b28e840 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -9,6 +9,7 @@ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 94ab59de9..c5a736cc4 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,8 @@ +plugins { + id "com.android.application" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +11,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,12 +21,10 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 31 - ndkVersion "20.1.5948944" + namespace "com.mapbox.mapboxglexample" + compileSdkVersion 35 + ndkVersion "25.1.8937393" lintOptions { disable 'InvalidPackage' @@ -35,8 +33,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.mapbox.mapboxglexample" - minSdkVersion 20 - targetSdkVersion 31 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index bba79f17c..519763b2e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @drawable/launch_background + diff --git a/example/android/build.gradle b/example/android/build.gradle index 8ae8ba30a..ad89b89e3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,14 +1,3 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' - } -} - allprojects { repositories { google() @@ -24,6 +13,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register("clean", Delete) { + delete rootProject.layout.buildDirectory } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 8428c0be2..15de90249 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip \ No newline at end of file diff --git a/example/android/settings.gradle b/example/android/settings.gradle index f860b7645..4f97b7cc6 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,24 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" \ No newline at end of file +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.2.2' apply false +} + +include ":app" \ No newline at end of file diff --git a/example/assets/fill-extrusion/indoor_3d_map.json b/example/assets/fill-extrusion/indoor_3d_map.json new file mode 100644 index 000000000..ac81fd8ca --- /dev/null +++ b/example/assets/fill-extrusion/indoor_3d_map.json @@ -0,0 +1,685 @@ +{ + "features": [ + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Bird Exhibit", + "height": 0, + "base_height": 0, + "color": "orange" + }, + "geometry": { + "coordinates": [ + [ + [-87.618312, 41.866282], + [-87.61832, 41.866674], + [-87.618087, 41.866676], + [-87.618087, 41.866661], + [-87.617423, 41.86667], + [-87.617416, 41.8663], + [-87.618312, 41.866282] + ] + ], + "type": "Polygon" + }, + "id": "06e8fa0b3f851e3ae0e1da5fc17e111e" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Bird Exhibit", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.617808, 41.866266], + [-87.617806, 41.866293], + [-87.617415, 41.866298], + [-87.617424, 41.866671], + [-87.617382, 41.866669], + [-87.617371, 41.866274], + [-87.617808, 41.866266] + ] + ], + "type": "Polygon" + }, + "id": "08a10ab2bf15c4d14669b588062f7f08" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "East Hallway", + "base_height": 0, + "height": 0, + "color": "indigo" + }, + "geometry": { + "coordinates": [ + [ + [-87.616704, 41.866141], + [-87.616707, 41.866338], + [-87.61572, 41.866355], + [-87.61572, 41.866148], + [-87.616704, 41.866141] + ] + ], + "type": "Polygon" + }, + "id": "09ead44537d94ece576c7bc001c33e53" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "East Entrance", + "base_height": 0, + "height": 0, + "color": "violet" + }, + "geometry": { + "coordinates": [ + [ + [-87.61544, 41.866149], + [-87.615449, 41.866358], + [-87.615721, 41.866355], + [-87.61572, 41.866143], + [-87.61544, 41.866149] + ] + ], + "type": "Polygon" + }, + "id": "12251ebf764b36cf3b8c5ad42e2deb29" + }, + { + "type": "Feature", + "properties": { + "height": 0, + "base_height": 0, + "level": 1, + "name": "Under the Earth", + "color": "red" + }, + "geometry": { + "coordinates": [ + [ + [-87.616701, 41.865816], + [-87.616705, 41.866115], + [-87.615712, 41.866125], + [-87.615706, 41.865802], + [-87.615936, 41.865801], + [-87.615938, 41.865824], + [-87.616701, 41.865816] + ] + ], + "type": "Polygon" + }, + "id": "1ce4f8c186a40b9927006644e27bfd69" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Atrium", + "base_height": 0, + "height": 0, + "color": "yellow" + }, + "geometry": { + "coordinates": [ + [ + [-87.617365, 41.865799], + [-87.6167, 41.865815], + [-87.616718, 41.866672], + [-87.617384, 41.86667], + [-87.617365, 41.865799] + ] + ], + "type": "Polygon" + }, + "id": "369f5d8865e677120b7576b1de6082eb" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Ancient Egypt", + "height": 0, + "base_height": 0, + "color": "blue" + }, + "geometry": { + "coordinates": [ + [ + [-87.61807, 41.865761], + [-87.618299, 41.865758], + [-87.618307, 41.866139], + [-87.617407, 41.86615], + [-87.617399, 41.865799], + [-87.61807, 41.865789], + [-87.61807, 41.865761] + ] + ], + "type": "Polygon" + }, + "id": "3e9f198afe6d7dff01ac81c6eaa511fb" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "West Arch", + "height": 40, + "base_height": 30, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.617424, 41.86667], + [-87.617384, 41.86667], + [-87.617365, 41.865799], + [-87.617405, 41.865799], + [-87.617424, 41.86667] + ] + ], + "type": "Polygon" + }, + "id": "402706f28b793d27c78d9615fff747f2" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Bird Exhibit", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.618312, 41.866283], + [-87.61797, 41.866288], + [-87.617972, 41.866265], + [-87.618312, 41.866259], + [-87.618312, 41.866283] + ] + ], + "type": "Polygon" + }, + "id": "43e1e2fc8399dee075ad59764f2a2878" + }, + { + "type": "Feature", + "properties": { + "name": "Arch", + "level": 1, + "color": "grey", + "base_height": 30, + "height": 40 + }, + "geometry": { + "coordinates": [ + [ + [-87.617971, 41.866291], + [-87.617973, 41.866265], + [-87.617805, 41.866267], + [-87.617806, 41.866294], + [-87.617971, 41.866291] + ] + ], + "type": "Polygon" + }, + "id": "4528ad9b9264cbec65bb2e55ac0012c1" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "color": "grey", + "base_height": 30, + "height": 40, + "name": "Arch" + }, + "geometry": { + "coordinates": [ + [ + [-87.617979, 41.866167], + [-87.617797, 41.866168], + [-87.617799, 41.866145], + [-87.617976, 41.866144], + [-87.617979, 41.866167] + ] + ], + "type": "Polygon" + }, + "id": "4be6817c3b595042c8d971eebd0c4fd3" + }, + { + "type": "Feature", + "properties": { + "name": "Kids Zone", + "level": 1, + "base_height": 0, + "height": 0, + "color": "green" + }, + "geometry": { + "coordinates": [ + [ + [-87.616718, 41.866675], + [-87.616709, 41.866371], + [-87.615725, 41.866381], + [-87.615732, 41.866711], + [-87.61596, 41.866711], + [-87.61596, 41.866688], + [-87.616718, 41.866675] + ] + ], + "type": "Polygon" + }, + "id": "5775eba03674ea1cb3550ffb38321432" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "color": "grey", + "name": "Arch", + "height": 40, + "base_height": 30 + }, + "geometry": { + "coordinates": [ + [ + [-87.616286, 41.866119], + [-87.616286, 41.866144], + [-87.616089, 41.866149], + [-87.616086, 41.86612], + [-87.616286, 41.866119] + ] + ], + "type": "Polygon" + }, + "id": "598832b1512dc9facc855c5367251531" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Arch", + "color": "grey", + "height": 40, + "base_height": 30 + }, + "geometry": { + "coordinates": [ + [ + [-87.616287, 41.866343], + [-87.616288, 41.866374], + [-87.616076, 41.866378], + [-87.616077, 41.866347], + [-87.616287, 41.866343] + ] + ], + "type": "Polygon" + }, + "id": "59ed13d4411ff5f88714d9af539788d2" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "color": "grey", + "name": "center_arch", + "base_height": 30, + "height": 40 + }, + "geometry": { + "coordinates": [ + [ + [-87.61737, 41.866198], + [-87.617372, 41.86624], + [-87.616708, 41.866243], + [-87.616708, 41.866203], + [-87.61737, 41.866198] + ] + ], + "type": "Polygon" + }, + "id": "67f8952a18dfe21ee0744a3e86bc41d8" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Kids Zone", + "height": 0, + "base_height": 40, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.615719, 41.866354], + [-87.615718, 41.866381], + [-87.616077, 41.866378], + [-87.616077, 41.866347], + [-87.615719, 41.866354] + ] + ], + "type": "Polygon" + }, + "id": "6bb2c92386bcf4678113d6b3e400ae3b" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Under the Earth", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.616089, 41.866149], + [-87.616089, 41.866119], + [-87.615711, 41.866124], + [-87.615713, 41.866147], + [-87.616089, 41.866149] + ] + ], + "type": "Polygon" + }, + "id": "844c87987089df6b9db3923f6a00e4d6" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Under the Earth", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.616707, 41.866115], + [-87.616286, 41.866119], + [-87.616285, 41.866144], + [-87.616705, 41.866141], + [-87.616707, 41.866115] + ] + ], + "type": "Polygon" + }, + "id": "85547a1ecdbd2ca1fdc6324aea3c6b70" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "North Entrance", + "height": 0, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.617386, 41.86667], + [-87.617388, 41.866786], + [-87.617228, 41.866786], + [-87.617226, 41.866848], + [-87.616867, 41.866849], + [-87.616868, 41.866791], + [-87.616718, 41.866791], + [-87.616718, 41.866672], + [-87.617386, 41.86667] + ] + ], + "type": "Polygon" + }, + "id": "91ab1ee01729c33568c7b57eb258e699" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Ancient Egypt", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.617394, 41.865799], + [-87.617405, 41.86615], + [-87.617802, 41.866147], + [-87.6178, 41.866167], + [-87.617369, 41.866172], + [-87.617364, 41.865799], + [-87.617394, 41.865799] + ] + ], + "type": "Polygon" + }, + "id": "943171d55d207024791e15294def7e8f" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Ancient Egypt", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.617976, 41.866166], + [-87.617976, 41.866143], + [-87.618309, 41.866139], + [-87.618309, 41.866161], + [-87.617976, 41.866166] + ] + ], + "type": "Polygon" + }, + "id": "a37230898905988cab9b72927dc99258" + }, + { + "type": "Feature", + "properties": { + "name": "West Hallway", + "level": 1, + "base_height": 0, + "height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.618309, 41.866161], + [-87.618312, 41.86626], + [-87.61737, 41.866272], + [-87.61737, 41.86617], + [-87.618309, 41.866161] + ] + ], + "type": "Polygon" + }, + "id": "c7cc79da8711d746985f23a9b22c1d5e" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "Kids Zone", + "height": 40, + "base_height": 0, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.616286, 41.866343], + [-87.616286, 41.866374], + [-87.61671, 41.866371], + [-87.616708, 41.866338], + [-87.616286, 41.866343] + ] + ], + "type": "Polygon" + }, + "id": "cfbf2aee6a73a45c12e7fc7432d6009e" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "South Entrance", + "height": 0, + "base_height": 0, + "color": "teal" + }, + "geometry": { + "coordinates": [ + [ + [-87.617359, 41.865801], + [-87.617355, 41.865674], + [-87.617221, 41.865677], + [-87.617219, 41.86559], + [-87.616824, 41.865595], + [-87.616826, 41.86568], + [-87.616695, 41.865683], + [-87.6167, 41.865813], + [-87.617359, 41.865801] + ] + ], + "type": "Polygon" + }, + "id": "d71ab89467076ad023161c37f4ff0d5f" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "East Arch", + "height": 40, + "base_height": 30, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.616688, 41.866675], + [-87.616716, 41.866675], + [-87.616699, 41.865814], + [-87.616665, 41.865814], + [-87.616688, 41.866675] + ] + ], + "type": "Polygon" + }, + "id": "dd2baec57e4079eb65dcae5be495da62" + }, + { + "type": "Feature", + "properties": { + "level": 1, + "name": "outer-walls", + "base_height": 0, + "height": 40, + "color": "grey" + }, + "geometry": { + "coordinates": [ + [ + [-87.618087, 41.86666], + [-87.618087, 41.866674], + [-87.618319, 41.866674], + [-87.618298, 41.865757], + [-87.618326, 41.865756], + [-87.618347, 41.866693], + [-87.618068, 41.866696], + [-87.618067, 41.866675], + [-87.61741, 41.866684], + [-87.617416, 41.866802], + [-87.617247, 41.866803], + [-87.617246, 41.866866], + [-87.616846, 41.866868], + [-87.616848, 41.866807], + [-87.61669, 41.866811], + [-87.616693, 41.866693], + [-87.615992, 41.866701], + [-87.615992, 41.866729], + [-87.615703, 41.866733], + [-87.615689, 41.866377], + [-87.615412, 41.866382], + [-87.615411, 41.866122], + [-87.615689, 41.866119], + [-87.615682, 41.865781], + [-87.615966, 41.865779], + [-87.615969, 41.865798], + [-87.616669, 41.865794], + [-87.616663, 41.865662], + [-87.6168, 41.865661], + [-87.616796, 41.865571], + [-87.61726, 41.865559], + [-87.617258, 41.865654], + [-87.617388, 41.865652], + [-87.617395, 41.865778], + [-87.618045, 41.865773], + [-87.618044, 41.865742], + [-87.618325, 41.865739], + [-87.618326, 41.865758], + [-87.61807, 41.865761], + [-87.61807, 41.865789], + [-87.617356, 41.8658], + [-87.617356, 41.865672], + [-87.617218, 41.865677], + [-87.617215, 41.86559], + [-87.616822, 41.865595], + [-87.616827, 41.86568], + [-87.616694, 41.865681], + [-87.616697, 41.865813], + [-87.615939, 41.865824], + [-87.615937, 41.8658], + [-87.615706, 41.865802], + [-87.615713, 41.866143], + [-87.615441, 41.86615], + [-87.61545, 41.866357], + [-87.615724, 41.866353], + [-87.615733, 41.866712], + [-87.615959, 41.86671], + [-87.615959, 41.866688], + [-87.616719, 41.866672], + [-87.61672, 41.866791], + [-87.616869, 41.86679], + [-87.616868, 41.86685], + [-87.617223, 41.866847], + [-87.617227, 41.866786], + [-87.617387, 41.866785], + [-87.617383, 41.86667], + [-87.618087, 41.86666] + ] + ], + "type": "Polygon" + }, + "id": "ef6512f46485e27963c248bcc945c3db" + } + ], + "type": "FeatureCollection" +} diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c40f8995b..846740d33 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -381,10 +381,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -512,10 +509,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -539,10 +533,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a8..175629709 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,13 @@ -import UIKit import Flutter +import UIKit @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 8fbf2ef03..922b790c3 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -51,5 +51,7 @@ Shows your location on the map and helps improve the map NSLocationAlwaysUsageDescription Shows your location on the map and helps improve the map + CADisableMinimumFrameDurationOnPhone + diff --git a/example/lib/animate_camera.dart b/example/lib/animate_camera.dart index 36f6a9c41..ec9cd55c9 100644 --- a/example/lib/animate_camera.dart +++ b/example/lib/animate_camera.dart @@ -79,6 +79,7 @@ class AnimateCameraState extends State { CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), + duration: Duration(seconds: 5), ) .then((result) => print( "mapController.animateCamera() returned $result")); diff --git a/example/lib/click_annotations.dart b/example/lib/click_annotations.dart index 1a43c3b84..77a1876ac 100644 --- a/example/lib/click_annotations.dart +++ b/example/lib/click_annotations.dart @@ -28,6 +28,7 @@ class ClickAnnotationBody extends StatefulWidget { class ClickAnnotationBodyState extends State { ClickAnnotationBodyState(); static const LatLng center = const LatLng(-33.88, 151.16); + bool overlapping = false; MapboxMapController? controller; @@ -131,20 +132,37 @@ class ClickAnnotationBodyState extends State { @override Widget build(BuildContext context) { - return MapboxMap( - accessToken: MapsDemo.ACCESS_TOKEN, - annotationOrder: [ - AnnotationType.fill, - AnnotationType.line, - AnnotationType.circle, - AnnotationType.symbol, - ], - onMapCreated: _onMapCreated, - onStyleLoadedCallback: _onStyleLoaded, - initialCameraPosition: const CameraPosition( - target: center, - zoom: 12.0, + return Scaffold( + body: MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + annotationOrder: [ + AnnotationType.fill, + AnnotationType.line, + AnnotationType.circle, + AnnotationType.symbol, + ], + onMapCreated: _onMapCreated, + onStyleLoadedCallback: _onStyleLoaded, + initialCameraPosition: const CameraPosition( + target: center, + zoom: 12.0, + ), ), + floatingActionButton: ElevatedButton( + onPressed: () { + setState(() { + overlapping = !overlapping; + }); + controller!.setSymbolIconAllowOverlap(overlapping); + controller!.setSymbolIconIgnorePlacement(overlapping); + + controller!.setSymbolTextAllowOverlap(overlapping); + controller!.setSymbolTextIgnorePlacement(overlapping); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text("Toggle overlapping"), + )), ); } } diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart index ae1e051c7..48574c614 100644 --- a/example/lib/generated_plugin_registrant.dart +++ b/example/lib/generated_plugin_registrant.dart @@ -4,8 +4,8 @@ // ignore_for_file: directives_ordering // ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages -import 'package:device_info_plus_web/device_info_plus_web.dart'; import 'package:location_web/location_web.dart'; import 'package:mapbox_gl_web/mapbox_gl_web.dart'; @@ -13,7 +13,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; // ignore: public_member_api_docs void registerPlugins(Registrar registrar) { - DeviceInfoPlusPlugin.registerWith(registrar); LocationWebPlugin.registerWith(registrar); MapboxMapPlugin.registerWith(registrar); registrar.registerMessageHandler(); diff --git a/example/lib/layer.dart b/example/lib/layer.dart index 240383629..bdcd8828e 100644 --- a/example/lib/layer.dart +++ b/example/lib/layer.dart @@ -24,7 +24,9 @@ class LayerState extends State { late MapboxMapController controller; Timer? bikeTimer; Timer? filterTimer; + Timer? visibilityTimer; int filteredId = 0; + bool isVisible = true; @override Widget build(BuildContext context) { @@ -148,12 +150,18 @@ class LayerState extends State { filteredId = filteredId == 0 ? 1 : 0; controller.setFilter('fills', ['==', 'id', filteredId]); }); + + visibilityTimer = Timer.periodic(Duration(seconds: 5), (t) { + isVisible = !isVisible; + controller.setVisibility('water', isVisible); + }); } @override void dispose() { bikeTimer?.cancel(); filterTimer?.cancel(); + visibilityTimer?.cancel(); super.dispose(); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 2b6e4c899..e28938b2c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,32 +4,32 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:location/location.dart'; -import 'package:mapbox_gl_example/custom_marker.dart'; -import 'package:mapbox_gl_example/full_map.dart'; -import 'package:mapbox_gl_example/offline_regions.dart'; -import 'package:mapbox_gl_example/place_batch.dart'; -import 'package:mapbox_gl_example/layer.dart'; -import 'package:mapbox_gl_example/sources.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; import 'animate_camera.dart'; import 'annotation_order_maps.dart'; +import 'click_annotations.dart'; +import 'custom_marker.dart'; import 'full_map.dart'; +import 'layer.dart'; import 'line.dart'; import 'local_style.dart'; import 'map_ui.dart'; import 'move_camera.dart'; -import 'click_annotations.dart'; +import 'offline_regions.dart'; import 'page.dart'; +import 'place_batch.dart'; import 'place_circle.dart'; +import 'place_fill.dart'; import 'place_source.dart'; import 'place_symbol.dart'; -import 'place_fill.dart'; import 'scrolling_map.dart'; -import 'package:mapbox_gl/mapbox_gl.dart'; +import 'sources.dart'; +import 'take_snapshot.dart'; final List _allPages = [ MapUiPage(), @@ -48,6 +48,7 @@ final List _allPages = [ AnnotationOrderPage(), CustomMarkerPage(), BatchAddPage(), + TakeSnapPage(), ClickAnnotationPage(), Sources() ]; diff --git a/example/lib/map_ui.dart b/example/lib/map_ui.dart index b10a0334a..5be6b2bd8 100644 --- a/example/lib/map_ui.dart +++ b/example/lib/map_ui.dart @@ -351,7 +351,7 @@ class MapUiBodyState extends State { "Map click: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); print("Filter $_featureQueryFilter"); List features = await mapController! - .queryRenderedFeatures(point, [], _featureQueryFilter); + .queryRenderedFeatures(point, ["landuse"], _featureQueryFilter); print('# features: ${features.length}'); _clearFill(); if (features.isEmpty && _featureQueryFilter != null) { diff --git a/example/lib/place_source.dart b/example/lib/place_source.dart index 426a7760d..428dc74aa 100644 --- a/example/lib/place_source.dart +++ b/example/lib/place_source.dart @@ -64,6 +64,23 @@ class PlaceSymbolBodyState extends State { ); } + /// Update an asset image as a source to the currently displayed style + Future updateImageSourceFromAsset( + String imageSourceId, String assetName) async { + final ByteData bytes = await rootBundle.load(assetName); + final Uint8List list = bytes.buffer.asUint8List(); + return controller.updateImageSource( + imageSourceId, + list, + const LatLngQuad( + bottomRight: LatLng(-33.89884564291081, 151.25229835510254), + bottomLeft: LatLng(-33.89884564291081, 151.20131492614746), + topLeft: LatLng(-33.934601369931634, 151.20131492614746), + topRight: LatLng(-33.934601369931634, 151.25229835510254), + ), + ); + } + Future removeImageSource(String imageSourceId) { return controller.removeSource(imageSourceId); } @@ -97,17 +114,14 @@ class PlaceSymbolBodyState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( - child: SizedBox( - width: 300.0, - height: 200.0, - child: MapboxMap( - accessToken: MapsDemo.ACCESS_TOKEN, - onMapCreated: _onMapCreated, - initialCameraPosition: const CameraPosition( - target: LatLng(-33.852, 151.211), - zoom: 11.0, - ), + SizedBox( + height: 300.0, + child: MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 10.0, ), ), ), @@ -130,6 +144,18 @@ class PlaceSymbolBodyState extends State { }); }, ), + TextButton( + child: const Text('Update source (asset image)'), + onPressed: !sourceAdded + ? null + : () { + updateImageSourceFromAsset(SOURCE_ID, + 'assets/symbols/custom-icon.png') + .then((value) { + setState(() => sourceAdded = true); + }); + }, + ), TextButton( child: const Text('Remove source (asset image)'), onPressed: sourceAdded diff --git a/example/lib/place_symbol.dart b/example/lib/place_symbol.dart index 2edc59051..c4aa68eb9 100644 --- a/example/lib/place_symbol.dart +++ b/example/lib/place_symbol.dart @@ -275,8 +275,8 @@ class PlaceSymbolBodyState extends State { setState(() { _iconAllowOverlap = !_iconAllowOverlap; }); - controller!.setSymbolIconAllowOverlap(_iconAllowOverlap); - controller!.setSymbolTextAllowOverlap(_iconAllowOverlap); + await controller!.setSymbolIconAllowOverlap(_iconAllowOverlap); + await controller!.setSymbolTextAllowOverlap(_iconAllowOverlap); } @override @@ -287,8 +287,7 @@ class PlaceSymbolBodyState extends State { children: [ Center( child: SizedBox( - width: 300.0, - height: 200.0, + height: 300.0, child: MapboxMap( accessToken: MapsDemo.ACCESS_TOKEN, onMapCreated: _onMapCreated, diff --git a/example/lib/sources.dart b/example/lib/sources.dart index 86200c154..04e4c9e76 100644 --- a/example/lib/sources.dart +++ b/example/lib/sources.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; @@ -99,6 +102,89 @@ class FullMapState extends State { )); } + static Future addGeojsonHeatmap(MapboxMapController controller) async { + await controller.addSource( + "earthquakes-heatmap-source", + GeojsonSourceProperties( + data: + 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson', + )); + await controller.addLayer( + "earthquakes-heatmap-source", + "earthquakes-heatmap-layer", + HeatmapLayerProperties( + heatmapColor: [ + Expressions.interpolate, + ["linear"], + ["heatmap-density"], + 0, + "rgba(33.0, 102.0, 172.0, 0.0)", + 0.2, + "rgb(103.0, 169.0, 207.0)", + 0.4, + "rgb(209.0, 229.0, 240.0)", + 0.6, + "rgb(253.0, 219.0, 240.0)", + 0.8, + "rgb(239.0, 138.0, 98.0)", + 1, + "rgb(178.0, 24.0, 43.0)", + ], + heatmapWeight: [ + Expressions.interpolate, + ["linear"], + [Expressions.get, "mag"], + 0, + 0, + 6, + 1, + ], + heatmapIntensity: [ + Expressions.interpolate, + ["linear"], + [Expressions.zoom], + 0, + 1, + 9, + 3, + ], + heatmapRadius: [ + Expressions.interpolate, + ["linear"], + [Expressions.zoom], + 0, + 2, + 9, + 20, + ], + heatmapOpacity: [ + Expressions.interpolate, + ["linear"], + [Expressions.zoom], + 7, + 1, + 9, + 0.5 + ], + )); + } + + static Future addIndoorBuilding(MapboxMapController controller) async { + final jsonStr = + await rootBundle.loadString("assets/fill-extrusion/indoor_3d_map.json"); + await controller.addGeoJsonSource( + "indoor-building-source", jsonDecode(jsonStr)); + await controller.addFillExtrusionLayer( + "indoor-building-source", + "indoor-building-layer", + FillExtrusionLayerProperties( + fillExtrusionOpacity: 0.5, + fillExtrusionHeight: [Expressions.get, "height"], + fillExtrusionBase: [Expressions.get, "base_height"], + fillExtrusionColor: [Expressions.get, "color"], + )); + } + static Future addVector(MapboxMapController controller) async { await controller.addSource( "terrain", @@ -191,6 +277,19 @@ class FullMapState extends State { addDetails: addGeojsonCluster, position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 5), ), + StyleInfo( + name: "Geojson heatmap", + baseStyle: MapboxStyles.DARK, + addDetails: addGeojsonHeatmap, + position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 5), + ), + StyleInfo( + name: "Indoor Building", + baseStyle: MapboxStyles.LIGHT, + addDetails: addIndoorBuilding, + position: CameraPosition( + target: LatLng(41.86625, -87.61694), zoom: 16, tilt: 20, bearing: 40), + ), StyleInfo( name: "Raster", baseStyle: MapboxStyles.EMPTY, diff --git a/example/lib/take_snapshot.dart b/example/lib/take_snapshot.dart new file mode 100644 index 000000000..49288a9b0 --- /dev/null +++ b/example/lib/take_snapshot.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; + +import 'main.dart'; +import 'page.dart'; + +class TakeSnapPage extends ExamplePage { + TakeSnapPage() : super(const Icon(Icons.camera_alt), 'Take snapshot'); + + @override + Widget build(BuildContext context) { + return const TakeSnapshot(); + } +} + +class TakeSnapshot extends StatefulWidget { + const TakeSnapshot(); + + @override + State createState() => FullMapState(); +} + +class FullMapState extends State { + FullMapState(); + + MapboxMapController? mapController; + final mapKey = GlobalKey(); + String? snapshotResult; + + void _onMapCreated(MapboxMapController controller) { + mapController = controller; + } + + void _onTakeSnapshot([bool writeToDisk = true]) async { + final renderBox = mapKey.currentContext?.findRenderObject() as RenderBox; + + final snapshotOptions = SnapshotOptions( + width: renderBox.size.width, + height: renderBox.size.height, + writeToDisk: writeToDisk, + withLogo: false, + ); + final result = await mapController?.takeSnapshot(snapshotOptions); + debugPrint("result: $result"); + _setResult(result); + } + + void _onTakeSnapshotWithBounds() async { + final renderBox = mapKey.currentContext?.findRenderObject() as RenderBox; + final bounds = await mapController?.getVisibleRegion(); + + final snapshotOptions = SnapshotOptions( + width: renderBox.size.width, + height: renderBox.size.height, + writeToDisk: true, + withLogo: false, + bounds: bounds, + ); + final uri = await mapController?.takeSnapshot(snapshotOptions); + + _setResult(uri); + } + + void _onTakeSnapshotWithCameraPosition() async { + final renderBox = mapKey.currentContext?.findRenderObject() as RenderBox; + + final snapshotOptions = SnapshotOptions( + width: renderBox.size.width, + height: renderBox.size.height, + writeToDisk: true, + withLogo: false, + centerCoordinate: LatLng(40.79796, -74.126410), + zoomLevel: 12, + pitch: 30, + heading: 20, + ); + final uri = await mapController?.takeSnapshot(snapshotOptions); + _setResult(uri); + } + + void _setResult(String? result) { + if (result != null) { + setState(() { + snapshotResult = result.replaceAll("file:", ""); + }); + } + } + + Uint8List convertBase64Image(String base64String) { + return Base64Decoder().convert(base64String.split(',').last); + } + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height; + return Column( + children: [ + Expanded( + child: MapboxMap( + key: mapKey, + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + myLocationEnabled: true, + styleString: MapboxStyles.SATELLITE, + ), + ), + const SizedBox( + height: 5, + ), + Container( + height: height * 0.4, + child: Column( + children: [ + Wrap( + spacing: 10, + alignment: WrapAlignment.center, + children: [ + ElevatedButton( + onPressed: _onTakeSnapshot, + child: Text("Take Snap"), + ), + ElevatedButton( + onPressed: _onTakeSnapshotWithBounds, + child: Text("With Bounds"), + ), + ElevatedButton( + onPressed: _onTakeSnapshotWithCameraPosition, + child: Text("With Camera Position"), + ), + ElevatedButton( + onPressed: () => _onTakeSnapshot(false), + child: Text("With Base64"), + ), + ], + ), + const SizedBox( + height: 10, + ), + if (snapshotResult != null) + Container( + decoration: BoxDecoration(border: Border.all()), + child: snapshotResult!.contains("base64") + ? Image.memory( + convertBase64Image(snapshotResult!), + gaplessPlayback: true, + height: height * 0.20, + ) + : Image.file( + File(snapshotResult!), + height: height * 0.20, + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef6437..8f3dd47c6 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -3,7 +3,7 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } + override func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + return true + } } diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift index 2722837ec..decbd0eb1 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -2,14 +2,14 @@ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = frame + contentViewController = flutterViewController + setFrame(windowFrame, display: true) - RegisterGeneratedPlugins(registry: flutterViewController) + RegisterGeneratedPlugins(registry: flutterViewController) - super.awakeFromNib() - } + super.awakeFromNib() + } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9731f59c0..bb0d73dcd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,11 +13,11 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 - location: ^4.0.0 + location: ^8.0.0 path_provider: ^2.0.0 http: ^0.13.0 collection: ^1.0.0 - device_info_plus: ^3.2.2 + device_info_plus: ^9.0.0 dependency_overrides: mapbox_gl_platform_interface: @@ -53,6 +53,7 @@ flutter: assets: - assets/fill/cat_silhouette_pattern.png + - assets/fill-extrusion/indoor_3d_map.json - assets/symbols/custom-icon.png - assets/symbols/2.0x/custom-icon.png - assets/symbols/3.0x/custom-icon.png diff --git a/example/web/index.html b/example/web/index.html index 4d19c8101..f6098b72b 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -15,7 +15,7 @@ - + example diff --git a/install_formatting_tools.sh b/install_formatting_tools.sh new file mode 100755 index 000000000..e9aad1960 --- /dev/null +++ b/install_formatting_tools.sh @@ -0,0 +1,21 @@ +echo "Cleanup" +rm swiftformat +rm -r __MACOSX +rm google-java-format-1.13.0-all-deps.jar + +if [ $(uname) = "Linux" ] +then + echo "Install for Linux" + wget https://github.com/nicklockwood/SwiftFormat/releases/download/0.48.18/swiftformat_linux.zip + unzip swiftformat_linux.zip + rm swiftformat_linux.zip + mv swiftformat_linux swiftformat +else + echo "Install for MAC" + wget https://github.com/nicklockwood/SwiftFormat/releases/download/0.48.18/swiftformat.zip + unzip swiftformat.zip + rm swiftformat.zip +fi +chmod +x swiftformat + +wget https://github.com/google/google-java-format/releases/download/v1.13.0/google-java-format-1.13.0-all-deps.jar diff --git a/ios/Classes/LayerPropertyConverter.swift b/ios/Classes/LayerPropertyConverter.swift index e36fbba54..b711adcb9 100644 --- a/ios/Classes/LayerPropertyConverter.swift +++ b/ios/Classes/LayerPropertyConverter.swift @@ -251,6 +251,41 @@ class LayerPropertyConverter { } } + class func addFillExtrusionProperties( + fillExtrusionLayer: MGLFillExtrusionStyleLayer, + properties: [String: String] + ) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "fill-extrusion-opacity": + fillExtrusionLayer.fillExtrusionOpacity = expression + case "fill-extrusion-color": + fillExtrusionLayer.fillExtrusionColor = expression + case "fill-extrusion-translate": + fillExtrusionLayer.fillExtrusionTranslation = expression + case "fill-extrusion-translate-anchor": + fillExtrusionLayer.fillExtrusionTranslationAnchor = expression + case "fill-extrusion-pattern": + fillExtrusionLayer.fillExtrusionPattern = expression + case "fill-extrusion-height": + fillExtrusionLayer.fillExtrusionHeight = expression + case "fill-extrusion-base": + fillExtrusionLayer.fillExtrusionBase = expression + case "fill-extrusion-vertical-gradient": + fillExtrusionLayer.fillExtrusionHasVerticalGradient = expression + case "visibility": + fillExtrusionLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + class func addRasterProperties(rasterLayer: MGLRasterStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { let expression = interpretExpression( @@ -314,6 +349,35 @@ class LayerPropertyConverter { } } + class func addHeatmapProperties( + heatmapLayer: MGLHeatmapStyleLayer, + properties: [String: String] + ) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "heatmap-radius": + heatmapLayer.heatmapRadius = expression + case "heatmap-weight": + heatmapLayer.heatmapWeight = expression + case "heatmap-intensity": + heatmapLayer.heatmapIntensity = expression + case "heatmap-color": + heatmapLayer.heatmapColor = expression + case "heatmap-opacity": + heatmapLayer.heatmapOpacity = expression + case "visibility": + heatmapLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + private class func interpretExpression(propertyName: String, expression: String) -> NSExpression? { diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index e1d55b8ab..4af7ff3b4 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -227,12 +227,13 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma ) } if let top = arguments["top"] as? Double, - let bottom = arguments["bottom"] as? Double, let left = arguments["left"] as? Double, - let right = arguments["right"] as? Double + let width = arguments["width"] as? Double, + let height = arguments["height"] as? Double { + dump(arguments) features = mapView.visibleFeatures( - in: CGRect(x: left, y: top, width: right, height: bottom), + in: CGRect(x: left, y: top, width: width, height: height), styleLayerIdentifiers: styleLayerIdentifiers, predicate: filterExpression ) @@ -336,8 +337,9 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma animationTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName .easeInEaseOut)) result(nil) + } else { + mapView.setCamera(camera, animated: true) } - mapView.setCamera(camera, animated: true) } result(nil) @@ -353,6 +355,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let maxzoom = arguments["maxzoom"] as? Double let filter = arguments["filter"] as? String + removeLayer(layerId: layerId) let addResult = addSymbolLayer( sourceId: sourceId, layerId: layerId, @@ -381,6 +384,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let maxzoom = arguments["maxzoom"] as? Double let filter = arguments["filter"] as? String + removeLayer(layerId: layerId) let addResult = addLineLayer( sourceId: sourceId, layerId: layerId, @@ -409,6 +413,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let maxzoom = arguments["maxzoom"] as? Double let filter = arguments["filter"] as? String + removeLayer(layerId: layerId) let addResult = addFillLayer( sourceId: sourceId, layerId: layerId, @@ -425,6 +430,35 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case let .failure(error): result(error.flutterError) } + case "fillExtrusionLayer#add": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + guard let layerId = arguments["layerId"] as? String else { return } + guard let properties = arguments["properties"] as? [String: String] else { return } + guard let enableInteraction = arguments["enableInteraction"] as? Bool else { return } + let belowLayerId = arguments["belowLayerId"] as? String + let sourceLayer = arguments["sourceLayer"] as? String + let minzoom = arguments["minzoom"] as? Double + let maxzoom = arguments["maxzoom"] as? Double + let filter = arguments["filter"] as? String + + removeLayer(layerId: layerId) + let addResult = addFillExtrusionLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + sourceLayerIdentifier: sourceLayer, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + properties: properties + ) + switch addResult { + case .success: result(nil) + case let .failure(error): result(error.flutterError) + } + case "circleLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -437,6 +471,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let maxzoom = arguments["maxzoom"] as? Double let filter = arguments["filter"] as? String + removeLayer(layerId: layerId) let addResult = addCircleLayer( sourceId: sourceId, layerId: layerId, @@ -461,6 +496,8 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let belowLayerId = arguments["belowLayerId"] as? String let minzoom = arguments["minzoom"] as? Double let maxzoom = arguments["maxzoom"] as? Double + + removeLayer(layerId: layerId) addHillshadeLayer( sourceId: sourceId, layerId: layerId, @@ -471,6 +508,26 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma ) result(nil) + case "heatmapLayer#add": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + guard let layerId = arguments["layerId"] as? String else { return } + guard let properties = arguments["properties"] as? [String: String] else { return } + let belowLayerId = arguments["belowLayerId"] as? String + let minzoom = arguments["minzoom"] as? Double + let maxzoom = arguments["maxzoom"] as? Double + + removeLayer(layerId: layerId) + addHeatmapLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + properties: properties + ) + result(nil) + case "rasterLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -549,6 +606,41 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma mapView.style?.addSource(source) result(nil) + case "style#updateImageSource": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let imageSourceId = arguments["imageSourceId"] as? String else { return } + guard let imageSource = mapView.style? + .source(withIdentifier: imageSourceId) as? MGLImageSource else { return } + let bytes = arguments["bytes"] as? FlutterStandardTypedData + if bytes != nil { + guard let data = bytes!.data as? Data else { return } + guard let image = UIImage(data: data) else { return } + imageSource.image = image + } + let coordinates = arguments["coordinates"] as? [[Double]] + if coordinates != nil { + let quad = MGLCoordinateQuad( + topLeft: CLLocationCoordinate2D( + latitude: coordinates![0][0], + longitude: coordinates![0][1] + ), + bottomLeft: CLLocationCoordinate2D( + latitude: coordinates![3][0], + longitude: coordinates![3][1] + ), + bottomRight: CLLocationCoordinate2D( + latitude: coordinates![2][0], + longitude: coordinates![2][1] + ), + topRight: CLLocationCoordinate2D( + latitude: coordinates![1][0], + longitude: coordinates![1][1] + ) + ) + imageSource.coordinates = quad + } + result(nil) + case "style#removeSource": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -648,12 +740,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case "style#removeLayer": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let layerId = arguments["layerId"] as? String else { return } - guard let layer = mapView.style?.layer(withIdentifier: layerId) else { - result(nil) - return - } - interactiveFeatureLayerIds.remove(layerId) - mapView.style?.removeLayer(layer) + removeLayer(layerId: layerId) result(nil) case "style#setFilter": @@ -669,6 +756,17 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case let .failure(error): result(error.flutterError) } + case "map#setVisibility": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let layerId = arguments["layerId"] as? String else { return } + guard let isVisible = arguments["isVisible"] as? Bool else { return } + guard let layer = mapView.style?.layer(withIdentifier: layerId) else { + result(nil) + return + } + layer.isVisible = isVisible + result(nil) + case "source#addGeoJson": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -696,11 +794,132 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma guard let geojson = arguments["geojsonFeature"] as? String else { return } setFeature(sourceId: sourceId, geojsonFeature: geojson) result(nil) + case "snapshot#takeSnapshot": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + let camera = MGLMapCamera() + + guard let pitch = arguments["pitch"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", message: "pitch is not a number", + details: nil)) + return + } + camera.pitch = pitch.doubleValue + + guard let heading = arguments["heading"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", message: "heading is not a number", + details: nil)) + return + } + camera.heading = heading.doubleValue + + camera.centerCoordinate = mapView.centerCoordinate + if arguments["centerCoordinate"] != nil { + guard let centerCoordinate = arguments["centerCoordinate"] as? [NSNumber] else { + result(FlutterError( + code: "invalidArgument", + message: "centerCoordinate is not a number list", + details: nil + )) + return + } + camera.centerCoordinate = CLLocationCoordinate2D( + latitude: centerCoordinate[0].doubleValue, + longitude: centerCoordinate[1].doubleValue + ) + } + + guard let width = arguments["width"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", message: "width is not a number", + details: nil)) + return + } + guard let height = arguments["height"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", message: "height is not a number", + details: nil)) + return + } + + let size = CGSize(width: width.doubleValue, height: height.doubleValue) + + var styleURL: URL = mapView.styleURL + if arguments["styleUri"] != nil { + guard let styleUri = arguments["styleUri"] as? String else { + result( + FlutterError(code: "invalidArgument", message: "styleUri is not a string", + details: nil) + ) + return + } + styleURL = URL(string: styleUri)! + } + + let snapshotOptions: MGLMapSnapshotOptions = .init( + styleURL: styleURL, + camera: camera, + size: size + ) + + snapshotOptions.zoomLevel = mapView.zoomLevel + if arguments["zoomLevel"] != nil { + guard let zoomLevel = arguments["zoomLevel"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", + message: "zoomLevel is not a number", details: nil)) + return + } + snapshotOptions.zoomLevel = zoomLevel.doubleValue + } + + if arguments["bounds"] != nil { + guard let bounds = arguments["bounds"] as? [[NSNumber]] else { + result(FlutterError(code: "invalidArgument", + message: "bounds is not a number list", details: nil)) + return + } + let sw = bounds[0] + let ne = bounds[1] + snapshotOptions.coordinateBounds = MGLCoordinateBounds( + sw: CLLocationCoordinate2D(latitude: sw[0].doubleValue, + longitude: sw[1].doubleValue), + ne: CLLocationCoordinate2D( + latitude: ne[0].doubleValue, + longitude: ne[1].doubleValue + ) + ) + } + + let snapshotter: MGLMapSnapshotter? = MGLMapSnapshotter(options: snapshotOptions) + + snapshotter?.start { snapshot, error in + if error != nil { + result(FlutterError( + code: "canCreateSnapshot", + message: error?.localizedDescription, + details: error.debugDescription + )) + } else if let image = snapshot?.image { + guard let writeToDisk = arguments["writeToDisk"] as? NSNumber else { + result(FlutterError(code: "invalidArgument", + message: "writeToDisk is not a boolean", details: nil)) + return + } + + let value = writeToDisk.boolValue ? RNMBImageUtils + .createTempFile(image) : RNMBImageUtils.createBase64(image) + result(value.absoluteString) + } + } default: result(FlutterMethodNotImplemented) } } + private func removeLayer(layerId: String) { + if let layer = mapView.style?.layer(withIdentifier: layerId) { + mapView.style?.removeLayer(layer) + interactiveFeatureLayerIds.remove(layerId) + } + } + private func loadIconImage(name: String) -> UIImage? { // Build up the full path of the asset. // First find the last '/' ans split the image name in the asset directory and the image file name. @@ -1102,6 +1321,51 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma return .success(()) } + func addFillExtrusionLayer( + sourceId: String, + layerId: String, + belowLayerId: String?, + sourceLayerIdentifier: String?, + minimumZoomLevel: Double?, + maximumZoomLevel: Double?, + filter: String?, + enableInteraction: Bool, + properties: [String: String] + ) -> Result { + if let style = mapView.style { + if let source = style.source(withIdentifier: sourceId) { + let layer = MGLFillExtrusionStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addFillExtrusionProperties( + fillExtrusionLayer: layer, + properties: properties + ) + if let sourceLayerIdentifier = sourceLayerIdentifier { + layer.sourceLayerIdentifier = sourceLayerIdentifier + } + if let minimumZoomLevel = minimumZoomLevel { + layer.minimumZoomLevel = Float(minimumZoomLevel) + } + if let maximumZoomLevel = maximumZoomLevel { + layer.maximumZoomLevel = Float(maximumZoomLevel) + } + if let filter = filter { + if case let .failure(error) = setFilter(layer, filter) { + return .failure(error) + } + } + if let id = belowLayerId, let belowLayer = style.layer(withIdentifier: id) { + style.insertLayer(layer, below: belowLayer) + } else { + style.addLayer(layer) + } + if enableInteraction { + interactiveFeatureLayerIds.insert(layerId) + } + } + } + return .success(()) + } + func addCircleLayer( sourceId: String, layerId: String, @@ -1200,6 +1464,36 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } } + func addHeatmapLayer( + sourceId: String, + layerId: String, + belowLayerId: String?, + minimumZoomLevel: Double?, + maximumZoomLevel: Double?, + properties: [String: String] + ) { + if let style = mapView.style { + if let source = style.source(withIdentifier: sourceId) { + let layer = MGLHeatmapStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addHeatmapProperties( + heatmapLayer: layer, + properties: properties + ) + if let minimumZoomLevel = minimumZoomLevel { + layer.minimumZoomLevel = Float(minimumZoomLevel) + } + if let maximumZoomLevel = maximumZoomLevel { + layer.maximumZoomLevel = Float(maximumZoomLevel) + } + if let id = belowLayerId, let belowLayer = style.layer(withIdentifier: id) { + style.insertLayer(layer, below: belowLayer) + } else { + style.addLayer(layer) + } + } + } + } + func addRasterLayer( sourceId: String, layerId: String, diff --git a/ios/Classes/RNMBImageUtils.swift b/ios/Classes/RNMBImageUtils.swift new file mode 100644 index 000000000..1ecd74176 --- /dev/null +++ b/ios/Classes/RNMBImageUtils.swift @@ -0,0 +1,26 @@ +// +// RNMBImageUtils.swift +// mapbox_gl +// +// Created by mac on 30/05/2022. +// + +enum RNMBImageUtils { + static func createTempFile(_ image: UIImage) -> URL { + let fileID = UUID().uuidString + let pathComponent = "Documents/rctmgl-snapshot-\(fileID).jpeg" + + let filePath = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(pathComponent) + + let data = image.jpegData(compressionQuality: 1.0) + try! data?.write(to: filePath, options: [.atomic]) + return filePath + } + + static func createBase64(_ image: UIImage) -> URL { + let data = image.jpegData(compressionQuality: 1.0) + let b64string: String = data!.base64EncodedString(options: [.endLineWithCarriageReturn]) + let result = "data:image/jpeg;base64,\(b64string)" + return URL(string: result)! + } +} diff --git a/lib/mapbox_gl.dart b/lib/mapbox_gl.dart index 0c836a3e5..ea45970d3 100644 --- a/lib/mapbox_gl.dart +++ b/lib/mapbox_gl.dart @@ -41,6 +41,7 @@ export 'package:mapbox_gl_platform_interface/mapbox_gl_platform_interface.dart' LineOptions, Fill, FillOptions, + SnapshotOptions, SourceProperties, RasterSourceProperties, VectorSourceProperties, diff --git a/lib/src/annotation_manager.dart b/lib/src/annotation_manager.dart index 2aa47c0c8..65bb22426 100644 --- a/lib/src/annotation_manager.dart +++ b/lib/src/annotation_manager.dart @@ -9,7 +9,11 @@ abstract class AnnotationManager { final void Function(T)? onTap; /// base id of the manager. User [layerdIds] to get the actual ids. - final String id; + String get id => "${managerType}_$randomPostFix"; + + final String managerType; + + final String randomPostFix; List get layerIds => [for (int i = 0; i < allLayerProperties.length; i++) _makeLayerId(i)]; @@ -29,9 +33,13 @@ abstract class AnnotationManager { Set get annotations => _idToAnnotation.values.toSet(); - AnnotationManager(this.controller, - {this.onTap, this.selectLayer, required this.enableInteraction}) - : id = getRandomString() { + AnnotationManager( + this.controller, { + required this.managerType, + this.onTap, + this.selectLayer, + required this.enableInteraction, + }) : randomPostFix = getRandomString() { for (var i = 0; i < allLayerProperties.length; i++) { final layerId = _makeLayerId(i); controller.addGeoJsonSource(layerId, buildFeatureCollection([]), @@ -50,7 +58,6 @@ abstract class AnnotationManager { Future _rebuildLayers() async { for (var i = 0; i < allLayerProperties.length; i++) { final layerId = _makeLayerId(i); - await controller.removeLayer(layerId); await controller.addLayer(layerId, layerId, allLayerProperties[i]); } } @@ -172,6 +179,7 @@ class LineManager extends AnnotationManager { {void Function(Line)? onTap, bool enableInteraction = true}) : super( controller, + managerType: "line", onTap: onTap, enableInteraction: enableInteraction, selectLayer: (Line line) => line.options.linePattern == null ? 0 : 1, @@ -201,6 +209,7 @@ class FillManager extends AnnotationManager { bool enableInteraction = true, }) : super( controller, + managerType: "fill", onTap: onTap, enableInteraction: enableInteraction, selectLayer: (Fill fill) => fill.options.fillPattern == null ? 0 : 1, @@ -228,6 +237,7 @@ class CircleManager extends AnnotationManager { bool enableInteraction = true, }) : super( controller, + managerType: "circle", enableInteraction: enableInteraction, onTap: onTap, ); @@ -260,6 +270,7 @@ class SymbolManager extends AnnotationManager { _textIgnorePlacement = textIgnorePlacement, super( controller, + managerType: "symbol", enableInteraction: enableInteraction, onTap: onTap, ); diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 1d4583f13..e70e06a27 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -173,6 +173,7 @@ class MapboxMapController extends ChangeNotifier { onUserLocationUpdated?.call(location); }); } + bool _disposed = false; FillManager? fillManager; LineManager? lineManager; @@ -253,18 +254,48 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes after listeners have been notified. Future _updateMapOptions(Map optionsUpdate) async { + _disposeGuard(); _cameraPosition = await _mapboxGlPlatform.updateMapOptions(optionsUpdate); notifyListeners(); } + /// Triggers a resize event for the map on web (ignored on Android or iOS). + /// + /// Checks first if a resize is required or if it looks like it is already correctly resized. + /// If it looks good, the resize call will be skipped. + /// + /// To force resize map (without any checks) have a look at forceResizeWebMap() + void resizeWebMap() { + _disposeGuard(); + _mapboxGlPlatform.resizeWebMap(); + } + + /// Triggers a hard map resize event on web and does not check if it is required or not. + void forceResizeWebMap() { + _disposeGuard(); + _mapboxGlPlatform.forceResizeWebMap(); + } + + void _disposeGuard() { + if (_disposed) { + throw StateError( + 'This MapboxMapController has already been disposed. This happens if flutter disposes a MapboxMap and you try to use its Controller afterwards.', + ); + } + } + /// Starts an animated change of the map camera position. /// + /// [duration] is the amount of time, that the transition animation should take. + /// /// The returned [Future] completes after the change has been started on the /// platform side. /// It returns true if the camera was successfully moved and false if the movement was canceled. /// Note: this currently always returns immediately with a value of null on iOS - Future animateCamera(CameraUpdate cameraUpdate) async { - return _mapboxGlPlatform.animateCamera(cameraUpdate); + Future animateCamera(CameraUpdate cameraUpdate, + {Duration? duration}) async { + _disposeGuard(); + return _mapboxGlPlatform.animateCamera(cameraUpdate, duration: duration); } /// Instantaneously re-position the camera. @@ -275,6 +306,7 @@ class MapboxMapController extends ChangeNotifier { /// It returns true if the camera was successfully moved and false if the movement was canceled. /// Note: this currently always returns immediately with a value of null on iOS Future moveCamera(CameraUpdate cameraUpdate) async { + _disposeGuard(); return _mapboxGlPlatform.moveCamera(cameraUpdate); } @@ -292,6 +324,7 @@ class MapboxMapController extends ChangeNotifier { /// Future addGeoJsonSource(String sourceId, Map geojson, {String? promoteId}) async { + _disposeGuard(); await _mapboxGlPlatform.addGeoJsonSource(sourceId, geojson, promoteId: promoteId); } @@ -309,6 +342,7 @@ class MapboxMapController extends ChangeNotifier { /// platform side. Future setGeoJsonSource( String sourceId, Map geojson) async { + _disposeGuard(); await _mapboxGlPlatform.setGeoJsonSource(sourceId, geojson); } @@ -325,6 +359,7 @@ class MapboxMapController extends ChangeNotifier { /// platform side. Future setGeoJsonFeature( String sourceId, Map geojsonFeature) async { + _disposeGuard(); await _mapboxGlPlatform.setFeatureForGeoJsonSource( sourceId, geojsonFeature); } @@ -340,6 +375,10 @@ class MapboxMapController extends ChangeNotifier { /// If [enableInteraction] is set the layer is considered for touch or drag /// events. [sourceLayer] is used to selected a specific source layer from /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. /// [filter] determines which features should be rendered in the layer. /// Filters are written as [expressions]. /// @@ -352,6 +391,7 @@ class MapboxMapController extends ChangeNotifier { double? maxzoom, dynamic filter, bool enableInteraction = true}) async { + _disposeGuard(); await _mapboxGlPlatform.addSymbolLayer( sourceId, layerId, @@ -376,6 +416,10 @@ class MapboxMapController extends ChangeNotifier { /// If [enableInteraction] is set the layer is considered for touch or drag /// events. [sourceLayer] is used to selected a specific source layer from /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. /// [filter] determines which features should be rendered in the layer. /// Filters are written as [expressions]. /// @@ -388,6 +432,7 @@ class MapboxMapController extends ChangeNotifier { double? maxzoom, dynamic filter, bool enableInteraction = true}) async { + _disposeGuard(); await _mapboxGlPlatform.addLineLayer( sourceId, layerId, @@ -412,6 +457,10 @@ class MapboxMapController extends ChangeNotifier { /// If [enableInteraction] is set the layer is considered for touch or drag /// events. [sourceLayer] is used to selected a specific source layer from /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. /// [filter] determines which features should be rendered in the layer. /// Filters are written as [expressions]. /// @@ -424,6 +473,7 @@ class MapboxMapController extends ChangeNotifier { double? maxzoom, dynamic filter, bool enableInteraction = true}) async { + _disposeGuard(); await _mapboxGlPlatform.addFillLayer( sourceId, layerId, @@ -437,6 +487,47 @@ class MapboxMapController extends ChangeNotifier { ); } + /// Add a fill extrusion layer to the map with the given properties + /// + /// Consider using [addLayer] for an unified layer api. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + /// + /// Setting [belowLayerId] adds the new layer below the given id. + /// If [enableInteraction] is set the layer is considered for touch or drag + /// events. [sourceLayer] is used to selected a specific source layer from + /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. + /// [filter] determines which features should be rendered in the layer. + /// Filters are written as [expressions]. + /// + /// [expressions]: https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions + Future addFillExtrusionLayer( + String sourceId, String layerId, FillExtrusionLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + _disposeGuard(); + await _mapboxGlPlatform.addFillExtrusionLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + /// Add a circle layer to the map with the given properties /// /// Consider using [addLayer] for an unified layer api. @@ -448,6 +539,10 @@ class MapboxMapController extends ChangeNotifier { /// If [enableInteraction] is set the layer is considered for touch or drag /// events. [sourceLayer] is used to selected a specific source layer from /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. /// [filter] determines which features should be rendered in the layer. /// Filters are written as [expressions]. /// @@ -460,6 +555,7 @@ class MapboxMapController extends ChangeNotifier { double? maxzoom, dynamic filter, bool enableInteraction = true}) async { + _disposeGuard(); await _mapboxGlPlatform.addCircleLayer( sourceId, layerId, @@ -482,13 +578,18 @@ class MapboxMapController extends ChangeNotifier { /// /// Setting [belowLayerId] adds the new layer below the given id. /// [sourceLayer] is used to selected a specific source layer from - /// Raster source + /// Raster source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. Future addRasterLayer( String sourceId, String layerId, RasterLayerProperties properties, {String? belowLayerId, String? sourceLayer, double? minzoom, double? maxzoom}) async { + _disposeGuard(); await _mapboxGlPlatform.addRasterLayer( sourceId, layerId, @@ -509,13 +610,18 @@ class MapboxMapController extends ChangeNotifier { /// /// Setting [belowLayerId] adds the new layer below the given id. /// [sourceLayer] is used to selected a specific source layer from - /// Raster source + /// Raster source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. Future addHillshadeLayer( String sourceId, String layerId, HillshadeLayerProperties properties, {String? belowLayerId, String? sourceLayer, double? minzoom, double? maxzoom}) async { + _disposeGuard(); await _mapboxGlPlatform.addHillshadeLayer( sourceId, layerId, @@ -527,12 +633,45 @@ class MapboxMapController extends ChangeNotifier { ); } + /// Add a heatmap layer to the map with the given properties + /// + /// Consider using [addLayer] for an unified layer api. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + /// + /// Setting [belowLayerId] adds the new layer below the given id. + /// [sourceLayer] is used to selected a specific source layer from + /// Raster source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. + Future addHeatmapLayer( + String sourceId, String layerId, HeatmapLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + _disposeGuard(); + await _mapboxGlPlatform.addHeatmapLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + ); + } + /// Updates user location tracking mode. /// /// The returned [Future] completes after the change has been made on the /// platform side. Future updateMyLocationTrackingMode( MyLocationTrackingMode myLocationTrackingMode) async { + _disposeGuard(); return _mapboxGlPlatform .updateMyLocationTrackingMode(myLocationTrackingMode); } @@ -542,6 +681,7 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the change has been made on the /// platform side. Future matchMapLanguageWithDeviceDefault() async { + _disposeGuard(); return _mapboxGlPlatform.matchMapLanguageWithDeviceDefault(); } @@ -558,6 +698,7 @@ class MapboxMapController extends ChangeNotifier { /// platform side. Future updateContentInsets(EdgeInsets insets, [bool animated = false]) async { + _disposeGuard(); return _mapboxGlPlatform.updateContentInsets(insets, animated); } @@ -568,6 +709,7 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the change has been made on the /// platform side. Future setMapLanguage(String language) async { + _disposeGuard(); return _mapboxGlPlatform.setMapLanguage(language); } @@ -576,6 +718,7 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the change has been made on the /// platform side. Future setTelemetryEnabled(bool enabled) async { + _disposeGuard(); return _mapboxGlPlatform.setTelemetryEnabled(enabled); } @@ -584,6 +727,7 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the query has been made on the /// platform side. Future getTelemetryEnabled() async { + _disposeGuard(); return _mapboxGlPlatform.getTelemetryEnabled(); } @@ -954,17 +1098,20 @@ class MapboxMapController extends ChangeNotifier { /// Query rendered features at a point in screen cooridnates Future queryRenderedFeatures( Point point, List layerIds, List? filter) async { + _disposeGuard(); return _mapboxGlPlatform.queryRenderedFeatures(point, layerIds, filter); } /// Query rendered features in a Rect in screen coordinates Future queryRenderedFeaturesInRect( Rect rect, List layerIds, String? filter) async { + _disposeGuard(); return _mapboxGlPlatform.queryRenderedFeaturesInRect( rect, layerIds, filter); } Future invalidateAmbientCache() async { + _disposeGuard(); return _mapboxGlPlatform.invalidateAmbientCache(); } @@ -972,11 +1119,13 @@ class MapboxMapController extends ChangeNotifier { /// /// Return last latlng, nullable Future requestMyLocationLatLng() async { + _disposeGuard(); return _mapboxGlPlatform.requestMyLocationLatLng(); } /// This method returns the boundaries of the region currently displayed in the map. Future getVisibleRegion() async { + _disposeGuard(); return _mapboxGlPlatform.getVisibleRegion(); } @@ -1016,6 +1165,7 @@ class MapboxMapController extends ChangeNotifier { /// } /// ``` Future addImage(String name, Uint8List bytes, [bool sdf = false]) { + _disposeGuard(); return _mapboxGlPlatform.addImage(name, bytes, sdf); } @@ -1042,23 +1192,35 @@ class MapboxMapController extends ChangeNotifier { /// Adds an image source to the style currently displayed in the map, so that it can later be referred to by the provided id. Future addImageSource( String imageSourceId, Uint8List bytes, LatLngQuad coordinates) { + _disposeGuard(); return _mapboxGlPlatform.addImageSource(imageSourceId, bytes, coordinates); } + /// Update an image source to the style currently displayed in the map, so that it can later be referred to by the provided id. + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates) { + _disposeGuard(); + return _mapboxGlPlatform.updateImageSource( + imageSourceId, bytes, coordinates); + } + /// Removes previously added image source by id @Deprecated("This method was renamed to removeSource") Future removeImageSource(String imageSourceId) { + _disposeGuard(); return _mapboxGlPlatform.removeSource(imageSourceId); } /// Removes previously added source by id Future removeSource(String sourceId) { + _disposeGuard(); return _mapboxGlPlatform.removeSource(sourceId); } /// Adds a Mapbox image layer to the map's style at render time. Future addImageLayer(String layerId, String imageSourceId, {double? minzoom, double? maxzoom}) { + _disposeGuard(); return _mapboxGlPlatform.addLayer(layerId, imageSourceId, minzoom, maxzoom); } @@ -1066,6 +1228,7 @@ class MapboxMapController extends ChangeNotifier { Future addImageLayerBelow( String layerId, String sourceId, String imageSourceId, {double? minzoom, double? maxzoom}) { + _disposeGuard(); return _mapboxGlPlatform.addLayerBelow( layerId, sourceId, imageSourceId, minzoom, maxzoom); } @@ -1075,19 +1238,29 @@ class MapboxMapController extends ChangeNotifier { Future addLayerBelow( String layerId, String sourceId, String imageSourceId, {double? minzoom, double? maxzoom}) { + _disposeGuard(); return _mapboxGlPlatform.addLayerBelow( layerId, sourceId, imageSourceId, minzoom, maxzoom); } /// Removes a Mapbox style layer Future removeLayer(String layerId) { + _disposeGuard(); return _mapboxGlPlatform.removeLayer(layerId); } Future setFilter(String layerId, dynamic filter) { + _disposeGuard(); return _mapboxGlPlatform.setFilter(layerId, filter); } + /// Sets the visibility by specifying [isVisible] of the layer with + /// the specified id [layerId]. + /// Returns silently if [layerId] does not exist. + Future setVisibility(String layerId, bool isVisible) { + return _mapboxGlPlatform.setVisibility(layerId, isVisible); + } + /// Returns the point on the screen that corresponds to a geographical coordinate ([latLng]). The screen location is in screen pixels (not display pixels) relative to the top left of the map (not of the whole screen) /// /// Note: The resulting x and y coordinates are rounded to [int] on web, on other platforms they may differ very slightly (in the range of about 10^-10) from the actual nearest screen coordinate. @@ -1095,33 +1268,38 @@ class MapboxMapController extends ChangeNotifier { /// /// Returns null if [latLng] is not currently visible on the map. Future toScreenLocation(LatLng latLng) async { + _disposeGuard(); return _mapboxGlPlatform.toScreenLocation(latLng); } Future> toScreenLocationBatch(Iterable latLngs) async { + _disposeGuard(); return _mapboxGlPlatform.toScreenLocationBatch(latLngs); } /// Returns the geographic location (as [LatLng]) that corresponds to a point on the screen. The screen location is specified in screen pixels (not display pixels) relative to the top left of the map (not the top left of the whole screen). Future toLatLng(Point screenLocation) async { + _disposeGuard(); return _mapboxGlPlatform.toLatLng(screenLocation); } /// Returns the distance spanned by one pixel at the specified [latitude] and current zoom level. /// The distance between pixels decreases as the latitude approaches the poles. This relationship parallels the relationship between longitudinal coordinates at different latitudes. Future getMetersPerPixelAtLatitude(double latitude) async { + _disposeGuard(); return _mapboxGlPlatform.getMetersPerPixelAtLatitude(latitude); } /// Add a new source to the map Future addSource(String sourceid, SourceProperties properties) async { + _disposeGuard(); return _mapboxGlPlatform.addSource(sourceid, properties); } /// Add a layer to the map with the given properties /// /// The returned [Future] completes after the change has been made on the - /// platform side. + /// platform side. If the layer already exists, the layer is updated. /// /// Setting [belowLayerId] adds the new layer below the given id. /// If [enableInteraction] is set the layer is considered for touch or drag @@ -1129,6 +1307,10 @@ class MapboxMapController extends ChangeNotifier { /// [HillshadeLayerProperties]. /// [sourceLayer] is used to selected a specific source layer from Vector /// source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. /// [filter] determines which features should be rendered in the layer. /// Filters are written as [expressions]. /// [filter] is not supported by RasterLayer and HillshadeLayer. @@ -1150,6 +1332,13 @@ class MapboxMapController extends ChangeNotifier { minzoom: minzoom, maxzoom: maxzoom, filter: filter); + } else if (properties is FillExtrusionLayerProperties) { + addFillExtrusionLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + enableInteraction: enableInteraction, + minzoom: minzoom, + maxzoom: maxzoom); } else if (properties is LineLayerProperties) { addLineLayer(sourceId, layerId, properties, belowLayerId: belowLayerId, @@ -1192,13 +1381,31 @@ class MapboxMapController extends ChangeNotifier { sourceLayer: sourceLayer, minzoom: minzoom, maxzoom: maxzoom); + } else if (properties is HeatmapLayerProperties) { + addHeatmapLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom); } else { throw UnimplementedError("Unknown layer type $properties"); } } + /// Generates static raster images of the map. Each snapshot image depicts a portion of a map defined by an [SnapshotOptions] object you provide + /// Android/iOS: Return snapshot uri in app specific cache storage or base64 string + /// Web: Return base64 string with current camera posision of [MapboxMap] + /// + /// Default will return snapshot uri in Android and iOS + /// If you want base64 value, you must set writeToDisk option to False + Future takeSnapshot(SnapshotOptions snapshotOptions) async { + _disposeGuard(); + return _mapboxGlPlatform.takeSnapshot(snapshotOptions); + } + @override void dispose() { + _disposed = true; super.dispose(); _mapboxGlPlatform.dispose(); } diff --git a/lib/src/layer_properties.dart b/lib/src/layer_properties.dart index 4ada94868..8624c23d9 100644 --- a/lib/src/layer_properties.dart +++ b/lib/src/layer_properties.dart @@ -1751,6 +1751,188 @@ class FillLayerProperties implements LayerProperties { } } +class FillExtrusionLayerProperties implements LayerProperties { + // Paint Properties + /// The opacity of the entire fill extrusion layer. This is rendered on a + /// per-layer, not per-feature, basis, and data-driven styling is not + /// available. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionOpacity; + + /// The base color of the extruded fill. The extrusion's surfaces will be + /// shaded differently based on this color in combination with the root + /// `light` settings. If this color is specified as `rgba` with an alpha + /// component, the alpha component will be ignored; use + /// `fill-extrusion-opacity` to set layer opacity. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionColor; + + /// The geometry's offset. Values are [x, y] where negatives indicate left + /// and up (on the flat plane), respectively. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionTranslate; + + /// Controls the frame of reference for `fill-extrusion-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The fill extrusion is translated relative to the map. + /// "viewport" + /// The fill extrusion is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionTranslateAnchor; + + /// Name of image in sprite to use for drawing images on extruded fills. + /// For seamless patterns, image width and height must be a factor of two + /// (2, 4, 8, ..., 512). Note that zoom-dependent expressions will be + /// evaluated only at integer zoom levels. + /// + /// Type: resolvedImage + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, macos, ios + final dynamic fillExtrusionPattern; + + /// The height with which to extrude this layer. + /// + /// Type: number + /// default: 0 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionHeight; + + /// The height with which to extrude the base of this layer. Must be less + /// than or equal to `fill-extrusion-height`. + /// + /// Type: number + /// default: 0 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionBase; + + /// Whether to apply a vertical gradient to the sides of a fill-extrusion + /// layer. If true, sides will be shaded slightly darker farther down. + /// + /// Type: boolean + /// default: true + /// + /// Sdk Support: + /// basic functionality with js, ios, macos + final dynamic fillExtrusionVerticalGradient; + + // Layout Properties + /// Whether this layer is displayed. + /// + /// Type: enum + /// default: visible + /// Options: + /// "visible" + /// The layer is shown. + /// "none" + /// The layer is not shown. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic visibility; + + const FillExtrusionLayerProperties({ + this.fillExtrusionOpacity, + this.fillExtrusionColor, + this.fillExtrusionTranslate, + this.fillExtrusionTranslateAnchor, + this.fillExtrusionPattern, + this.fillExtrusionHeight, + this.fillExtrusionBase, + this.fillExtrusionVerticalGradient, + this.visibility, + }); + + FillExtrusionLayerProperties copyWith(FillExtrusionLayerProperties changes) { + return FillExtrusionLayerProperties( + fillExtrusionOpacity: + changes.fillExtrusionOpacity ?? fillExtrusionOpacity, + fillExtrusionColor: changes.fillExtrusionColor ?? fillExtrusionColor, + fillExtrusionTranslate: + changes.fillExtrusionTranslate ?? fillExtrusionTranslate, + fillExtrusionTranslateAnchor: + changes.fillExtrusionTranslateAnchor ?? fillExtrusionTranslateAnchor, + fillExtrusionPattern: + changes.fillExtrusionPattern ?? fillExtrusionPattern, + fillExtrusionHeight: changes.fillExtrusionHeight ?? fillExtrusionHeight, + fillExtrusionBase: changes.fillExtrusionBase ?? fillExtrusionBase, + fillExtrusionVerticalGradient: changes.fillExtrusionVerticalGradient ?? + fillExtrusionVerticalGradient, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('fill-extrusion-opacity', fillExtrusionOpacity); + addIfPresent('fill-extrusion-color', fillExtrusionColor); + addIfPresent('fill-extrusion-translate', fillExtrusionTranslate); + addIfPresent( + 'fill-extrusion-translate-anchor', fillExtrusionTranslateAnchor); + addIfPresent('fill-extrusion-pattern', fillExtrusionPattern); + addIfPresent('fill-extrusion-height', fillExtrusionHeight); + addIfPresent('fill-extrusion-base', fillExtrusionBase); + addIfPresent( + 'fill-extrusion-vertical-gradient', fillExtrusionVerticalGradient); + addIfPresent('visibility', visibility); + return json; + } + + factory FillExtrusionLayerProperties.fromJson(Map json) { + return FillExtrusionLayerProperties( + fillExtrusionOpacity: json['fill-extrusion-opacity'], + fillExtrusionColor: json['fill-extrusion-color'], + fillExtrusionTranslate: json['fill-extrusion-translate'], + fillExtrusionTranslateAnchor: json['fill-extrusion-translate-anchor'], + fillExtrusionPattern: json['fill-extrusion-pattern'], + fillExtrusionHeight: json['fill-extrusion-height'], + fillExtrusionBase: json['fill-extrusion-base'], + fillExtrusionVerticalGradient: json['fill-extrusion-vertical-gradient'], + visibility: json['visibility'], + ); + } +} + class RasterLayerProperties implements LayerProperties { // Paint Properties /// The opacity at which the image will be drawn. @@ -2070,3 +2252,129 @@ class HillshadeLayerProperties implements LayerProperties { ); } } + +class HeatmapLayerProperties implements LayerProperties { + // Paint Properties + /// Radius of influence of one heatmap point in pixels. Increasing the + /// value makes the heatmap smoother, but less detailed. + /// + /// Type: number + /// default: 30 + /// minimum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic heatmapRadius; + + /// A measure of how much an individual point contributes to the heatmap. + /// A value of 10 would be equivalent to having 10 points of weight 1 in + /// the same spot. Especially useful when combined with clustering. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic heatmapWeight; + + /// Similar to `heatmap-weight` but controls the intensity of the heatmap + /// globally. Primarily used for adjusting the heatmap based on zoom + /// level. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic heatmapIntensity; + + /// Defines the color of each pixel based on its density value in a + /// heatmap. Should be an expression that uses `["heatmap-density"]` as + /// input. + /// + /// Type: color + /// default: [interpolate, [linear], [heatmap-density], 0, rgba(0, 0, 255, 0), 0.1, royalblue, 0.3, cyan, 0.5, lime, 0.7, yellow, 1, red] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic heatmapColor; + + /// The global opacity at which the heatmap layer will be drawn. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic heatmapOpacity; + + // Layout Properties + /// Whether this layer is displayed. + /// + /// Type: enum + /// default: visible + /// Options: + /// "visible" + /// The layer is shown. + /// "none" + /// The layer is not shown. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic visibility; + + const HeatmapLayerProperties({ + this.heatmapRadius, + this.heatmapWeight, + this.heatmapIntensity, + this.heatmapColor, + this.heatmapOpacity, + this.visibility, + }); + + HeatmapLayerProperties copyWith(HeatmapLayerProperties changes) { + return HeatmapLayerProperties( + heatmapRadius: changes.heatmapRadius ?? heatmapRadius, + heatmapWeight: changes.heatmapWeight ?? heatmapWeight, + heatmapIntensity: changes.heatmapIntensity ?? heatmapIntensity, + heatmapColor: changes.heatmapColor ?? heatmapColor, + heatmapOpacity: changes.heatmapOpacity ?? heatmapOpacity, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('heatmap-radius', heatmapRadius); + addIfPresent('heatmap-weight', heatmapWeight); + addIfPresent('heatmap-intensity', heatmapIntensity); + addIfPresent('heatmap-color', heatmapColor); + addIfPresent('heatmap-opacity', heatmapOpacity); + addIfPresent('visibility', visibility); + return json; + } + + factory HeatmapLayerProperties.fromJson(Map json) { + return HeatmapLayerProperties( + heatmapRadius: json['heatmap-radius'], + heatmapWeight: json['heatmap-weight'], + heatmapIntensity: json['heatmap-intensity'], + heatmapColor: json['heatmap-color'], + heatmapOpacity: json['heatmap-opacity'], + visibility: json['visibility'], + ); + } +} diff --git a/lib/src/mapbox_map.dart b/lib/src/mapbox_map.dart index ffd86c02e..6649c8c24 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -55,6 +55,8 @@ class MapboxMap extends StatefulWidget { AnnotationType.line, AnnotationType.circle, ], + this.useDelayedDisposal, + this.useHybridCompositionOverride, }) : assert(annotationOrder.length <= 4), assert(annotationConsumeTapEvents.length > 0), super(key: key); @@ -222,6 +224,13 @@ class MapboxMap extends StatefulWidget { /// * All fade/transition animations have completed final OnMapIdleCallback? onMapIdle; + // This flag has no effect anymore and will be removed in the next major release. + @deprecated + final bool? useDelayedDisposal; + + /// Override hybrid mode per map instance + final bool? useHybridCompositionOverride; + /// Set `MapboxMap.useHybridComposition` to `false` in order use Virtual-Display /// (better for Android 9 and below but may result in errors on Android 12) /// or leave it `true` (default) to use Hybrid composition (Slower on Android 9 and below). @@ -251,7 +260,8 @@ class _MapboxMapState extends State { 'options': _MapboxMapOptions.fromWidget(widget).toMap(), 'accessToken': widget.accessToken, 'onAttributionClickOverride': widget.onAttributionClick != null, - 'dragEnabled': widget.dragEnabled + 'dragEnabled': widget.dragEnabled, + 'useHybridCompositionOverride': widget.useHybridCompositionOverride, }; return _mapboxGlPlatform.buildView( creationParams, onPlatformViewCreated, widget.gestureRecognizers); diff --git a/mapbox_gl_platform_interface/CHANGELOG.md b/mapbox_gl_platform_interface/CHANGELOG.md index 9c4cda24a..c187c4f50 100644 --- a/mapbox_gl_platform_interface/CHANGELOG.md +++ b/mapbox_gl_platform_interface/CHANGELOG.md @@ -1,10 +1,19 @@ +## 0.16.0, May 19, 2022 +* Annotation manager moved to dart [#779](https://github.com/flutter-mapbox-gl/maps/pull/779) +* Fix issue with map disposal on web [#895](https://github.com/flutter-mapbox-gl/maps/pull/895) +* Add support for layer zoom limits [#934](https://github.com/flutter-mapbox-gl/maps/pull/934) +* Add and default to Hybrid composition on Android [#916](https://github.com/flutter-mapbox-gl/maps/pull/916) +* Implement layer filtering [#997](https://github.com/flutter-mapbox-gl/maps/pull/997) +* Drag event types support added [#987](https://github.com/flutter-mapbox-gl/maps/pull/987) +* Support filtering on addLayer [#1024](https://github.com/flutter-mapbox-gl/maps/pull/1024) + ## 0.15.0, January 13, 2022 -* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) +* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) * Fix web issues with style loaded, feature tap, add promoteId, pointer change issue [#785](https://github.com/flutter-mapbox-gl/maps/pull/785) * Full style source support [#797](https://github.com/flutter-mapbox-gl/maps/pull/797) ## 0.14.0, November 13, 2021 -* Remove memory leaks by disposing internal components [#706](https://github.com/tobrun/flutter-mapbox-gl/pull/706) +* Remove memory leaks by disposing internal components [#706](https://github.com/tobrun/flutter-mapbox-gl/pull/706) * Add support for Layers, properties and expressions backed by GeoJsonSource [#723](https://github.com/tobrun/flutter-mapbox-gl/pull/723) * Add attribution button gravity, position normally [#731](https://github.com/tobrun/flutter-mapbox-gl/pull/731) * Remove MapboxGlPlatform.getInstance [#710](https://github.com/tobrun/flutter-mapbox-gl/pull/710) diff --git a/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart b/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart index bf59ae3bb..5281f1f0d 100644 --- a/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart +++ b/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart @@ -3,13 +3,13 @@ library mapbox_gl_platform_interface; import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show visibleForTesting; part 'src/annotation.dart'; part 'src/callbacks.dart'; @@ -21,5 +21,6 @@ part 'src/method_channel_mapbox_gl.dart'; part 'src/symbol.dart'; part 'src/fill.dart'; part 'src/ui.dart'; +part 'src/snapshot.dart'; part 'src/mapbox_gl_platform_interface.dart'; part 'src/source_properties.dart'; diff --git a/mapbox_gl_platform_interface/lib/src/camera.dart b/mapbox_gl_platform_interface/lib/src/camera.dart index 3094923aa..0c49e3371 100644 --- a/mapbox_gl_platform_interface/lib/src/camera.dart +++ b/mapbox_gl_platform_interface/lib/src/camera.dart @@ -80,7 +80,7 @@ class CameraPosition { } @override - int get hashCode => hashValues(bearing, target, tilt, zoom); + int get hashCode => Object.hash(bearing, target, tilt, zoom); @override String toString() => diff --git a/mapbox_gl_platform_interface/lib/src/location.dart b/mapbox_gl_platform_interface/lib/src/location.dart index 7c058dda9..879cd74cf 100644 --- a/mapbox_gl_platform_interface/lib/src/location.dart +++ b/mapbox_gl_platform_interface/lib/src/location.dart @@ -53,7 +53,7 @@ class LatLng { } @override - int get hashCode => hashValues(latitude, longitude); + int get hashCode => Object.hash(latitude, longitude); } /// A latitude/longitude aligned rectangle. @@ -106,7 +106,7 @@ class LatLngBounds { } @override - int get hashCode => hashValues(southwest, northeast); + int get hashCode => Object.hash(southwest, northeast); } /// A geographical area representing a non-aligned quadrilateral @@ -164,7 +164,7 @@ class LatLngQuad { } @override - int get hashCode => hashValues(topLeft, topRight, bottomRight, bottomLeft); + int get hashCode => Object.hash(topLeft, topRight, bottomRight, bottomLeft); } /// User's observed location diff --git a/mapbox_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart b/mapbox_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart index 49cacae91..346998bd0 100644 --- a/mapbox_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart +++ b/mapbox_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart @@ -51,13 +51,16 @@ abstract class MapboxGlPlatform { OnPlatformViewCreatedCallback onPlatformViewCreated, Set>? gestureRecognizers); Future updateMapOptions(Map optionsUpdate); - Future animateCamera(CameraUpdate cameraUpdate); + Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}); Future moveCamera(CameraUpdate cameraUpdate); Future updateMyLocationTrackingMode( MyLocationTrackingMode myLocationTrackingMode); Future matchMapLanguageWithDeviceDefault(); + void resizeWebMap(); + void forceResizeWebMap(); + Future updateContentInsets(EdgeInsets insets, bool animated); Future setMapLanguage(String language); Future setTelemetryEnabled(bool enabled); @@ -78,6 +81,9 @@ abstract class MapboxGlPlatform { Future addImageSource( String imageSourceId, Uint8List bytes, LatLngQuad coordinates); + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates); + Future addLayer(String imageLayerId, String imageSourceId, double? minzoom, double? maxzoom); @@ -88,6 +94,8 @@ abstract class MapboxGlPlatform { Future setFilter(String layerId, dynamic filter); + Future setVisibility(String layerId, bool isVisible); + Future toScreenLocation(LatLng latLng); Future> toScreenLocationBatch(Iterable latLngs); @@ -142,6 +150,15 @@ abstract class MapboxGlPlatform { dynamic filter, required bool enableInteraction}); + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}); + Future addRasterLayer( String sourceId, String layerId, Map properties, {String? belowLayerId, @@ -156,8 +173,17 @@ abstract class MapboxGlPlatform { double? minzoom, double? maxzoom}); + Future addHeatmapLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}); + Future addSource(String sourceId, SourceProperties properties); + Future takeSnapshot(SnapshotOptions snapshotOptions); + @mustCallSuper void dispose() { // clear all callbacks to avoid cyclic refs diff --git a/mapbox_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart b/mapbox_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart index 38da956cf..2187f21c8 100644 --- a/mapbox_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart +++ b/mapbox_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart @@ -141,7 +141,10 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { OnPlatformViewCreatedCallback onPlatformViewCreated, Set>? gestureRecognizers) { if (defaultTargetPlatform == TargetPlatform.android) { - if (useHybridComposition) { + final useHybridCompositionParam = + (creationParams['useHybridCompositionOverride'] ?? + useHybridComposition) as bool; + if (useHybridCompositionParam) { return PlatformViewLink( viewType: 'plugins.flutter.io/mapbox_gl', surfaceFactory: ( @@ -156,8 +159,8 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { ); }, onCreatePlatformView: (PlatformViewCreationParams params) { - final SurfaceAndroidViewController controller = - PlatformViewsService.initSurfaceAndroidView( + late AndroidViewController controller; + controller = PlatformViewsService.initAndroidView( id: params.id, viewType: 'plugins.flutter.io/mapbox_gl', layoutDirection: TextDirection.ltr, @@ -211,9 +214,10 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future animateCamera(cameraUpdate) async { + Future animateCamera(cameraUpdate, {Duration? duration}) async { return await _channel.invokeMethod('camera#animate', { 'cameraUpdate': cameraUpdate.toJson(), + 'duration': duration?.inMilliseconds, }); } @@ -298,8 +302,13 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { { 'left': rect.left, 'top': rect.top, + //specific arguments needed for android rect function 'right': rect.right, 'bottom': rect.bottom, + //specific arguments needed for iOS rect function + 'width': rect.width, + 'height': rect.height, + //arguments for mapbox 'layerIds': layerIds, 'filter': filter, }, @@ -385,6 +394,22 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } } + @override + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates) async { + try { + return await _channel + .invokeMethod('style#updateImageSource', { + 'imageSourceId': imageSourceId, + 'bytes': bytes, + 'length': bytes?.length, + 'coordinates': coordinates?.toList() + }); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + @override Future toScreenLocation(LatLng latLng) async { try { @@ -484,6 +509,16 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } } + @override + Future setVisibility(String layerId, bool isVisible) async { + try { + return await _channel.invokeMethod('style#setVisibility', + {'layerId': layerId, 'isVisible': isVisible}); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + @override Future toLatLng(Point screenLocation) async { try { @@ -621,6 +656,29 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { }); } + @override + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('fillExtrusionLayer#add', { + 'sourceId': sourceId, + 'layerId': layerId, + 'belowLayerId': belowLayerId, + 'sourceLayer': sourceLayer, + 'minzoom': minzoom, + 'maxzoom': maxzoom, + 'filter': jsonEncode(filter), + 'enableInteraction': enableInteraction, + 'properties': properties + .map((key, value) => MapEntry(key, jsonEncode(value))) + }); + } + @override void dispose() { super.dispose(); @@ -671,6 +729,24 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { }); } + @override + Future addHeatmapLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + await _channel.invokeMethod('heatmapLayer#add', { + 'sourceId': sourceId, + 'layerId': layerId, + 'belowLayerId': belowLayerId, + 'minzoom': minzoom, + 'maxzoom': maxzoom, + 'properties': properties + .map((key, value) => MapEntry(key, jsonEncode(value))) + }); + } + Future setFeatureForGeoJsonSource( String sourceId, Map geojsonFeature) async { await _channel.invokeMethod('source#setFeature', { @@ -678,4 +754,22 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { 'geojsonFeature': jsonEncode(geojsonFeature) }); } + + @override + void forceResizeWebMap() {} + + @override + void resizeWebMap() {} + + @override + Future takeSnapshot(SnapshotOptions snapshotOptions) async { + try { + debugPrint("${snapshotOptions.toJson()}"); + var uri = await _channel.invokeMethod( + 'snapshot#takeSnapshot', snapshotOptions.toJson()); + return uri; + } on PlatformException catch (e) { + return new Future.error(e); + } + } } diff --git a/mapbox_gl_platform_interface/lib/src/snapshot.dart b/mapbox_gl_platform_interface/lib/src/snapshot.dart new file mode 100644 index 000000000..679770a57 --- /dev/null +++ b/mapbox_gl_platform_interface/lib/src/snapshot.dart @@ -0,0 +1,155 @@ +part of mapbox_gl_platform_interface; + +/// Set of options for taking map snapshot +class SnapshotOptions { + /// Dimensions of the snapshot + /// The width of the image + final double width; + + /// Dimensions of the snapshot + /// The height of the image + final double height; + + /// If you want to take snapshot with camera position option + /// + /// Current center coordinate of camera position + final LatLng? centerCoordinate; + + /// The coordinate rectangle that encompasses the bounds to capture. This is applied after the camera position + final LatLngBounds? bounds; + + /// If you want to take snapshot with camera position option + /// + /// Zoom level of camera position + final double? zoomLevel; + + /// If you want to take snapshot with camera position option + /// + /// Pitch toward the horizon measured in degrees, with 0 degrees resulting in a two-dimensional map + final double? pitch; + + /// If you want to take snapshot with camera position option + /// + /// Heading measured in degrees clockwise from true north + final double? heading; + + /// URL of the map style to snapshot. The URL may be a full HTTP or HTTPS URL, a Mapbox style URL + final String? styleUri; + + /// StyleJson of the map style to snapshot + final String? styleJson; + + /// Android Only: The flag indicating to show the Mapbox logo + final bool withLogo; + + /// True: Save snapshot in cache and return path + /// False: Return base64 value + final bool writeToDisk; + + ///The [width] and [height] arguments must not be null + SnapshotOptions( + {required this.width, + required this.height, + this.centerCoordinate, + this.bounds, + this.zoomLevel, + double? pitch, + double? heading, + this.styleUri, + this.styleJson, + bool? withLogo, + bool? writeToDisk}) + : this.withLogo = withLogo ?? false, + this.writeToDisk = writeToDisk ?? true, + this.pitch = pitch ?? 0, + this.heading = heading ?? 0; + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('width', Platform.isAndroid ? width.toInt() : width); + addIfPresent('height', Platform.isAndroid ? height.toInt() : height); + + if (bounds != null) { + if (Platform.isAndroid) { + final featureCollection = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + bounds!.northeast.longitude, + bounds!.northeast.latitude + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + bounds!.southwest.longitude, + bounds!.southwest.latitude + ] + } + } + ] + }; + addIfPresent("bounds", featureCollection.toString()); + } else { + final list = [ + [ + bounds!.southwest.latitude, + bounds!.southwest.longitude, + ], + [ + bounds!.northeast.latitude, + bounds!.northeast.longitude, + ] + ]; + addIfPresent("bounds", list); + } + } + if (centerCoordinate != null && zoomLevel != null) { + if (Platform.isAndroid) { + final feature = { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + centerCoordinate!.longitude, + centerCoordinate!.latitude + ] + } + }; + addIfPresent('centerCoordinate', feature.toString()); + } else { + final list = [ + centerCoordinate!.latitude, + centerCoordinate!.longitude, + ]; + addIfPresent('centerCoordinate', list); + } + + addIfPresent('zoomLevel', zoomLevel); + } + addIfPresent('pitch', pitch); + addIfPresent('heading', heading); + addIfPresent('styleUri', styleUri); + addIfPresent('styleJson', styleJson); + addIfPresent('withLogo', withLogo); + addIfPresent('writeToDisk', writeToDisk); + return json; + } +} diff --git a/mapbox_gl_platform_interface/lib/src/source_properties.dart b/mapbox_gl_platform_interface/lib/src/source_properties.dart index 17351e9b0..c6b450c7c 100644 --- a/mapbox_gl_platform_interface/lib/src/source_properties.dart +++ b/mapbox_gl_platform_interface/lib/src/source_properties.dart @@ -601,7 +601,7 @@ class VideoSourceProperties implements SourceProperties { /// Corners of video specified in longitude, latitude pairs. /// /// Type: array - final List? coordinates; + final List? coordinates; const VideoSourceProperties({ this.urls, @@ -610,7 +610,7 @@ class VideoSourceProperties implements SourceProperties { VideoSourceProperties copyWith( List? urls, - List? coordinates, + List? coordinates, ) { return VideoSourceProperties( urls: urls ?? this.urls, @@ -650,7 +650,7 @@ class ImageSourceProperties implements SourceProperties { /// Corners of image specified in longitude, latitude pairs. /// /// Type: array - final List? coordinates; + final List? coordinates; const ImageSourceProperties({ this.url, @@ -659,7 +659,7 @@ class ImageSourceProperties implements SourceProperties { ImageSourceProperties copyWith( String? url, - List? coordinates, + List? coordinates, ) { return ImageSourceProperties( url: url ?? this.url, diff --git a/mapbox_gl_platform_interface/lib/src/ui.dart b/mapbox_gl_platform_interface/lib/src/ui.dart index 5bacf8a08..6a766d8fa 100644 --- a/mapbox_gl_platform_interface/lib/src/ui.dart +++ b/mapbox_gl_platform_interface/lib/src/ui.dart @@ -143,7 +143,7 @@ class MinMaxZoomPreference { } @override - int get hashCode => hashValues(minZoom, maxZoom); + int get hashCode => Object.hash(minZoom, maxZoom); @override String toString() { diff --git a/mapbox_gl_platform_interface/pubspec.yaml b/mapbox_gl_platform_interface/pubspec.yaml index 6cbdc932b..cd1269fc2 100644 --- a/mapbox_gl_platform_interface/pubspec.yaml +++ b/mapbox_gl_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: mapbox_gl_platform_interface description: A common platform interface for the mapbox_gl plugin. -version: 0.15.0 +version: 0.16.0 homepage: https://github.com/tobrun/flutter-mapbox-gl dependencies: @@ -9,5 +9,5 @@ dependencies: meta: ^1.0.5 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' flutter: ">=2.0.0" diff --git a/mapbox_gl_web/CHANGELOG.md b/mapbox_gl_web/CHANGELOG.md index a8330bde4..854f294c6 100644 --- a/mapbox_gl_web/CHANGELOG.md +++ b/mapbox_gl_web/CHANGELOG.md @@ -1,5 +1,16 @@ +## 0.16.0, May 19, 2022 +* Fix type issues in query rendered features in rect [#862](https://github.com/flutter-mapbox-gl/maps/pull/862) +* Annotation manager moved to dart [#779](https://github.com/flutter-mapbox-gl/maps/pull/779) +* Fix issue with map disposal on web [#895](https://github.com/flutter-mapbox-gl/maps/pull/895) +* Fix for rescale issues on web [#896](https://github.com/flutter-mapbox-gl/maps/pull/896) +* Upgraded mapbox gl js to 2.7.0 [#889](https://github.com/flutter-mapbox-gl/maps/pull/889) +* Add support for layer zoom limits [#934](https://github.com/flutter-mapbox-gl/maps/pull/934) +* Implement layer filtering [#997](https://github.com/flutter-mapbox-gl/maps/pull/997) +* Drag event types support added [#987](https://github.com/flutter-mapbox-gl/maps/pull/987) +* Support filtering on addLayer [#1024](https://github.com/flutter-mapbox-gl/maps/pull/1024) + ## 0.15.0, January 13, 2022 -* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) +* Callbacks added to onFeatureTapped will now also get the position (`Point`) and the location (`LatLng`) of the click passed when called [#798](https://github.com/flutter-mapbox-gl/maps/pull/798) * Fix web issues with style loaded, feature tap, add promoteId, pointer change issue [#785](https://github.com/flutter-mapbox-gl/maps/pull/785) * Add check for Dart formatting [#803](https://github.com/flutter-mapbox-gl/maps/pull/803) * Remove unnecessary print of style height and width [#847](https://github.com/flutter-mapbox-gl/maps/pull/847) diff --git a/mapbox_gl_web/lib/mapbox_gl_web.dart b/mapbox_gl_web/lib/mapbox_gl_web.dart index a6296a824..0c154a4e6 100644 --- a/mapbox_gl_web/lib/mapbox_gl_web.dart +++ b/mapbox_gl_web/lib/mapbox_gl_web.dart @@ -1,11 +1,13 @@ library mapbox_gl_web; import 'dart:async'; +import 'dart:convert'; // FIXED HERE: https://github.com/dart-lang/linter/pull/1985 // ignore_for_file: avoid_web_libraries_in_flutter import 'dart:html'; // ignore: unused_import import 'dart:js'; +import 'dart:js_util'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -13,13 +15,14 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Element; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:mapbox_gl_platform_interface/mapbox_gl_platform_interface.dart'; import 'package:mapbox_gl_dart/mapbox_gl_dart.dart' hide Point, Source; import 'package:mapbox_gl_dart/mapbox_gl_dart.dart' as mapbox show Point; import 'package:image/image.dart' hide Point; +import 'package:js/js_util.dart' as jsUtil; import 'package:mapbox_gl_web/src/layer_tools.dart'; part 'src/convert.dart'; diff --git a/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart b/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart index 8a506b8a5..c3872cf50 100644 --- a/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart +++ b/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart @@ -24,6 +24,7 @@ class MapboxWebGlPlatform extends MapboxGlPlatform String? _navigationControlPosition; NavigationControl? _navigationControl; + Timer? lastResizeObserverTimer; @override Widget buildView( @@ -47,8 +48,10 @@ class MapboxWebGlPlatform extends MapboxGlPlatform ui.platformViewRegistry.registerViewFactory( 'plugins.flutter.io/mapbox_gl_$identifier', (int viewId) { _mapElement = DivElement() - ..style.width = '100%' - ..style.height = '100%'; + ..style.position = 'absolute' + ..style.top = '0' + ..style.bottom = '0' + ..style.width = '100%'; callback(viewId); return _mapElement; }); @@ -59,7 +62,6 @@ class MapboxWebGlPlatform extends MapboxGlPlatform await _addStylesheetToShadowRoot(_mapElement); if (_creationParams.containsKey('initialCameraPosition')) { var camera = _creationParams['initialCameraPosition']; - _dragEnabled = _creationParams['dragEnabled'] ?? true; if (_creationParams.containsKey('accessToken')) { @@ -73,6 +75,7 @@ class MapboxWebGlPlatform extends MapboxGlPlatform zoom: camera['zoom'], bearing: camera['bearing'], pitch: camera['tilt'], + preserveDrawingBuffer: true, ), ); _map.on('load', _onStyleLoaded); @@ -82,16 +85,31 @@ class MapboxWebGlPlatform extends MapboxGlPlatform _map.on('movestart', _onCameraMoveStarted); _map.on('move', _onCameraMove); _map.on('moveend', _onCameraIdle); - _map.on('resize', _onMapResize); + _map.on('resize', (_) => _onMapResize()); _map.on('styleimagemissing', _loadFromAssets); if (_dragEnabled) { _map.on('mouseup', _onMouseUp); _map.on('mousemove', _onMouseMove); } + + _initResizeObserver(); } Convert.interpretMapboxMapOptions(_creationParams['options'], this); } + void _initResizeObserver() { + final resizeObserver = ResizeObserver((entries, observer) { + // The resize observer might be called a lot of times when the user resizes the browser window with the mouse for example. + // Due to the fact that the resize call is quite expensive it should not be called for every triggered event but only the last one, like "onMoveEnd". + // But because there is no event type for the end, there is only the option to spawn timers and cancel the previous ones if they get overwritten by a new event. + lastResizeObserverTimer?.cancel(); + lastResizeObserverTimer = Timer(Duration(milliseconds: 50), () { + _onMapResize(); + }); + }); + resizeObserver.observe(document.body as Element); + } + void _loadFromAssets(Event event) async { final imagePath = event.id; final ByteData bytes = await rootBundle.load(imagePath); @@ -177,9 +195,25 @@ class MapboxWebGlPlatform extends MapboxGlPlatform } @override - Future animateCamera(CameraUpdate cameraUpdate) async { + Future animateCamera(CameraUpdate cameraUpdate, + {Duration? duration}) async { final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); - _map.flyTo(cameraOptions); + + final around = getProperty(cameraOptions, 'around'); + final bearing = getProperty(cameraOptions, 'bearing'); + final center = getProperty(cameraOptions, 'center'); + final pitch = getProperty(cameraOptions, 'pitch'); + final zoom = getProperty(cameraOptions, 'zoom'); + + _map.flyTo({ + if (around.jsObject != null) 'around': around, + if (bearing != null) 'bearing': bearing, + if (center.jsObject != null) 'center': center, + if (pitch != null) 'pitch': pitch, + if (zoom != null) 'zoom': zoom, + if (duration != null) 'duration': duration.inMilliseconds, + }); + return true; } @@ -340,12 +374,12 @@ class MapboxWebGlPlatform extends MapboxGlPlatform void _onStyleLoaded(_) { _mapReady = true; - _map.resize(); + _onMapResize(); onMapStyleLoadedPlatform(null); } - void _onMapResize(Event e) { - Timer(Duration(microseconds: 10), () { + void _onMapResize() { + Timer(Duration(), () { var container = _map.getContainer(); var canvas = _map.getCanvas(); var widthMismatch = canvas.clientWidth != container.clientWidth; @@ -607,7 +641,13 @@ class MapboxWebGlPlatform extends MapboxGlPlatform } _interactiveFeatureLayerIds.clear(); - _map.setStyle(styleString); + try { + final styleJson = jsonDecode(styleString ?? ''); + final styleJsObject = jsUtil.jsify(styleJson); + _map.setStyle(styleJsObject); + } catch (_) { + _map.setStyle(styleString); + } // catch style loaded for later style changes if (_mapReady) { _map.once("styledata", _onStyleLoaded); @@ -652,8 +692,10 @@ class MapboxWebGlPlatform extends MapboxGlPlatform @override Future removeLayer(String layerId) async { - _interactiveFeatureLayerIds.remove(layerId); - _map.removeLayer(layerId); + if (_map.getLayer(layerId) != null) { + _interactiveFeatureLayerIds.remove(layerId); + _map.removeLayer(layerId); + } } @override @@ -661,6 +703,15 @@ class MapboxWebGlPlatform extends MapboxGlPlatform _map.setFilter(layerId, filter); } + @override + Future setVisibility(String layerId, bool isVisible) async { + final layer = _map.getLayer(layerId); + if (layer != null) { + _map.setLayoutProperty( + layerId, 'visibility', isVisible ? 'visible' : 'none'); + } + } + @override Future addGeoJsonSource(String sourceId, Map geojson, {String? promoteId}) async { @@ -732,6 +783,24 @@ class MapboxWebGlPlatform extends MapboxGlPlatform enableInteraction: enableInteraction); } + @override + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + return _addLayer(sourceId, layerId, properties, "fill-extrusion", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction); + } + @override Future addLineLayer( String sourceId, String layerId, Map properties, @@ -783,6 +852,21 @@ class MapboxWebGlPlatform extends MapboxGlPlatform enableInteraction: false); } + @override + Future addHeatmapLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + return _addLayer(sourceId, layerId, properties, "heatmap", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + enableInteraction: false); + } + @override Future addRasterLayer( String sourceId, String layerId, Map properties, @@ -811,6 +895,8 @@ class MapboxWebGlPlatform extends MapboxGlPlatform final paint = Map.fromEntries( properties.entries.where((entry) => !isLayoutProperty(entry.key))); + removeLayer(layerId); + _map.addLayer({ 'id': layerId, 'type': layerType, @@ -910,6 +996,12 @@ class MapboxWebGlPlatform extends MapboxGlPlatform throw UnimplementedError(); } + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates) { + // TODO: implement addImageSource + throw UnimplementedError(); + } + @override Future addLayer(String imageLayerId, String imageSourceId, double? minzoom, double? maxzoom) { @@ -949,4 +1041,32 @@ class MapboxWebGlPlatform extends MapboxGlPlatform } } } + + @override + Future takeSnapshot(SnapshotOptions snapshotOptions) async { + if (snapshotOptions.styleUri != null || snapshotOptions.styleJson != null) { + throw UnsupportedError("style option is not supported"); + } + if (snapshotOptions.bounds != null) { + throw UnsupportedError("bounds option is not supported"); + } + if (snapshotOptions.centerCoordinate != null || + snapshotOptions.zoomLevel != null || + snapshotOptions.pitch != 0 || + snapshotOptions.heading != 0) { + throw UnsupportedError("camera posision option is not supported"); + } + final base64String = await _map.getCanvas().toDataUrl('image/jpeg'); + return base64String; + } + + @override + void resizeWebMap() { + _onMapResize(); + } + + @override + void forceResizeWebMap() { + _map.resize(); + } } diff --git a/mapbox_gl_web/pubspec.yaml b/mapbox_gl_web/pubspec.yaml index e6e736718..d14051e77 100644 --- a/mapbox_gl_web/pubspec.yaml +++ b/mapbox_gl_web/pubspec.yaml @@ -1,6 +1,6 @@ name: mapbox_gl_web description: Web platform implementation of mapbox_gl -version: 0.15.0 +version: 0.16.0 homepage: https://github.com/tobrun/flutter-mapbox-gl flutter: @@ -21,7 +21,7 @@ dependencies: url: https://github.com/tobrun/flutter-mapbox-gl.git path: mapbox_gl_platform_interface mapbox_gl_dart: ^0.2.1 - image: ^3.0.0 + image: '>=3.0.0 <5.0.0' dependency_overrides: mapbox_gl_platform_interface: diff --git a/pubspec.lock b/pubspec.lock index 4aab14388..3064bfbed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,28 +5,32 @@ packages: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: a92e39b291073bb840a72cf43d96d2a63c74e9a485d227833e8ea0054d16ad16 + url: "https://pub.dev" source: hosted version: "3.1.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + url: "https://pub.dev" source: hosted version: "3.0.1" flutter: @@ -43,56 +47,70 @@ packages: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "3e5c9ef82c0af7823be4cb5294a829a6e0548a6f6b4e261e6386509a9e03bcab" + url: "https://pub.dev" source: hosted version: "3.0.2" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" mapbox_gl_dart: dependency: transitive description: name: mapbox_gl_dart - url: "https://pub.dartlang.org" + sha256: de6d03718e5eb05c9eb1ddaae7f0383b28acb5afa16405e1deed7ff04dd34f3d + url: "https://pub.dev" source: hosted - version: "0.2.0-nullsafety.0" + version: "0.2.1" mapbox_gl_platform_interface: dependency: "direct main" description: path: mapbox_gl_platform_interface relative: true source: path - version: "0.14.0" + version: "0.16.0" mapbox_gl_web: dependency: "direct main" description: path: mapbox_gl_web relative: true source: path - version: "0.14.0" + version: "0.16.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" source: hosted version: "1.8.0" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "85e8f8b118afcccf948a9844d199e56260117400bd9b9982d87bf1d62ebc1690" + url: "https://pub.dev" source: hosted version: "4.1.0" sky_engine: @@ -104,23 +122,26 @@ packages: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "88d26fad429944d29b7c418177d156d04bbef049f295cf48130eccc84f0b8b4b" + url: "https://pub.dev" source: hosted version: "5.1.0" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index d99a102e5..d14c36b8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mapbox_gl description: A Flutter plugin for integrating Mapbox Maps inside a Flutter application on Android, iOS and web platfroms. -version: 0.15.0 +version: 0.16.0 homepage: https://github.com/tobrun/flutter-mapbox-gl dependencies: @@ -34,6 +34,6 @@ flutter: default_package: mapbox_gl_web environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' # Flutter versions prior to 1.10 did not support the flutter.plugin.platforms map. flutter: ">=2.0.0" diff --git a/scripts/lib/conversions.dart b/scripts/lib/conversions.dart index 1cd5a411c..016f8f86a 100644 --- a/scripts/lib/conversions.dart +++ b/scripts/lib/conversions.dart @@ -35,6 +35,9 @@ const renamedIosProperties = { "visibility": "isVisible", "rasterBrightnessMin": "minimumRasterBrightness", "rasterBrightnessMax": "maximumRasterBrightness", + "fillExtrusionTranslate": "fillExtrusionTranslation", + "fillExtrusionTranslateAnchor": "fillExtrusionTranslationAnchor", + "fillExtrusionVerticalGradient": "fillExtrusionHasVerticalGradient", }; const dartTypeMappingTable = { diff --git a/scripts/lib/generate.dart b/scripts/lib/generate.dart index e797bb688..3ee9fed6b 100644 --- a/scripts/lib/generate.dart +++ b/scripts/lib/generate.dart @@ -15,8 +15,10 @@ main() async { "circle", "line", "fill", + "fill-extrusion", "raster", - "hillshade" + "hillshade", + "heatmap" ]; final sourceTypes = [ "vector", @@ -33,6 +35,7 @@ main() async { { "type": type, "typePascal": ReCase(type).pascalCase, + "typeCamel": ReCase(type).camelCase, "paint_properties": buildStyleProperties(styleJson, "paint_$type"), "layout_properties": buildStyleProperties(styleJson, "layout_$type"), }, @@ -42,6 +45,7 @@ main() async { { "type": type.replaceAll("_", "-"), "typePascal": ReCase(type).pascalCase, + "typeCamel": ReCase(type).camelCase, "properties": buildSourceProperties(styleJson, "source_$type"), }, ], @@ -54,7 +58,7 @@ main() async { ...type["layout_properties"].map((p) => p["value"]).toList() ].toSet().map((p) => {"property": p}).toList(); - const templates = [ + final templates = [ "android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java", "ios/Classes/LayerPropertyConverter.swift", "lib/src/layer_expressions.dart", @@ -76,7 +80,7 @@ Future render( print("Rendering $filename"); var templateFile = - await File('./scripts/templates/$filename.template').readAsString(); + await File('scripts/templates/$filename.template').readAsString(); var template = Template(templateFile); var outputFile = File('$outputPath/$filename'); @@ -122,9 +126,9 @@ Map buildSourceProperty( final typeDart = dartTypeMappingTable[value["type"]]; final typeSwift = swiftTypeMappingTable[value["type"]]; final nestedTypeDart = dartTypeMappingTable[value["value"]] ?? - dartTypeMappingTable[value["value"]["type"]]; + dartTypeMappingTable[value["value"]?["type"]]; final nestedTypeSwift = swiftTypeMappingTable[value["value"]] ?? - swiftTypeMappingTable[value["value"]["type"]]; + swiftTypeMappingTable[value["value"]?["type"]]; var defaultValue = value["default"]; if (defaultValue is List) { diff --git a/scripts/templates/LayerPropertyConverter.swift.template b/scripts/templates/LayerPropertyConverter.swift.template index 4719a9cdb..a06903db4 100644 --- a/scripts/templates/LayerPropertyConverter.swift.template +++ b/scripts/templates/LayerPropertyConverter.swift.template @@ -6,17 +6,17 @@ import MapboxAnnotationExtension class LayerPropertyConverter { {{#layerTypes}} - class func add{{typePascal}}Properties({{type}}Layer: MGL{{typePascal}}StyleLayer, properties: [String: String]) { + class func add{{typePascal}}Properties({{typeCamel}}Layer: MGL{{typePascal}}StyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) switch propertyName { {{#paint_properties}} case "{{{value}}}": {{#isIosAsCamelCase}} - {{type}}Layer.{{iosAsCamelCase}} = expression; + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression; {{/isIosAsCamelCase}} {{^isIosAsCamelCase}} - {{type}}Layer.{{valueAsCamelCase}} = expression; + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression; {{/isIosAsCamelCase}} break; {{/paint_properties}} @@ -24,14 +24,14 @@ class LayerPropertyConverter { case "{{{value}}}": {{^isVisibilityProperty}} {{#isIosAsCamelCase}} - {{type}}Layer.{{iosAsCamelCase}} = expression; + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression; {{/isIosAsCamelCase}} {{^isIosAsCamelCase}} - {{type}}Layer.{{valueAsCamelCase}} = expression; + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression; {{/isIosAsCamelCase}} {{/isVisibilityProperty}} {{#isVisibilityProperty}} - {{type}}Layer.{{iosAsCamelCase}} = propertyValue == "visible"; + {{typeCamel}}Layer.{{iosAsCamelCase}} = propertyValue == "visible"; {{/isVisibilityProperty}} break; {{/layout_properties}}