diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..3b0f14a32 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false \ No newline at end of file diff --git a/.github/workflows/flutter_ci.yml b/.github/workflows/flutter_ci.yml index af062a63b..cf1a18f55 100644 --- a/.github/workflows/flutter_ci.yml +++ b/.github/workflows/flutter_ci.yml @@ -8,31 +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: 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: ./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: ./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 @@ -43,32 +85,33 @@ 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: | echo "machine api.mapbox.com login mapbox password $SDK_REGISTRY_TOKEN" >> ~/.netrc + chmod 600 ~/.netrc cd ./example flutter build ios --release --no-codesign env: SDK_REGISTRY_TOKEN: ${{ secrets.SDK_REGISTRY_IOS}} - + build-web: name: "Build web" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - with: - channel: beta - - 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 f553615a2..dcc46455e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,89 @@ -## 0.12.0, April 12, 2020 +## 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) +* 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) +* Remove layer before adding layer if layer is added in place example [#766](https://github.com/flutter-mapbox-gl/maps/pull/766) +* Speed property is null when onUserLocationUpdated is called [#767](https://github.com/flutter-mapbox-gl/maps/pull/767) +* Improve iOS OnStyleReady reliability [#775](https://github.com/flutter-mapbox-gl/maps/pull/775) +* Handle line color and geometry [#776](https://github.com/flutter-mapbox-gl/maps/pull/776) +* Fix web issues with style loaded, feature tap, add promoteId, pointer change issue [#785](https://github.com/flutter-mapbox-gl/maps/pull/785) +* Fix more issues with style loading [#787](https://github.com/flutter-mapbox-gl/maps/pull/787) +* Updated settings gradle to new version [#789](https://github.com/flutter-mapbox-gl/maps/pull/789) +* Remove the callbacks in dispose of example click_annotations dart [#791](https://github.com/flutter-mapbox-gl/maps/pull/791) +* Add check for Dart formatting [#803](https://github.com/flutter-mapbox-gl/maps/pull/803) +* Add check for Swift formatting [#804](https://github.com/flutter-mapbox-gl/maps/pull/804) +* Fixed race condition with map#waitForMap [#808](https://github.com/flutter-mapbox-gl/maps/pull/808) +* Add option to not use annotations on android [#820](https://github.com/flutter-mapbox-gl/maps/pull/820) +* Add linepattern in line.dart [#825](https://github.com/flutter-mapbox-gl/maps/pull/825) +* Respect native scale when adding symbols on iOS [#835](https://github.com/flutter-mapbox-gl/maps/pull/835) +* Remove unnecessary print of style height and width [#847](https://github.com/flutter-mapbox-gl/maps/pull/847) +* Android embedding fixes - migrate to maven [#852](https://github.com/flutter-mapbox-gl/maps/pull/852) +* Full style source support [#797](https://github.com/flutter-mapbox-gl/maps/pull/797) +* Gesture fixes [#851](https://github.com/flutter-mapbox-gl/maps/pull/851) +* 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) +* 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) +* Add documentation for setMapLanguage [#740](https://github.com/tobrun/flutter-mapbox-gl/pull/740) +* Make sure onStyleLoaded callback is invoked when map is loaded and ready [#690](https://github.com/tobrun/flutter-mapbox-gl/pull/690) +* Enable onMapIdle callback for android [#729](https://github.com/tobrun/flutter-mapbox-gl/pull/729) +* Set attribution margin to use left margin [#714](https://github.com/tobrun/flutter-mapbox-gl/pull/714) +* Getting the ACCESS_TOKEN from env [#726](https://github.com/tobrun/flutter-mapbox-gl/pull/726) +* Fixed crashes with offline manager [#724](https://github.com/tobrun/flutter-mapbox-gl/pull/724) +* Add divider for example list [#712](https://github.com/tobrun/flutter-mapbox-gl/pull/712) +* Fix respecting annotationConsumeTapEvents on iOS [#716](https://github.com/tobrun/flutter-mapbox-gl/pull/716) +* Add getSymbolLatLng and getLineLatLngs for web [#720](https://github.com/tobrun/flutter-mapbox-gl/pull/720) +* Fix typo in downloads token property name according to docs [#721](https://github.com/tobrun/flutter-mapbox-gl/pull/721) +* Remove MapboxGlPlatform.getInstance [#710](https://github.com/tobrun/flutter-mapbox-gl/pull/710) + +## 0.13.0, October 21, 2021 +* Migrate to null-safety [#607](https://github.com/tobrun/flutter-mapbox-gl/pull/607) +* Add missing removeLines removeCircles and removeFills [#622](https://github.com/tobrun/flutter-mapbox-gl/pull/622) +* Add support for colors with alpha [#561](https://github.com/tobrun/flutter-mapbox-gl/pull/561) +* Support override of attribution click action (iOS) [#605](https://github.com/tobrun/flutter-mapbox-gl/pull/605) +* Update to Mapbox-Android-SDK 9.6.2 [#674](https://github.com/tobrun/flutter-mapbox-gl/pull/674) +* Fix Warning: Operand of null-aware operation '!' has type 'Locale' which excludes null [#676](https://github.com/tobrun/flutter-mapbox-gl/pull/676) +* Make build work with instructions in docs (android) [#698](https://github.com/tobrun/flutter-mapbox-gl/pull/698) +* Fix requestMyLocationLatLng in the platform interface [#697](https://github.com/tobrun/flutter-mapbox-gl/pull/697) + +## 0.12.0, April 12, 2021 * Update to Mapbox-Android-SDK 9.6.0 [#489](https://github.com/tobrun/flutter-mapbox-gl/pull/489) * Update to Mapbox-iOS-SDK 6.3.0 [#513](https://github.com/tobrun/flutter-mapbox-gl/pull/513) * Batch creation/removal for circles, fills and lines [#576](https://github.com/tobrun/flutter-mapbox-gl/pull/576) @@ -8,7 +93,7 @@ * Emit onTap only for the feature above the others [#589](https://github.com/tobrun/flutter-mapbox-gl/pull/589) * Add annotationOrder to web [#588](https://github.com/tobrun/flutter-mapbox-gl/pull/588) -## 0.11.0, March 30, 2020 +## 0.11.0, March 30, 2021 * Fixed issues caused by new android API [#544](https://github.com/tobrun/flutter-mapbox-gl/pull/544) * Add option to set maximum offline tile count [#549](https://github.com/tobrun/flutter-mapbox-gl/pull/549) * Fixed web build failure due to http package upgrade [#550](https://github.com/tobrun/flutter-mapbox-gl/pull/550) @@ -21,7 +106,7 @@ * Define which annotations consume the tap events [#575](https://github.com/tobrun/flutter-mapbox-gl/pull/575) * Remove failed offline region downloads [#583](https://github.com/tobrun/flutter-mapbox-gl/pull/583) -## 0.10.0, February 12, 2020 +## 0.10.0, February 12, 2021 * Merge offline regions [#532](https://github.com/tobrun/flutter-mapbox-gl/pull/532) * Update offline region metadata [#530](https://github.com/tobrun/flutter-mapbox-gl/pull/530) * Added web support for fills [#501](https://github.com/tobrun/flutter-mapbox-gl/pull/501) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a160f0ad8..235f7c060 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,14 +2,11 @@ We welcome contributions to this repository. Please follow these steps if you're interested in making contributions: -1. Please familiarize yourself with the [process of running the example app](https://github.com/tobrun/flutter-mapbox-gl#running-the-example-app) and [add a Mapbox access token](https://github.com/tobrun/flutter-mapbox-gl#adding-a-mapbox-access-token) as described in the Readme. -2. Ensure that existing [pull requests](https://github.com/tobrun/flutter-mapbox-gl/pulls) and [issues](https://github.com/tobrun/flutter-mapbox-gl/issues) don’t already cover your contribution or question. +- Please familiarize yourself with the process of running the example app and adding Mapbox access tokens as described in the Readme. -3. Create a new branch that will contain your contributed code. Along with your contribution you should also adapt the example app to showcase any new features or APIs you have developed. This also makes testing your contribution much easier. Eventually create a pull request once you're done making changes. +- Ensure that existing [pull requests](https://github.com/tobrun/flutter-mapbox-gl/pulls) and [issues](https://github.com/tobrun/flutter-mapbox-gl/issues) don’t already cover your contributions or questions. -4. If there are any changes that developers should be aware of, please update the [changelog](https://github.com/tobrun/flutter-mapbox-gl/blob/master/CHANGELOG.md) once your pull request has been merged to the `master` branch. +- Create a new branch that will contain your contributed code. Along with your contribution you should also adapt the example app to showcase any new features or APIs you have developed. This also makes testing your contribution much easier. In an ideal case, you also make your contribution cross platform but this isn't a true requirement. Eventually create a pull request once you're done making changes. # Code of conduct -Everyone is invited to participate in Mapbox’s open source projects and public discussions: we want to create a welcoming and friendly environment. Harassment of participants or other unethical and unprofessional behavior will not be tolerated in our spaces. The [Contributor Covenant](http://contributor-covenant.org) applies to all projects under the Mapbox organization and we ask that you please read [the full text](http://contributor-covenant.org/version/1/2/0/). - -You can learn more about our open source philosophy on [mapbox.com](https://www.mapbox.com/about/open/). +Everyone is invited to participate in open source projects and public discussions: we want to create a welcoming and friendly environment. Harassment of participants or other unethical and unprofessional behavior will not be tolerated in this repository spaces. The [Contributor Covenant](http://contributor-covenant.org) applies to this project and we ask that you please read [the full text](http://contributor-covenant.org/version/1/2/0/). diff --git a/LICENSE b/LICENSE index a4c4948f4..5b31bdb92 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -flutter-mapbox-gl copyright (c) 2018, Mapbox. +flutter-mapbox-gl copyright (c) 2021, Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 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 1c4075d17..112b4c2fd 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,120 @@ # Flutter Mapbox GL -> **Please note that this project is community driven and is not an official Mapbox product.** We welcome [feedback](https://github.com/tobrun/flutter-mapbox-gl/issues) and contributions. +> **Please note that this project is community driven and is not an official Mapbox product.** +> +> We welcome [feedback](https://github.com/tobrun/flutter-mapbox-gl/issues) and contributions. + + +## Table of contents + +- [Flutter Mapbox GL](#flutter-mapbox-gl) + - [Table of contents](#table-of-contents) + - [Introduction](#introduction) + - [Setting up](#setting-up) + - [Mobile](#mobile) + - [Secret Mapbox access token](#secret-mapbox-access-token) + - [Web](#web) + - [All platforms](#all-platforms) + - [Public Mapbox access token](#public-mapbox-access-token) + - [Supported API](#supported-api) + - [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) + - [Running the example code](#running-the-example-code) + - [Contributing](#contributing) + +## Introduction This Flutter plugin allows to show embedded interactive and customizable vector maps inside a Flutter widget. For the Android and iOS integration, we use [mapbox-gl-native](https://github.com/mapbox/mapbox-gl-native). For web, we rely on [mapbox-gl-js](https://github.com/mapbox/mapbox-gl-js). This project only supports a subset of the API exposed by these libraries. ![screenshot.png](screenshot.png) -## Running the example app +## Setting up -- Install [Flutter](https://flutter.io/get-started/) and validate its installation with `flutter doctor` -- Clone the repository with `git clone git@github.com:tobrun/flutter-mapbox-gl.git` -- Add a public Mapbox access token to the example app (see next section) -- Add a secret Mapbox access token for downloading the SDK -- Connect a mobile device or start an emulator, simulator or chrome -- Locate the id of a the device with `flutter devices` -- Run the app with `cd flutter_mapbox/example && flutter packages get && flutter run -d {device_id}` +This package is available on [pub.dev](https://pub.dev/packages/mapbox_gl). -## Adding a Mapbox Access Token +Get it by running the following command: -This project uses Mapbox vector tiles, which requires a Mapbox account and a Mapbox access token. Obtain a free access token on [your Mapbox account page](https://www.mapbox.com/account/access-tokens/). -> **Even if you do not use Mapbox vector tiles but vector tiles from a different source (like self-hosted tiles) with this plugin, you will need to specify any non-empty string as Access Token as explained below!** +``` +flutter pub add mapbox_gl +``` +### Mobile -The **recommended** way to provide your access token is through the `MapboxMap` constructor's `accessToken` parameter, which is available starting from the v0.8 release. Note that you should always use the same token throughout your entire app. +#### Secret Mapbox access token -An alternative method to provide access tokens that was required until the v0.7 release is described in [this wiki article](https://github.com/tobrun/flutter-mapbox-gl/wiki/Mapbox-access-tokens). +A secret access token with the `Downloads: Read` scope is required for the underlying Mapbox SDKs to be downloaded. +Information on setting it up is available in the Mapbox documentation: +[Android](https://docs.mapbox.com/android/maps/guides/install/), +[iOS](https://docs.mapbox.com/ios/maps/guides/install/). -### SDK Download token +If the properly configured token is not present, +the build process fails with one the following errors *(for Android/iOS respectively)*: -You must also [configure a secret access token having the Download: read -scope][https://docs.mapbox.com/ios/maps/guides/install/]. If this configuration -is not present, an error like the following appears during the iOS build. +``` +* What went wrong: +A problem occurred evaluating project ':mapbox_gl'. +> SDK Registry token is null. See README.md for more information. +``` ``` [!] Error installing Mapbox-iOS-SDK curl: (22) The requested URL returned error: 401 Unauthorized ``` -## Avoid Android UnsatisfiedLinkError +### Web -Update buildTypes in `android\app\build.gradle` +Include the JavaScript and CSS files in the `` of your `index.html` file: -```gradle -buildTypes { - release { - // other configs - ndk { - abiFilters 'armeabi-v7a','arm64-v8a','x86_64', 'x86' - } +``` + + + + ``` -## Using the SDK in your project +*Note: Look for latest version in [Mapbox GL JS documentation](https://docs.mapbox.com/mapbox-gl-js/guides/).* + +### All platforms + +#### Public Mapbox access token -This project is available on [pub.dev](https://pub.dev/packages/mapbox_gl), follow the [instructions](https://flutter.dev/docs/development/packages-and-plugins/using-packages#adding-a-package-dependency-to-an-app) to integrate a package into your flutter application. For platform specific integration, use the flutter application under the example folder as reference. +A public access token must be provided to a MapboxMap widget for retrieving styles and resources. +While you can hardcode it directly into source files, +it's good practise to retrieve access tokens from some external source +(e.g. a config file or an environment variable). +The example app uses the following technique: + +The access token is passed via the command line arguments when either building + +``` +flutter build --dart-define ACCESS_TOKEN=YOUR_TOKEN_HERE +``` + +or running the application + +``` +flutter run --dart-define ACCESS_TOKEN=YOUR_TOKEN_HERE +``` + +Then it's retrieved in Dart: +``` +MapboxMap( + ... + accessToken: const String.fromEnvironment("ACCESS_TOKEN"), + ... +) +``` ## Supported API @@ -64,10 +124,24 @@ This project is available on [pub.dev](https://pub.dev/packages/mapbox_gl), foll | Camera | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Gesture | :white_check_mark: | :white_check_mark: | :white_check_mark: | | User Location | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Symbol | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Circle | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Line | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Fill | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Style DSL | :x: | :x: | :x: | +| Raster Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Symbol Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| 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 | :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: | +| Image Source | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Expressions | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Symbol Annotation | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Circle Annotation | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Line Annotation | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Fill Annotation | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Map Styles @@ -80,7 +154,7 @@ Map styles can be supplied by setting the `styleString` in the `MapOptions`. The ## Offline Sideloading -Support for offline maps is available by *"side loading"* the required map tiles and including them in your `assets` folder. +Support for offline maps is available by side loading the required map tiles and including them in your `assets` folder. * Create your tiles package by following the guide available [here](https://docs.mapbox.com/ios/maps/overview/offline/). @@ -134,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 @@ -158,19 +250,14 @@ xml ... [Your explanation here] ``` -Mapbox [recommends](https://docs.mapbox.com/help/tutorials/first-steps-ios-sdk/#display-the-users-location) the explanation "Shows your location on the map and helps improve the map". - -## Documentation - -This README file currently houses all of the documentation for this Flutter project. Please visit [mapbox.com/android-docs](https://www.mapbox.com/android-docs/) if you'd like more information about the Mapbox Maps SDK for Android and [mapbox.com/ios-sdk](https://www.mapbox.com/ios-sdk/) for more information about the Mapbox Maps SDK for iOS. - -## Getting Help +[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". -- **Need help with your code?**: Look for previous questions on the [#mapbox tag](https://stackoverflow.com/questions/tagged/mapbox+flutter) — or [ask a new question](https://stackoverflow.com/questions/tagged/mapbox+android). -- **Have a bug to report?** [Open an issue](https://github.com/tobrun/flutter-mapbox-gl/issues/new). If possible, include a full log and information which shows the issue. -- **Have a feature request?** [Open an issue](https://github.com/tobrun/flutter-mapbox-gl/issues/new). Tell us what the feature should do and why you want the feature. +## 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) ## Contributing -We welcome contributions to this repository! If you're interested in helping build this Mapbox/Flutter integration, please read [the contribution guide](https://github.com/tobrun/flutter-mapbox-gl/blob/master/CONTRIBUTING.md) to learn how to get started. +We welcome contributions to this repository! If you're interested in helping build this Mapbox-Flutter integration, please read [the contribution guide](https://github.com/tobrun/flutter-mapbox-gl/blob/master/CONTRIBUTING.md) to learn how to get started. diff --git a/RELEASE.md b/RELEASE.md index 2fb3ffff1..488c2d8f7 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,20 +2,28 @@ This document describes the steps needed to make a release: +### Update Changelog + For each supported library: - `mapbox_gl_platform_interface` - `mapbox_gl_web` - `flutter-mapbox-gl` -Perform the following actions: - - Update `CHANGELOG.md` with the commits associated since previous release. - - Update library version in `pubspec.yaml` +Update the changelog by listing the commits that occurred for that given library. +Starting with `flutter-mapbox-gl` allows you to capture them all and be more granular +when updating the other libraries. Once the CHANGELOG.md's are updated, make a PR +and merge to master. + +### Release libraries + +#### Release `mapbox_gl_platform_interface` + +Update library version in `mapbox_gl_platform_interface/pubspec.yaml` and run `flutter pub publish`. + +#### Release `mapbox_gl_web` -Publish `mapbox_gl_platform_interface`, `mapbox_gl_web` and `flutter-mapbox-gl` after eachother with: - - `flutter pub publish` +Update library version in `mapbox_gl_web/pubspec.yaml` in `mapbox_gl_platform_interface`, -Before publishgin in `mapbox_gl_web` update the version of the `mapbox_gl_platform_interface` -Repeat this action for `flutter-mapbox-gl` for both dependencies: ``` Replace: @@ -33,4 +41,13 @@ dependency_overrides: path: ../mapbox_gl_platform_interface ``` -Before being able to publish `flutter-mapbox-gl`, you will have to PR and merge changelog changes. \ No newline at end of file +and run `flutter pub publish` in `mapbox_gl_web`. + +#### Release `flutter-mapbox-gl` + +Update library version in `pubspec.yaml`, replace both web as platform interface conform to above and run `flutter pub publish` from root of project. + +### Tag Release + +Once the PR that updates version numbers is merged, create a release for that version number +with the contents of the root CHANGELOG.md. \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index e6b13d711..39bacb48e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,22 +4,21 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.2.0' } } rootProject.allprojects { - def token = System.getenv('SDK_REGISTRY_TOKEN') + def token = System.getenv('SDK_REGISTRY_TOKEN') ?: project.properties['MAPBOX_DOWNLOADS_TOKEN'] if (token == null || token.empty) { throw new Exception("SDK Registry token is null. See README.md for more information.") } repositories { google() - jcenter() mavenCentral() maven { url 'https://api.mapbox.com/downloads/v2/releases/maven' @@ -37,7 +36,9 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + namespace "com.mapbox.mapboxgl" + compileSdkVersion 31 + ndkVersion "20.1.5948944" defaultConfig { minSdkVersion 20 @@ -52,10 +53,12 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } dependencies { - implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:9.6.0" + implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:9.6.2" implementation "com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9:0.9.0" 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 new file mode 100644 index 000000000..8428c0be2 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +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 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/CircleBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java deleted file mode 100644 index db7accf56..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java +++ /dev/null @@ -1,76 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Circle; -import com.mapbox.mapboxsdk.plugins.annotation.CircleManager; -import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions; - -class CircleBuilder implements CircleOptionsSink { - private final CircleManager circleManager; - private final CircleOptions circleOptions; - - CircleBuilder(CircleManager circleManager) { - this.circleManager = circleManager; - this.circleOptions = new CircleOptions(); - } - - public CircleOptions getCircleOptions(){ - return this.circleOptions; - } - - Circle build() { - return circleManager.create(circleOptions); - } - - @Override - public void setCircleRadius(float circleRadius) { - circleOptions.withCircleRadius(circleRadius); - } - - @Override - public void setCircleColor(String circleColor) { - circleOptions.withCircleColor(circleColor); - } - - @Override - public void setCircleBlur(float circleBlur) { - circleOptions.withCircleBlur(circleBlur); - } - - @Override - public void setCircleOpacity(float circleOpacity) { - circleOptions.withCircleOpacity(circleOpacity); - } - - @Override - public void setCircleStrokeWidth(float circleStrokeWidth) { - circleOptions.withCircleStrokeWidth(circleStrokeWidth); - } - - @Override - public void setCircleStrokeColor(String circleStrokeColor) { - circleOptions.withCircleStrokeColor(circleStrokeColor); - } - - @Override - public void setCircleStrokeOpacity(float circleStrokeOpacity) { - circleOptions.withCircleStrokeOpacity(circleStrokeOpacity); - } - - @Override - public void setGeometry(LatLng geometry) { - circleOptions.withGeometry(Point.fromLngLat(geometry.getLongitude(), geometry.getLatitude())); - } - - @Override - public void setDraggable(boolean draggable) { - circleOptions.withDraggable(draggable); - } -} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/CircleController.java b/android/src/main/java/com/mapbox/mapboxgl/CircleController.java deleted file mode 100644 index 76860ea6f..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/CircleController.java +++ /dev/null @@ -1,96 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import android.graphics.Color; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Circle; -import com.mapbox.mapboxsdk.plugins.annotation.CircleManager; - -/** Controller of a single Circle on the map. */ -class CircleController implements CircleOptionsSink { - private final Circle circle; - private final OnCircleTappedListener onTappedListener; - private boolean consumeTapEvents; - - CircleController(Circle circle, boolean consumeTapEvents, OnCircleTappedListener onTappedListener) { - this.circle = circle; - this.consumeTapEvents = consumeTapEvents; - this.onTappedListener = onTappedListener; - } - - public Circle getCircle(){ - return this.circle; - } - - boolean onTap() { - if (onTappedListener != null) { - onTappedListener.onCircleTapped(circle); - } - return consumeTapEvents; - } - - void remove(CircleManager circleManager) { - circleManager.delete(circle); - } - - @Override - public void setCircleRadius(float circleRadius) { - circle.setCircleRadius(circleRadius); - } - - @Override - public void setCircleColor(String circleColor) { - circle.setCircleColor(Color.parseColor(circleColor)); - } - - @Override - public void setCircleBlur(float circleBlur) { - circle.setCircleBlur(circleBlur); - } - - @Override - public void setCircleOpacity(float circleOpacity) { - circle.setCircleOpacity(circleOpacity); - } - - @Override - public void setCircleStrokeWidth(float circleStrokeWidth) { - circle.setCircleStrokeWidth(circleStrokeWidth); - } - - @Override - public void setCircleStrokeColor(String circleStrokeColor) { - circle.setCircleStrokeColor(Color.parseColor(circleStrokeColor)); - } - - @Override - public void setCircleStrokeOpacity(float circleStrokeOpacity) { - circle.setCircleStrokeOpacity(circleStrokeOpacity); - } - - @Override - public void setGeometry(LatLng geometry) { - circle.setGeometry(Point.fromLngLat(geometry.getLongitude(), geometry.getLatitude())); - } - - public LatLng getGeometry() { - Point point = circle.getGeometry(); - return new LatLng(point.latitude(), point.longitude()); - } - - @Override - public void setDraggable(boolean draggable) { - circle.setDraggable(draggable); - } - - public void update(CircleManager circleManager) { - circleManager.update(circle); - } - -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/CircleOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/CircleOptionsSink.java deleted file mode 100644 index 80bd8ea04..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/CircleOptionsSink.java +++ /dev/null @@ -1,31 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.geometry.LatLng; - -/** Receiver of Circle configuration options. */ -interface CircleOptionsSink { - - void setCircleRadius(float circleRadius); - - void setCircleColor(String circleColor); - - void setCircleBlur(float circleBlur); - - void setCircleOpacity(float circleOpacity); - - void setCircleStrokeWidth(float circleStrokeWidth); - - void setCircleStrokeColor(String circleStrokeColor); - - void setCircleStrokeOpacity(float circleStrokeOpacity); - - void setGeometry(LatLng geometry); - - void setDraggable(boolean draggable); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/Convert.java b/android/src/main/java/com/mapbox/mapboxgl/Convert.java index ca22c4b54..f22b9d068 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/Convert.java +++ b/android/src/main/java/com/mapbox/mapboxgl/Convert.java @@ -4,52 +4,28 @@ package com.mapbox.mapboxgl; +import android.content.Context; import android.graphics.Point; +import android.util.DisplayMetrics; import com.mapbox.geojson.Polygon; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdate; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.log.Logger; import com.mapbox.mapboxsdk.maps.MapboxMap; - import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * Conversions between JSON-like values and MapboxMaps data types. - */ +/** Conversions between JSON-like values and MapboxMaps data types. */ class Convert { - private final static String TAG = "Convert"; - -// private static BitmapDescriptor toBitmapDescriptor(Object o) { -// final List data = toList(o); -// switch (toString(data.get(0))) { -// case "defaultMarker": -// if (data.size() == 1) { -// return BitmapDescriptorFactory.defaultMarker(); -// } else { -// return BitmapDescriptorFactory.defaultMarker(toFloat(data.get(1))); -// } -// case "fromAsset": -// if (data.size() == 2) { -// return BitmapDescriptorFactory.fromAsset( -// FlutterMain.getLookupKeyForAsset(toString(data.get(1)))); -// } else { -// return BitmapDescriptorFactory.fromAsset( -// FlutterMain.getLookupKeyForAsset(toString(data.get(1)), toString(data.get(2)))); -// } -// default: -// throw new IllegalArgumentException("Cannot interpret " + o + " as BitmapDescriptor"); -// } -// } + private static final String TAG = "Convert"; - private static boolean toBoolean(Object o) { + static boolean toBoolean(Object o) { return (Boolean) o; } @@ -63,19 +39,6 @@ static CameraPosition toCameraPosition(Object o) { return builder.build(); } - static List toAnnotationOrder(Object o) { - final List data = toList(o); - List annotations = new ArrayList(); - for (int index = 0; index < data.size(); index++) { - annotations.add(toString(data.get(index))); - } - return annotations; - } - - static List toAnnotationConsumeTapEvents(Object o) { - return toAnnotationOrder(o); - } - static boolean isScrollByCameraUpdate(Object o) { return toString(toList(o).get(0)).equals("scrollBy"); } @@ -88,15 +51,17 @@ static CameraUpdate toCameraUpdate(Object o, MapboxMap mapboxMap, float density) case "newLatLng": return CameraUpdateFactory.newLatLng(toLatLng(data.get(1))); case "newLatLngBounds": - return CameraUpdateFactory.newLatLngBounds(toLatLngBounds(data.get(1)), toPixels(data.get(2), density), - toPixels(data.get(3), density), toPixels(data.get(4), density), toPixels(data.get(5), density)); + return CameraUpdateFactory.newLatLngBounds( + toLatLngBounds(data.get(1)), + toPixels(data.get(2), density), + toPixels(data.get(3), density), + toPixels(data.get(4), density), + toPixels(data.get(5), density)); case "newLatLngZoom": return CameraUpdateFactory.newLatLngZoom(toLatLng(data.get(1)), toFloat(data.get(2))); case "scrollBy": mapboxMap.scrollBy( - toFractionalPixels(data.get(1), density), - toFractionalPixels(data.get(2), density) - ); + toFractionalPixels(data.get(1), density), toFractionalPixels(data.get(2), density)); return null; case "zoomBy": if (data.size() == 2) { @@ -119,15 +84,15 @@ static CameraUpdate toCameraUpdate(Object o, MapboxMap mapboxMap, float density) } } - private static double toDouble(Object o) { + static double toDouble(Object o) { return ((Number) o).doubleValue(); } - private static float toFloat(Object o) { + static float toFloat(Object o) { return ((Number) o).floatValue(); } - private static Float toFloatWrapper(Object o) { + static Float toFloatWrapper(Object o) { return (o == null) ? null : toFloat(o); } @@ -151,12 +116,12 @@ private static Object toJson(LatLng latLng) { return Arrays.asList(latLng.getLatitude(), latLng.getLongitude()); } - private static LatLng toLatLng(Object o) { + static LatLng toLatLng(Object o) { final List data = toList(o); return new LatLng(toDouble(data.get(0)), toDouble(data.get(1))); } - private static LatLngBounds toLatLngBounds(Object o) { + static LatLngBounds toLatLngBounds(Object o) { if (o == null) { return null; } @@ -168,15 +133,19 @@ private static LatLngBounds toLatLngBounds(Object o) { return builder.build(); } - static List toLatLngList(Object o) { + static List toLatLngList(Object o, boolean flippedOrder) { if (o == null) { return null; } final List data = toList(o); List latLngList = new ArrayList<>(); - for (int i=0; i< data.size(); i++) { final List coords = toList(data.get(i)); - latLngList.add(new LatLng(toDouble(coords.get(0)), toDouble(coords.get(1)))); + if (flippedOrder) { + latLngList.add(new LatLng(toDouble(coords.get(1)), toDouble(coords.get(0)))); + } else { + latLngList.add(new LatLng(toDouble(coords.get(0)), toDouble(coords.get(1)))); + } } return latLngList; } @@ -188,7 +157,7 @@ private static List> toLatLngListList(Object o) { final List data = toList(o); List> latLngListList = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { - List latLngList = toLatLngList(data.get(i)); + List latLngList = toLatLngList(data.get(i), false); latLngListList.add(latLngList); } return latLngListList; @@ -199,14 +168,15 @@ static Polygon interpretListLatLng(List> geometry) { for (List innerGeometry : geometry) { List innerPoints = new ArrayList<>(innerGeometry.size()); for (LatLng latLng : innerGeometry) { - innerPoints.add(com.mapbox.geojson.Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude())); + innerPoints.add( + com.mapbox.geojson.Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude())); } points.add(innerPoints); } return Polygon.fromLngLats(points); } - private static List toList(Object o) { + static List toList(Object o) { return (List) o; } @@ -231,11 +201,12 @@ private static Point toPoint(Object o, float density) { return new Point(toPixels(data.get(0), density), toPixels(data.get(1), density)); } - private static String toString(Object o) { + static String toString(Object o) { return (String) o; } - static void interpretMapboxMapOptions(Object o, MapboxMapOptionsSink sink) { + static void interpretMapboxMapOptions(Object o, MapboxMapOptionsSink sink, Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final Map data = toMap(o); final Object cameraTargetBounds = data.get("cameraTargetBounds"); if (cameraTargetBounds != null) { @@ -254,8 +225,8 @@ static void interpretMapboxMapOptions(Object o, MapboxMapOptionsSink sink) { if (minMaxZoomPreference != null) { final List zoomPreferenceData = toList(minMaxZoomPreference); sink.setMinMaxZoomPreference( // - toFloatWrapper(zoomPreferenceData.get(0)), // - toFloatWrapper(zoomPreferenceData.get(1))); + toFloatWrapper(zoomPreferenceData.get(0)), // + toFloatWrapper(zoomPreferenceData.get(1))); } final Object rotateGesturesEnabled = data.get("rotateGesturesEnabled"); if (rotateGesturesEnabled != null) { @@ -290,260 +261,30 @@ static void interpretMapboxMapOptions(Object o, MapboxMapOptionsSink sink) { sink.setMyLocationRenderMode(toInt(myLocationRenderMode)); } final Object logoViewMargins = data.get("logoViewMargins"); - if(logoViewMargins != null){ + if (logoViewMargins != null) { final List logoViewMarginsData = toList(logoViewMargins); - sink.setLogoViewMargins(toInt(logoViewMarginsData.get(0)), toInt(logoViewMarginsData.get(1))); + final Point point = toPoint(logoViewMarginsData, metrics.density); + sink.setLogoViewMargins(point.x, point.y); } final Object compassGravity = data.get("compassViewPosition"); - if(compassGravity != null){ + if (compassGravity != null) { sink.setCompassGravity(toInt(compassGravity)); } final Object compassViewMargins = data.get("compassViewMargins"); - if(compassViewMargins != null){ + if (compassViewMargins != null) { final List compassViewMarginsData = toList(compassViewMargins); - sink.setCompassViewMargins(toInt(compassViewMarginsData.get(0)), toInt(compassViewMarginsData.get(1))); + final Point point = toPoint(compassViewMarginsData, metrics.density); + sink.setCompassViewMargins(point.x, point.y); + } + final Object attributionButtonGravity = data.get("attributionButtonPosition"); + if (attributionButtonGravity != null) { + sink.setAttributionButtonGravity(toInt(attributionButtonGravity)); } final Object attributionButtonMargins = data.get("attributionButtonMargins"); - if(attributionButtonMargins != null){ + if (attributionButtonMargins != null) { final List attributionButtonMarginsData = toList(attributionButtonMargins); - sink.setAttributionButtonMargins(toInt(attributionButtonMarginsData.get(0)), toInt(attributionButtonMarginsData.get(1))); - } - } - - static void interpretSymbolOptions(Object o, SymbolOptionsSink sink) { - final Map data = toMap(o); - final Object iconSize = data.get("iconSize"); - if (iconSize != null) { - sink.setIconSize(toFloat(iconSize)); - } - final Object iconImage = data.get("iconImage"); - if (iconImage != null) { - sink.setIconImage(toString(iconImage)); - } - final Object iconRotate = data.get("iconRotate"); - if (iconRotate != null) { - sink.setIconRotate(toFloat(iconRotate)); - } - final Object iconOffset = data.get("iconOffset"); - if (iconOffset != null) { - sink.setIconOffset(new float[] {toFloat(toList(iconOffset).get(0)), toFloat(toList(iconOffset).get(1))}); - } - final Object iconAnchor = data.get("iconAnchor"); - if (iconAnchor != null) { - sink.setIconAnchor(toString(iconAnchor)); - } - final ArrayList fontNames = (ArrayList) data.get("fontNames"); - if (fontNames != null) { - sink.setFontNames((String[]) fontNames.toArray(new String[0])); - } - final Object textField = data.get("textField"); - if (textField != null) { - sink.setTextField(toString(textField)); - } - final Object textSize = data.get("textSize"); - if (textSize != null) { - sink.setTextSize(toFloat(textSize)); - } - final Object textMaxWidth = data.get("textMaxWidth"); - if (textMaxWidth != null) { - sink.setTextMaxWidth(toFloat(textMaxWidth)); - } - final Object textLetterSpacing = data.get("textLetterSpacing"); - if (textLetterSpacing != null) { - sink.setTextLetterSpacing(toFloat(textLetterSpacing)); - } - final Object textJustify = data.get("textJustify"); - if (textJustify != null) { - sink.setTextJustify(toString(textJustify)); - } - final Object textAnchor = data.get("textAnchor"); - if (textAnchor != null) { - sink.setTextAnchor(toString(textAnchor)); - } - final Object textRotate = data.get("textRotate"); - if (textRotate != null) { - sink.setTextRotate(toFloat(textRotate)); - } - final Object textTransform = data.get("textTransform"); - if (textTransform != null) { - sink.setTextTransform(toString(textTransform)); - } - final Object textOffset = data.get("textOffset"); - if (textOffset != null) { - sink.setTextOffset(new float[] {toFloat(toList(textOffset).get(0)), toFloat(toList(textOffset).get(1))}); - } - final Object iconOpacity = data.get("iconOpacity"); - if (iconOpacity != null) { - sink.setIconOpacity(toFloat(iconOpacity)); - } - final Object iconColor = data.get("iconColor"); - if (iconColor != null) { - sink.setIconColor(toString(iconColor)); - } - final Object iconHaloColor = data.get("iconHaloColor"); - if (iconHaloColor != null) { - sink.setIconHaloColor(toString(iconHaloColor)); - } - final Object iconHaloWidth = data.get("iconHaloWidth"); - if (iconHaloWidth != null) { - sink.setIconHaloWidth(toFloat(iconHaloWidth)); - } - final Object iconHaloBlur = data.get("iconHaloBlur"); - if (iconHaloBlur != null) { - sink.setIconHaloBlur(toFloat(iconHaloBlur)); - } - final Object textOpacity = data.get("textOpacity"); - if (textOpacity != null) { - sink.setTextOpacity(toFloat(textOpacity)); - } - final Object textColor = data.get("textColor"); - if (textColor != null) { - sink.setTextColor(toString(textColor)); - } - final Object textHaloColor = data.get("textHaloColor"); - if (textHaloColor != null) { - sink.setTextHaloColor(toString(textHaloColor)); - } - final Object textHaloWidth = data.get("textHaloWidth"); - if (textHaloWidth != null) { - sink.setTextHaloWidth(toFloat(textHaloWidth)); - } - final Object textHaloBlur = data.get("textHaloBlur"); - if (textHaloBlur != null) { - sink.setTextHaloBlur(toFloat(textHaloBlur)); - } - final Object geometry = data.get("geometry"); - if (geometry != null) { - sink.setGeometry(toLatLng(geometry)); - } - final Object symbolSortKey = data.get("zIndex"); - if (symbolSortKey != null) { - sink.setSymbolSortKey(toFloat(symbolSortKey)); - } - final Object draggable = data.get("draggable"); - if (draggable != null) { - sink.setDraggable(toBoolean(draggable)); - } - } - - static void interpretCircleOptions(Object o, CircleOptionsSink sink) { - final Map data = toMap(o); - final Object circleRadius = data.get("circleRadius"); - if (circleRadius != null) { - sink.setCircleRadius(toFloat(circleRadius)); - } - final Object circleColor = data.get("circleColor"); - if (circleColor != null) { - sink.setCircleColor(toString(circleColor)); - } - final Object circleBlur = data.get("circleBlur"); - if (circleBlur != null) { - sink.setCircleBlur(toFloat(circleBlur)); - } - final Object circleOpacity = data.get("circleOpacity"); - if (circleOpacity != null) { - sink.setCircleOpacity(toFloat(circleOpacity)); - } - final Object circleStrokeWidth = data.get("circleStrokeWidth"); - if (circleStrokeWidth != null) { - sink.setCircleStrokeWidth(toFloat(circleStrokeWidth)); - } - final Object circleStrokeColor = data.get("circleStrokeColor"); - if (circleStrokeColor != null) { - sink.setCircleStrokeColor(toString(circleStrokeColor)); - } - final Object circleStrokeOpacity = data.get("circleStrokeOpacity"); - if (circleStrokeOpacity != null) { - sink.setCircleStrokeOpacity(toFloat(circleStrokeOpacity)); - } - final Object geometry = data.get("geometry"); - if (geometry != null) { - sink.setGeometry(toLatLng(geometry)); - } - final Object draggable = data.get("draggable"); - if (draggable != null) { - sink.setDraggable(toBoolean(draggable)); - } - } - static void interpretLineOptions(Object o, LineOptionsSink sink) { - final Map data = toMap(o); - final Object lineJoin = data.get("lineJoin"); - if (lineJoin != null) { - Logger.e(TAG, "setLineJoin" + lineJoin); - sink.setLineJoin(toString(lineJoin)); - } - final Object lineOpacity = data.get("lineOpacity"); - if (lineOpacity != null) { - Logger.e(TAG, "setLineOpacity" + lineOpacity); - sink.setLineOpacity(toFloat(lineOpacity)); - } - final Object lineColor = data.get("lineColor"); - if (lineColor != null) { - Logger.e(TAG, "setLineColor" + lineColor); - sink.setLineColor(toString(lineColor)); - } - final Object lineWidth = data.get("lineWidth"); - if (lineWidth != null) { - Logger.e(TAG, "setLineWidth" + lineWidth); - sink.setLineWidth(toFloat(lineWidth)); - } - final Object lineGapWidth = data.get("lineGapWidth"); - if (lineGapWidth != null) { - Logger.e(TAG, "setLineGapWidth" + lineGapWidth); - sink.setLineGapWidth(toFloat(lineGapWidth)); - } - final Object lineOffset = data.get("lineOffset"); - if (lineOffset != null) { - Logger.e(TAG, "setLineOffset" + lineOffset); - sink.setLineOffset(toFloat(lineOffset)); - } - final Object lineBlur = data.get("lineBlur"); - if (lineBlur != null) { - Logger.e(TAG, "setLineBlur" + lineBlur); - sink.setLineBlur(toFloat(lineBlur)); - } - final Object linePattern = data.get("linePattern"); - if (linePattern != null) { - Logger.e(TAG, "setLinePattern" + linePattern); - sink.setLinePattern(toString(linePattern)); - } - final Object geometry = data.get("geometry"); - if (geometry != null) { - Logger.e(TAG, "SetGeometry"); - sink.setGeometry(toLatLngList(geometry)); - } - final Object draggable = data.get("draggable"); - if (draggable != null) { - Logger.e(TAG, "SetDraggable"); - sink.setDraggable(toBoolean(draggable)); - } - } - - static void interpretFillOptions(Object o, FillOptionsSink sink) { - final Map data = toMap(o); - final Object fillOpacity = data.get("fillOpacity"); - if (fillOpacity != null) { - sink.setFillOpacity(toFloat(fillOpacity)); - } - final Object fillColor = data.get("fillColor"); - if (fillColor != null) { - sink.setFillColor(toString(fillColor)); - } - final Object fillOutlineColor = data.get("fillOutlineColor"); - if (fillOutlineColor != null) { - sink.setFillOutlineColor(toString(fillOutlineColor)); - } - final Object fillPattern = data.get("fillPattern"); - if (fillPattern != null) { - sink.setFillPattern(toString(fillPattern)); - } - final Object geometry = data.get("geometry"); - if (geometry != null) { - sink.setGeometry(toLatLngListList(geometry)); - } - final Object draggable = data.get("draggable"); - if (draggable != null) { - sink.setDraggable(toBoolean(draggable)); + final Point point = toPoint(attributionButtonMarginsData, metrics.density); + sink.setAttributionButtonMargins(point.x, point.y); } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java deleted file mode 100644 index b70589cba..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java +++ /dev/null @@ -1,62 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Fill; -import com.mapbox.mapboxsdk.plugins.annotation.FillManager; -import com.mapbox.mapboxsdk.plugins.annotation.FillOptions; - -import java.util.List; - -class FillBuilder implements FillOptionsSink { - private final FillManager fillManager; - private final FillOptions fillOptions; - - FillBuilder(FillManager fillManager) { - this.fillManager = fillManager; - this.fillOptions = new FillOptions(); - } - - public FillOptions getFillOptions(){ - return this.fillOptions; - } - - Fill build() { - return fillManager.create(fillOptions); - } - - @Override - public void setFillOpacity(float fillOpacity) { - fillOptions.withFillOpacity(fillOpacity); - } - - @Override - public void setFillColor(String fillColor) { - fillOptions.withFillColor(fillColor); - } - - @Override - public void setFillOutlineColor(String fillOutlineColor) { - fillOptions.withFillOutlineColor(fillOutlineColor); - } - - @Override - public void setFillPattern(String fillPattern) { - fillOptions.withFillPattern(fillPattern); - } - - @Override - public void setGeometry(List> geometry) { - fillOptions.withGeometry(Convert.interpretListLatLng(geometry)); - } - - @Override - public void setDraggable(boolean draggable) { - fillOptions.withDraggable(draggable); - } -} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/FillController.java b/android/src/main/java/com/mapbox/mapboxgl/FillController.java deleted file mode 100644 index 86bc01137..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/FillController.java +++ /dev/null @@ -1,78 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import android.graphics.Color; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Fill; -import com.mapbox.mapboxsdk.plugins.annotation.FillManager; - -import java.util.List; - -/** - * Controller of a single Fill on the map. - */ -class FillController implements FillOptionsSink { - private final Fill fill; - private final OnFillTappedListener onTappedListener; - private boolean consumeTapEvents; - - FillController(Fill fill, boolean consumeTapEvents, OnFillTappedListener onTappedListener) { - this.fill = fill; - this.consumeTapEvents = consumeTapEvents; - this.onTappedListener = onTappedListener; - } - - public Fill getFill(){ - return this.fill; - } - - boolean onTap() { - if (onTappedListener != null) { - onTappedListener.onFillTapped(fill); - } - return consumeTapEvents; - } - - void remove(FillManager fillManager) { - fillManager.delete(fill); - } - - @Override - public void setFillOpacity(float fillOpacity) { - fill.setFillOpacity(fillOpacity); - } - - @Override - public void setFillColor(String fillColor) { - fill.setFillColor(Color.parseColor(fillColor)); - } - - @Override - public void setFillOutlineColor(String fillOutlineColor) { - fill.setFillOutlineColor(Color.parseColor(fillOutlineColor)); - } - - @Override - public void setFillPattern(String fillPattern) { - fill.setFillPattern(fillPattern); - } - - @Override - public void setGeometry(List> geometry) { - fill.setGeometry(Convert.interpretListLatLng(geometry)); - } - - @Override - public void setDraggable(boolean draggable) { - fill.setDraggable(draggable); - } - - public void update(FillManager fillManager) { - fillManager.update(fill); - } -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/FillOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/FillOptionsSink.java deleted file mode 100644 index 849788103..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/FillOptionsSink.java +++ /dev/null @@ -1,27 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.geometry.LatLng; - -import java.util.List; - -/** Receiver of Fill configuration options. */ -interface FillOptionsSink { - - void setFillOpacity(float fillOpacity); - - void setFillColor(String fillColor); - - void setFillOutlineColor(String fillOutlineColor); - - void setFillPattern(String fillPattern); - - void setGeometry(List> geometry); - - void setDraggable(boolean draggable); -} 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 1b95f47f5..771fd8a28 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java +++ b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java @@ -2,14 +2,14 @@ import android.content.Context; import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.mapbox.mapboxsdk.net.ConnectivityReceiver; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import com.mapbox.mapboxsdk.net.ConnectivityReceiver; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -20,139 +20,127 @@ import java.io.OutputStream; import java.util.Map; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; - class GlobalMethodHandler implements MethodChannel.MethodCallHandler { - private static final String TAG = GlobalMethodHandler.class.getSimpleName(); - private static final String DATABASE_NAME = "mbgl-offline.db"; - private static final int BUFFER_SIZE = 1024 * 2; - - @Nullable - private PluginRegistry.Registrar registrar; - @Nullable - private FlutterPlugin.FlutterAssets flutterAssets; - @NonNull - private final Context context; - @NonNull - private final BinaryMessenger messenger; - - 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(); - this.messenger = binding.getBinaryMessenger(); - } - - @Override - public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { - String accessToken = methodCall.argument("accessToken"); - MapBoxUtils.getMapbox(context, accessToken); - - switch (methodCall.method) { - case "installOfflineMapTiles": - String tilesDb = methodCall.argument("tilesdb"); - installOfflineMapTiles(tilesDb); - result.success(null); - break; - case "setOffline": - boolean offline = methodCall.argument("offline"); - ConnectivityReceiver.instance(context).setConnected(offline ? false : null); - result.success(null); - break; - case "mergeOfflineRegions": - OfflineManagerUtils.mergeRegions(result, context, methodCall.argument("path")); - break; - case "setOfflineTileCountLimit": - OfflineManagerUtils.setOfflineTileCountLimit(result, context, methodCall.argument("limit").longValue()); - break; - case "downloadOfflineRegion": - // Get args from caller - Map definitionMap = (Map) methodCall.argument("definition"); - Map metadataMap = (Map) methodCall.argument("metadata"); - String channelName = methodCall.argument("channelName"); - - // Prepare args - OfflineChannelHandlerImpl channelHandler = new OfflineChannelHandlerImpl(messenger, channelName); - - // Start downloading - OfflineManagerUtils.downloadRegion(result, context, definitionMap, metadataMap, channelHandler); - break; - case "getListOfRegions": - OfflineManagerUtils.regionsList(result, context); - break; - case "updateOfflineRegionMetadata": - // Get download region arguments from caller - Map metadata = (Map) methodCall.argument("metadata"); - OfflineManagerUtils.updateRegionMetadata(result, context, methodCall.argument("id").longValue(), metadata); - break; - case "deleteOfflineRegion": - OfflineManagerUtils.deleteRegion(result, context, methodCall.argument("id").longValue()); - break; - default: - result.notImplemented(); - break; - } + private static final String TAG = GlobalMethodHandler.class.getSimpleName(); + private static final String DATABASE_NAME = "mbgl-offline.db"; + private static final int BUFFER_SIZE = 1024 * 2; + @NonNull private final Context context; + @NonNull private final BinaryMessenger messenger; + @Nullable private FlutterPlugin.FlutterAssets flutterAssets; + + GlobalMethodHandler(@NonNull FlutterPlugin.FlutterPluginBinding binding) { + this.context = binding.getApplicationContext(); + this.flutterAssets = binding.getFlutterAssets(); + this.messenger = binding.getBinaryMessenger(); + } + + private static void copy(InputStream input, OutputStream output) throws IOException { + final byte[] buffer = new byte[BUFFER_SIZE]; + final BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); + final BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); + int count = 0; + int n = 0; + try { + while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { + out.write(buffer, 0, n); + count += n; + } + out.flush(); + } finally { + try { + out.close(); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } + try { + in.close(); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } } - - private void installOfflineMapTiles(String tilesDb) { - final File dest = new File(context.getFilesDir(), DATABASE_NAME); - try (InputStream input = openTilesDbFile(tilesDb); - OutputStream output = new FileOutputStream(dest)) { - copy(input, output); - } catch (IOException e) { - e.printStackTrace(); - } + } + + @Override + public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { + String accessToken = methodCall.argument("accessToken"); + MapBoxUtils.getMapbox(context, accessToken); + + switch (methodCall.method) { + case "installOfflineMapTiles": + String tilesDb = methodCall.argument("tilesdb"); + installOfflineMapTiles(tilesDb); + result.success(null); + break; + case "setOffline": + boolean offline = methodCall.argument("offline"); + ConnectivityReceiver.instance(context).setConnected(offline ? false : null); + result.success(null); + break; + case "mergeOfflineRegions": + OfflineManagerUtils.mergeRegions(result, context, methodCall.argument("path")); + break; + case "setOfflineTileCountLimit": + OfflineManagerUtils.setOfflineTileCountLimit( + result, context, methodCall.argument("limit").longValue()); + break; + case "setHttpHeaders": + Map headers = (Map) methodCall.argument("headers"); + MapboxHttpRequestUtil.setHttpHeaders(headers, result); + break; + case "downloadOfflineRegion": + // Get args from caller + Map definitionMap = (Map) methodCall.argument("definition"); + Map metadataMap = (Map) methodCall.argument("metadata"); + String channelName = methodCall.argument("channelName"); + + // Prepare args + OfflineChannelHandlerImpl channelHandler = + new OfflineChannelHandlerImpl(messenger, channelName); + + // Start downloading + OfflineManagerUtils.downloadRegion( + result, context, definitionMap, metadataMap, channelHandler); + break; + case "getListOfRegions": + OfflineManagerUtils.regionsList(result, context); + break; + case "updateOfflineRegionMetadata": + // Get download region arguments from caller + Map metadata = (Map) methodCall.argument("metadata"); + OfflineManagerUtils.updateRegionMetadata( + result, context, methodCall.argument("id").longValue(), metadata); + break; + case "deleteOfflineRegion": + OfflineManagerUtils.deleteRegion( + result, context, methodCall.argument("id").longValue()); + break; + default: + result.notImplemented(); + break; } - - private InputStream openTilesDbFile(String tilesDb) throws IOException { - if (tilesDb.startsWith("/")) { // Absolute path. - return new FileInputStream(new File(tilesDb)); - } else { - String assetKey; - if (registrar != null) { - assetKey = registrar.lookupKeyForAsset(tilesDb); - } else if(flutterAssets != null) { - assetKey = flutterAssets.getAssetFilePathByName(tilesDb); - } else { - throw new IllegalStateException(); - } - return context.getAssets().open(assetKey); - } + } + + private void installOfflineMapTiles(String tilesDb) { + final File dest = new File(context.getFilesDir(), DATABASE_NAME); + try (InputStream input = openTilesDbFile(tilesDb); + OutputStream output = new FileOutputStream(dest)) { + copy(input, output); + } catch (IOException e) { + e.printStackTrace(); } - - private static void copy(InputStream input, OutputStream output) throws IOException { - final byte[] buffer = new byte[BUFFER_SIZE]; - final BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); - final BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); - int count = 0; - int n = 0; - try { - while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { - out.write(buffer, 0, n); - count += n; - } - out.flush(); - } finally { - try { - out.close(); - } catch (IOException e) { - Log.e(TAG, e.getMessage(), e); - } - try { - in.close(); - } catch (IOException e) { - Log.e(TAG, e.getMessage(), e); - } - } + } + + private InputStream openTilesDbFile(String tilesDb) throws IOException { + if (tilesDb.startsWith("/")) { // Absolute path. + return new FileInputStream(new File(tilesDb)); + } else { + String assetKey; + if (flutterAssets != null) { + assetKey = flutterAssets.getAssetFilePathByName(tilesDb); + } else { + throw new IllegalStateException(); + } + return context.getAssets().open(assetKey); } + } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java new file mode 100644 index 000000000..b10599dbb --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java @@ -0,0 +1,535 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +package com.mapbox.mapboxgl; + +import static com.mapbox.mapboxgl.Convert.toMap; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mapbox.mapboxsdk.style.expressions.Expression; +import com.mapbox.mapboxsdk.style.layers.PropertyFactory; +import com.mapbox.mapboxsdk.style.layers.PropertyValue; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +class LayerPropertyConverter { + static PropertyValue[] interpretSymbolLayerProperties(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 "icon-opacity": + properties.add(PropertyFactory.iconOpacity(expression)); + break; + case "icon-color": + properties.add(PropertyFactory.iconColor(expression)); + break; + case "icon-halo-color": + properties.add(PropertyFactory.iconHaloColor(expression)); + break; + case "icon-halo-width": + properties.add(PropertyFactory.iconHaloWidth(expression)); + break; + case "icon-halo-blur": + properties.add(PropertyFactory.iconHaloBlur(expression)); + break; + case "icon-translate": + properties.add(PropertyFactory.iconTranslate(expression)); + break; + case "icon-translate-anchor": + properties.add(PropertyFactory.iconTranslateAnchor(expression)); + break; + case "text-opacity": + properties.add(PropertyFactory.textOpacity(expression)); + break; + case "text-color": + properties.add(PropertyFactory.textColor(expression)); + break; + case "text-halo-color": + properties.add(PropertyFactory.textHaloColor(expression)); + break; + case "text-halo-width": + properties.add(PropertyFactory.textHaloWidth(expression)); + break; + case "text-halo-blur": + properties.add(PropertyFactory.textHaloBlur(expression)); + break; + case "text-translate": + properties.add(PropertyFactory.textTranslate(expression)); + break; + case "text-translate-anchor": + properties.add(PropertyFactory.textTranslateAnchor(expression)); + break; + case "symbol-placement": + properties.add(PropertyFactory.symbolPlacement(expression)); + break; + case "symbol-spacing": + properties.add(PropertyFactory.symbolSpacing(expression)); + break; + case "symbol-avoid-edges": + properties.add(PropertyFactory.symbolAvoidEdges(expression)); + break; + case "symbol-sort-key": + properties.add(PropertyFactory.symbolSortKey(expression)); + break; + case "symbol-z-order": + properties.add(PropertyFactory.symbolZOrder(expression)); + break; + case "icon-allow-overlap": + properties.add(PropertyFactory.iconAllowOverlap(expression)); + break; + case "icon-ignore-placement": + properties.add(PropertyFactory.iconIgnorePlacement(expression)); + break; + case "icon-optional": + properties.add(PropertyFactory.iconOptional(expression)); + break; + case "icon-rotation-alignment": + properties.add(PropertyFactory.iconRotationAlignment(expression)); + break; + case "icon-size": + properties.add(PropertyFactory.iconSize(expression)); + break; + case "icon-text-fit": + properties.add(PropertyFactory.iconTextFit(expression)); + break; + case "icon-text-fit-padding": + properties.add(PropertyFactory.iconTextFitPadding(expression)); + break; + case "icon-image": + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + properties.add(PropertyFactory.iconImage(jsonElement.getAsString())); + } else { + properties.add(PropertyFactory.iconImage(expression)); + } + break; + case "icon-rotate": + properties.add(PropertyFactory.iconRotate(expression)); + break; + case "icon-padding": + properties.add(PropertyFactory.iconPadding(expression)); + break; + case "icon-keep-upright": + properties.add(PropertyFactory.iconKeepUpright(expression)); + break; + case "icon-offset": + properties.add(PropertyFactory.iconOffset(expression)); + break; + case "icon-anchor": + properties.add(PropertyFactory.iconAnchor(expression)); + break; + case "icon-pitch-alignment": + properties.add(PropertyFactory.iconPitchAlignment(expression)); + break; + case "text-pitch-alignment": + properties.add(PropertyFactory.textPitchAlignment(expression)); + break; + case "text-rotation-alignment": + properties.add(PropertyFactory.textRotationAlignment(expression)); + break; + case "text-field": + properties.add(PropertyFactory.textField(expression)); + break; + case "text-font": + properties.add(PropertyFactory.textFont(expression)); + break; + case "text-size": + properties.add(PropertyFactory.textSize(expression)); + break; + case "text-max-width": + properties.add(PropertyFactory.textMaxWidth(expression)); + break; + case "text-line-height": + properties.add(PropertyFactory.textLineHeight(expression)); + break; + case "text-letter-spacing": + properties.add(PropertyFactory.textLetterSpacing(expression)); + break; + case "text-justify": + properties.add(PropertyFactory.textJustify(expression)); + break; + case "text-radial-offset": + properties.add(PropertyFactory.textRadialOffset(expression)); + break; + case "text-variable-anchor": + properties.add(PropertyFactory.textVariableAnchor(expression)); + break; + case "text-anchor": + properties.add(PropertyFactory.textAnchor(expression)); + break; + case "text-max-angle": + properties.add(PropertyFactory.textMaxAngle(expression)); + break; + case "text-writing-mode": + properties.add(PropertyFactory.textWritingMode(expression)); + break; + case "text-rotate": + properties.add(PropertyFactory.textRotate(expression)); + break; + case "text-padding": + properties.add(PropertyFactory.textPadding(expression)); + break; + case "text-keep-upright": + properties.add(PropertyFactory.textKeepUpright(expression)); + break; + case "text-transform": + properties.add(PropertyFactory.textTransform(expression)); + break; + case "text-offset": + properties.add(PropertyFactory.textOffset(expression)); + break; + case "text-allow-overlap": + properties.add(PropertyFactory.textAllowOverlap(expression)); + break; + case "text-ignore-placement": + properties.add(PropertyFactory.textIgnorePlacement(expression)); + break; + case "text-optional": + properties.add(PropertyFactory.textOptional(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + + static PropertyValue[] interpretCircleLayerProperties(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 "circle-radius": + properties.add(PropertyFactory.circleRadius(expression)); + break; + case "circle-color": + properties.add(PropertyFactory.circleColor(expression)); + break; + case "circle-blur": + properties.add(PropertyFactory.circleBlur(expression)); + break; + case "circle-opacity": + properties.add(PropertyFactory.circleOpacity(expression)); + break; + case "circle-translate": + properties.add(PropertyFactory.circleTranslate(expression)); + break; + case "circle-translate-anchor": + properties.add(PropertyFactory.circleTranslateAnchor(expression)); + break; + case "circle-pitch-scale": + properties.add(PropertyFactory.circlePitchScale(expression)); + break; + case "circle-pitch-alignment": + properties.add(PropertyFactory.circlePitchAlignment(expression)); + break; + case "circle-stroke-width": + properties.add(PropertyFactory.circleStrokeWidth(expression)); + break; + case "circle-stroke-color": + properties.add(PropertyFactory.circleStrokeColor(expression)); + break; + case "circle-stroke-opacity": + properties.add(PropertyFactory.circleStrokeOpacity(expression)); + break; + case "circle-sort-key": + properties.add(PropertyFactory.circleSortKey(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + + static PropertyValue[] interpretLineLayerProperties(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 "line-opacity": + properties.add(PropertyFactory.lineOpacity(expression)); + break; + case "line-color": + properties.add(PropertyFactory.lineColor(expression)); + break; + case "line-translate": + properties.add(PropertyFactory.lineTranslate(expression)); + break; + case "line-translate-anchor": + properties.add(PropertyFactory.lineTranslateAnchor(expression)); + break; + case "line-width": + properties.add(PropertyFactory.lineWidth(expression)); + break; + case "line-gap-width": + properties.add(PropertyFactory.lineGapWidth(expression)); + break; + case "line-offset": + properties.add(PropertyFactory.lineOffset(expression)); + break; + case "line-blur": + properties.add(PropertyFactory.lineBlur(expression)); + break; + case "line-dasharray": + properties.add(PropertyFactory.lineDasharray(expression)); + break; + case "line-pattern": + properties.add(PropertyFactory.linePattern(expression)); + break; + case "line-gradient": + properties.add(PropertyFactory.lineGradient(expression)); + break; + case "line-cap": + properties.add(PropertyFactory.lineCap(expression)); + break; + case "line-join": + properties.add(PropertyFactory.lineJoin(expression)); + break; + case "line-miter-limit": + properties.add(PropertyFactory.lineMiterLimit(expression)); + break; + case "line-round-limit": + properties.add(PropertyFactory.lineRoundLimit(expression)); + break; + case "line-sort-key": + properties.add(PropertyFactory.lineSortKey(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + + static PropertyValue[] interpretFillLayerProperties(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-antialias": + properties.add(PropertyFactory.fillAntialias(expression)); + break; + case "fill-opacity": + properties.add(PropertyFactory.fillOpacity(expression)); + break; + case "fill-color": + properties.add(PropertyFactory.fillColor(expression)); + break; + case "fill-outline-color": + properties.add(PropertyFactory.fillOutlineColor(expression)); + break; + case "fill-translate": + properties.add(PropertyFactory.fillTranslate(expression)); + break; + case "fill-translate-anchor": + properties.add(PropertyFactory.fillTranslateAnchor(expression)); + break; + case "fill-pattern": + properties.add(PropertyFactory.fillPattern(expression)); + break; + case "fill-sort-key": + properties.add(PropertyFactory.fillSortKey(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + 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(); + 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 "raster-opacity": + properties.add(PropertyFactory.rasterOpacity(expression)); + break; + case "raster-hue-rotate": + properties.add(PropertyFactory.rasterHueRotate(expression)); + break; + case "raster-brightness-min": + properties.add(PropertyFactory.rasterBrightnessMin(expression)); + break; + case "raster-brightness-max": + properties.add(PropertyFactory.rasterBrightnessMax(expression)); + break; + case "raster-saturation": + properties.add(PropertyFactory.rasterSaturation(expression)); + break; + case "raster-contrast": + properties.add(PropertyFactory.rasterContrast(expression)); + break; + case "raster-resampling": + properties.add(PropertyFactory.rasterResampling(expression)); + break; + case "raster-fade-duration": + properties.add(PropertyFactory.rasterFadeDuration(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + + static PropertyValue[] interpretHillshadeLayerProperties(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 "hillshade-illumination-direction": + properties.add(PropertyFactory.hillshadeIlluminationDirection(expression)); + break; + case "hillshade-illumination-anchor": + properties.add(PropertyFactory.hillshadeIlluminationAnchor(expression)); + break; + case "hillshade-exaggeration": + properties.add(PropertyFactory.hillshadeExaggeration(expression)); + break; + case "hillshade-shadow-color": + properties.add(PropertyFactory.hillshadeShadowColor(expression)); + break; + case "hillshade-highlight-color": + properties.add(PropertyFactory.hillshadeHighlightColor(expression)); + break; + case "hillshade-accent-color": + properties.add(PropertyFactory.hillshadeAccentColor(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue())); + break; + default: + break; + } + } + + 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/LineBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java deleted file mode 100644 index 5d7276143..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java +++ /dev/null @@ -1,82 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import java.util.List; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Line; -import com.mapbox.mapboxsdk.plugins.annotation.LineManager; -import com.mapbox.mapboxsdk.plugins.annotation.LineOptions; - -class LineBuilder implements LineOptionsSink { - private final LineManager lineManager; - private final LineOptions lineOptions; - - LineBuilder(LineManager lineManager) { - this.lineManager = lineManager; - this.lineOptions = new LineOptions(); - } - - public LineOptions getLineOptions(){ - return this.lineOptions; - } - - Line build() { - return lineManager.create(lineOptions); - } - - @Override - public void setLineJoin(String lineJoin) { - lineOptions.withLineJoin(lineJoin); - } - - @Override - public void setLineOpacity(float lineOpacity) { - lineOptions.withLineOpacity(lineOpacity); - } - - @Override - public void setLineColor(String lineColor) { - lineOptions.withLineColor(lineColor); - } - - @Override - public void setLineWidth(float lineWidth) { - lineOptions.withLineWidth(lineWidth); - } - - @Override - public void setLineGapWidth(float lineGapWidth) { - lineOptions.withLineGapWidth(lineGapWidth); - } - - @Override - public void setLineOffset(float lineOffset) { - lineOptions.withLineOffset(lineOffset); - } - - @Override - public void setLineBlur(float lineBlur) { - lineOptions.withLineBlur(lineBlur); - } - - @Override - public void setLinePattern(String linePattern) { - lineOptions.withLinePattern(linePattern); - } - - @Override - public void setGeometry(List geometry) { - lineOptions.withLatLngs(geometry); - } - - @Override - public void setDraggable(boolean draggable) { - lineOptions.withDraggable(draggable); - } -} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/LineController.java b/android/src/main/java/com/mapbox/mapboxgl/LineController.java deleted file mode 100644 index d7715909c..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/LineController.java +++ /dev/null @@ -1,112 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import java.util.ArrayList; -import java.util.List; - -import android.graphics.PointF; -import android.util.Log; - -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Line; -import com.mapbox.mapboxsdk.plugins.annotation.LineManager; -import com.mapbox.mapboxsdk.utils.ColorUtils; - -/** - * Controller of a single Line on the map. - */ -class LineController implements LineOptionsSink { - private final Line line; - private final OnLineTappedListener onTappedListener; - private boolean consumeTapEvents; - - LineController(Line line, boolean consumeTapEvents, OnLineTappedListener onTappedListener) { - this.line = line; - this.consumeTapEvents = consumeTapEvents; - this.onTappedListener = onTappedListener; - } - - public Line getLine(){ - return this.line; - } - - boolean onTap() { - if (onTappedListener != null) { - onTappedListener.onLineTapped(line); - } - return consumeTapEvents; - } - - void remove(LineManager lineManager) { - lineManager.delete(line); - } - - @Override - public void setLineJoin(String lineJoin) { - line.setLineJoin(lineJoin); - } - - @Override - public void setLineOpacity(float lineOpacity) { - line.setLineOpacity(lineOpacity); - } - - @Override - public void setLineColor(String lineColor) { - line.setLineColor(ColorUtils.rgbaToColor(lineColor)); - } - - @Override - public void setLineWidth(float lineWidth) { - line.setLineWidth(lineWidth); - } - - @Override - public void setLineGapWidth(float lineGapWidth) { - line.setLineGapWidth(lineGapWidth); - } - - @Override - public void setLineOffset(float lineOffset) { - line.setLineOffset(lineOffset); - } - - @Override - public void setLineBlur(float lineBlur) { - line.setLineBlur(lineBlur); - } - - @Override - public void setLinePattern(String linePattern) { - line.setLinePattern(linePattern); - } - - @Override - public void setGeometry(List geometry) { - line.setLatLngs(geometry); - } - - public List getGeometry() { - List points = line.getGeometry().coordinates(); - List latLngs = new ArrayList<>(); - for (Point point : points) { - latLngs.add(new LatLng(point.latitude(), point.longitude())); - } - return latLngs; - } - - @Override - public void setDraggable(boolean draggable) { - line.setDraggable(draggable); - } - - public void update(LineManager lineManager) { - lineManager.update(line); - } -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/LineOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/LineOptionsSink.java deleted file mode 100644 index ddd2646b7..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/LineOptionsSink.java +++ /dev/null @@ -1,36 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import java.util.List; -import com.mapbox.mapboxsdk.geometry.LatLng; - -/** - * Receiver of Line configuration options. - */ -interface LineOptionsSink { - - void setLineJoin(String lineJoin); - - void setLineOpacity(float lineOpacity); - - void setLineColor(String lineColor); - - void setLineWidth(float lineWidth); - - void setLineGapWidth(float lineGapWidth); - - void setLineOffset(float lineOffset); - - void setLineBlur(float lineBlur); - - void setLinePattern(String linePattern); - - void setGeometry(List geometry); - - void setDraggable(boolean draggable); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapBoxUtils.java b/android/src/main/java/com/mapbox/mapboxgl/MapBoxUtils.java index 2319da949..1bd491801 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapBoxUtils.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapBoxUtils.java @@ -5,33 +5,37 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.util.Log; - import androidx.annotation.NonNull; - import com.mapbox.mapboxsdk.Mapbox; abstract class MapBoxUtils { - private static final String TAG = "MapboxMapController"; + private static final String TAG = "MapboxMapController"; - static Mapbox getMapbox(Context context, String accessToken) { - return Mapbox.getInstance(context, accessToken == null ? getAccessToken(context) : accessToken); - } + static Mapbox getMapbox(Context context, String accessToken) { + return Mapbox.getInstance(context, accessToken == null ? getAccessToken(context) : accessToken); + } - private static String getAccessToken(@NonNull Context context) { - try { - ApplicationInfo ai = context.getPackageManager() - .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); - Bundle bundle = ai.metaData; - String token = bundle.getString("com.mapbox.token"); - if (token == null || token.isEmpty()) { - throw new NullPointerException(); - } - return token; - } catch (Exception e) { - Log.e(TAG, "Failed to find an Access Token in the Application meta-data. Maps may not load correctly. " + - "Please refer to the installation guide at https://github.com/tobrun/flutter-mapbox-gl#mapbox-access-token " + - "for troubleshooting advice." + e.getMessage()); - } - return null; + private static String getAccessToken(@NonNull Context context) { + try { + ApplicationInfo ai = + context + .getPackageManager() + .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = ai.metaData; + String token = bundle.getString("com.mapbox.token"); + if (token == null || token.isEmpty()) { + throw new NullPointerException(); + } + return token; + } catch (Exception e) { + Log.e( + TAG, + "Failed to find an Access Token in the Application meta-data. Maps may not load" + + " correctly. Please refer to the installation guide at" + + " https://github.com/tobrun/flutter-mapbox-gl#mapbox-access-token for" + + " troubleshooting advice." + + e.getMessage()); } + return null; + } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxHttpRequestUtil.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxHttpRequestUtil.java new file mode 100644 index 000000000..185bccd91 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxHttpRequestUtil.java @@ -0,0 +1,43 @@ +package com.mapbox.mapboxgl; + +import com.mapbox.mapboxsdk.module.http.HttpRequestUtil; +import io.flutter.plugin.common.MethodChannel; +import java.util.Map; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +abstract class MapboxHttpRequestUtil { + + public static void setHttpHeaders(Map headers, MethodChannel.Result result) { + HttpRequestUtil.setOkHttpClient(getOkHttpClient(headers, result).build()); + result.success(null); + } + + private static OkHttpClient.Builder getOkHttpClient( + Map headers, MethodChannel.Result result) { + try { + return new OkHttpClient.Builder() + .addNetworkInterceptor( + chain -> { + Request.Builder builder = chain.request().newBuilder(); + for (Map.Entry header : headers.entrySet()) { + if (header.getKey() == null || header.getKey().trim().isEmpty()) { + continue; + } + if (header.getValue() == null || header.getValue().trim().isEmpty()) { + builder.removeHeader(header.getKey()); + } else { + builder.header(header.getKey(), header.getValue()); + } + } + return chain.proceed(builder.build()); + }); + } catch (Exception e) { + result.error( + "OK_HTTP_CLIENT_ERROR", + "An unexcepted error happened during creating http " + "client" + e.getMessage(), + null); + throw new RuntimeException(e); + } + } +} diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java index 045bb76f5..44121b20c 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java @@ -5,45 +5,51 @@ package com.mapbox.mapboxgl; import android.content.Context; -import android.util.Log; import android.view.Gravity; - import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.geometry.LatLngBounds; import com.mapbox.mapboxsdk.maps.MapboxMapOptions; import com.mapbox.mapboxsdk.maps.Style; - import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.List; -import java.util.ArrayList; - class MapboxMapBuilder implements MapboxMapOptionsSink { public final String TAG = getClass().getSimpleName(); - private final MapboxMapOptions options = new MapboxMapOptions() - .textureMode(true) - .attributionEnabled(true); + private final MapboxMapOptions options = + new MapboxMapOptions().textureMode(true).attributionEnabled(true); private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; + private boolean dragEnabled = true; private int myLocationTrackingMode = 0; private int myLocationRenderMode = 0; private String styleString = Style.MAPBOX_STREETS; - private List annotationOrder = new ArrayList(); - private List annotationConsumeTapEvents = new ArrayList(); - + private LatLngBounds bounds = null; MapboxMapController build( - int id, Context context, BinaryMessenger messenger, MapboxMapsPlugin.LifecycleProvider lifecycleProvider, String accessToken) { + int id, + Context context, + BinaryMessenger messenger, + MapboxMapsPlugin.LifecycleProvider lifecycleProvider, + String accessToken) { final MapboxMapController controller = - new MapboxMapController(id, context, messenger, lifecycleProvider, options, accessToken, styleString, annotationOrder, annotationConsumeTapEvents); + new MapboxMapController( + id, + context, + messenger, + lifecycleProvider, + options, + accessToken, + styleString, + dragEnabled); controller.init(); controller.setMyLocationEnabled(myLocationEnabled); controller.setMyLocationTrackingMode(myLocationTrackingMode); controller.setMyLocationRenderMode(myLocationRenderMode); controller.setTrackCameraPosition(trackCameraPosition); + + if (null != bounds) { + controller.setCameraTargetBounds(bounds); + } + return controller; } @@ -58,15 +64,13 @@ public void setCompassEnabled(boolean compassEnabled) { @Override public void setCameraTargetBounds(LatLngBounds bounds) { - Log.e(TAG, "setCameraTargetBounds is supported only after map initiated."); - //throw new UnsupportedOperationException("setCameraTargetBounds is supported only after map initiated."); - //options.latLngBoundsForCameraTarget(bounds); + this.bounds = bounds; } @Override public void setStyleString(String styleString) { this.styleString = styleString; - //options. styleString(styleString); + // options. styleString(styleString); } @Override @@ -118,23 +122,23 @@ public void setMyLocationTrackingMode(int myLocationTrackingMode) { public void setMyLocationRenderMode(int myLocationRenderMode) { this.myLocationRenderMode = myLocationRenderMode; } - + public void setLogoViewMargins(int x, int y) { - options.logoMargins(new int[] { - (int) x, //left - (int) 0, //top - (int) 0, //right - (int) y, //bottom - }); + options.logoMargins( + new int[] { + (int) x, // left + (int) 0, // top + (int) 0, // right + (int) y, // bottom + }); } @Override public void setCompassGravity(int gravity) { - switch(gravity){ + switch (gravity) { case 0: options.compassGravity(Gravity.TOP | Gravity.START); break; - default: case 1: options.compassGravity(Gravity.TOP | Gravity.END); break; @@ -149,11 +153,12 @@ public void setCompassGravity(int gravity) { @Override public void setCompassViewMargins(int x, int y) { - switch(options.getCompassGravity()) - { + switch (options.getCompassGravity()) { case Gravity.TOP | Gravity.START: options.compassMargins(new int[] {(int) x, (int) y, 0, 0}); break; + // If the application code has not specified gravity, assume the platform + // default for the compass which is top-right default: case Gravity.TOP | Gravity.END: options.compassMargins(new int[] {0, (int) y, (int) x, 0}); @@ -168,21 +173,45 @@ public void setCompassViewMargins(int x, int y) { } @Override - public void setAttributionButtonMargins(int x, int y) { - options.attributionMargins(new int[] { - (int) x, //left - (int) 0, //top - (int) 0, //right - (int) y, //bottom - }); + public void setAttributionButtonGravity(int gravity) { + switch (gravity) { + case 0: + options.attributionGravity(Gravity.TOP | Gravity.START); + break; + case 1: + options.attributionGravity(Gravity.TOP | Gravity.END); + break; + case 2: + options.attributionGravity(Gravity.BOTTOM | Gravity.START); + break; + case 3: + options.attributionGravity(Gravity.BOTTOM | Gravity.END); + break; + } } - public void setAnnotationOrder(List annotations) { - this.annotationOrder = annotations; + @Override + public void setAttributionButtonMargins(int x, int y) { + switch (options.getAttributionGravity()) { + case Gravity.TOP | Gravity.START: + options.attributionMargins(new int[] {(int) x, (int) y, 0, 0}); + break; + case Gravity.TOP | Gravity.END: + options.attributionMargins(new int[] {0, (int) y, (int) x, 0}); + break; + // If the application code has not specified gravity, assume the platform + // default for the attribution button which is bottom left + default: + case Gravity.BOTTOM | Gravity.START: + options.attributionMargins(new int[] {(int) x, 0, 0, (int) y}); + break; + case Gravity.BOTTOM | Gravity.END: + options.attributionMargins(new int[] {0, 0, (int) x, (int) y}); + break; + } } - public void setAnnotationConsumeTapEvents(List annotations) { - this.annotationConsumeTapEvents = annotations; + public void setDragEnabled(boolean enabled) { + this.dragEnabled = enabled; } - } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java index 62b3cbd75..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; @@ -18,25 +21,30 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; - import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; - import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.mapbox.android.core.location.LocationEngine; import com.mapbox.android.core.location.LocationEngineCallback; import com.mapbox.android.core.location.LocationEngineProvider; import com.mapbox.android.core.location.LocationEngineResult; +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.android.gestures.MoveGestureDetector; 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; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; @@ -53,115 +61,128 @@ import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.mapboxsdk.maps.Style; import com.mapbox.mapboxsdk.offline.OfflineManager; -import com.mapbox.mapboxsdk.plugins.annotation.Annotation; -import com.mapbox.mapboxsdk.plugins.annotation.Circle; -import com.mapbox.mapboxsdk.plugins.annotation.CircleManager; -import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions; -import com.mapbox.mapboxsdk.plugins.annotation.Fill; -import com.mapbox.mapboxsdk.plugins.annotation.FillManager; -import com.mapbox.mapboxsdk.plugins.annotation.FillOptions; -import com.mapbox.mapboxsdk.plugins.annotation.Line; -import com.mapbox.mapboxsdk.plugins.annotation.LineManager; -import com.mapbox.mapboxsdk.plugins.annotation.LineOptions; -import com.mapbox.mapboxsdk.plugins.annotation.OnAnnotationClickListener; -import com.mapbox.mapboxsdk.plugins.annotation.Symbol; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions; 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; +import com.mapbox.mapboxsdk.style.layers.FillLayer; +import com.mapbox.mapboxsdk.style.layers.HeatmapLayer; +import com.mapbox.mapboxsdk.style.layers.HillshadeLayer; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.LineLayer; +import com.mapbox.mapboxsdk.style.layers.PropertyValue; import com.mapbox.mapboxsdk.style.layers.RasterLayer; +import com.mapbox.mapboxsdk.style.layers.SymbolLayer; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import com.mapbox.mapboxsdk.style.sources.ImageSource; - +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.platform.PlatformView; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.ArrayList; import java.util.Map; +import java.util.Set; +import java.util.UUID; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.platform.PlatformView; - -/** - * Controller of a single MapboxMaps MapView instance. - */ +/** Controller of a single MapboxMaps MapView instance. */ @SuppressLint("MissingPermission") final class MapboxMapController - implements DefaultLifecycleObserver, - MapboxMap.OnCameraIdleListener, - MapboxMap.OnCameraMoveListener, - MapboxMap.OnCameraMoveStartedListener, - OnAnnotationClickListener, - MapboxMap.OnMapClickListener, - MapboxMap.OnMapLongClickListener, - MapboxMapOptionsSink, - MethodChannel.MethodCallHandler, - OnMapReadyCallback, - OnCameraTrackingChangedListener, - OnSymbolTappedListener, - OnLineTappedListener, - OnCircleTappedListener, - OnFillTappedListener, - PlatformView { + implements DefaultLifecycleObserver, + MapboxMap.OnCameraIdleListener, + MapboxMap.OnCameraMoveListener, + MapboxMap.OnCameraMoveStartedListener, + MapView.OnDidBecomeIdleListener, + MapboxMap.OnMapClickListener, + MapboxMap.OnMapLongClickListener, + MapboxMapOptionsSink, + MethodChannel.MethodCallHandler, + OnMapReadyCallback, + OnCameraTrackingChangedListener, + PlatformView { + private static final String TAG = "MapboxMapController"; private final int id; private final MethodChannel methodChannel; private final MapboxMapsPlugin.LifecycleProvider lifecycleProvider; + 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 final Map symbols; - private final Map lines; - private final Map circles; - private final Map fills; - private SymbolManager symbolManager; - private LineManager lineManager; - private CircleManager circleManager; - private FillManager fillManager; private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private int myLocationTrackingMode = 0; private int myLocationRenderMode = 0; private boolean disposed = false; - private final float density; + private boolean dragEnabled = true; private MethodChannel.Result mapReadyResult; - private final Context context; - private final String styleStringInitial; private LocationComponent locationComponent = null; private LocationEngine locationEngine = null; private LocationEngineCallback locationEngineCallback = null; private LocalizationPlugin localizationPlugin; private Style style; - private List annotationOrder; - private List annotationConsumeTapEvents; + private Feature draggedFeature; + private AndroidGesturesManager androidGesturesManager; + private LatLng dragOrigin; + private LatLng dragPrevious; + private LatLngBounds bounds = null; + Style.OnStyleLoaded onStyleLoadedCallback = + new Style.OnStyleLoaded() { + @Override + public void onStyleLoaded(@NonNull Style style) { + MapboxMapController.this.style = style; + + updateMyLocationEnabled(); + + if (null != bounds) { + mapboxMap.setLatLngBoundsForCameraTarget(bounds); + } + + mapboxMap.addOnMapClickListener(MapboxMapController.this); + mapboxMap.addOnMapLongClickListener(MapboxMapController.this); + localizationPlugin = new LocalizationPlugin(mapView, mapboxMap, style); + + methodChannel.invokeMethod("map#onStyleLoaded", null); + } + }; MapboxMapController( - int id, - Context context, - BinaryMessenger messenger, - MapboxMapsPlugin.LifecycleProvider lifecycleProvider, - MapboxMapOptions options, - String accessToken, - String styleStringInitial, - List annotationOrder, - List annotationConsumeTapEvents) { + int id, + Context context, + BinaryMessenger messenger, + MapboxMapsPlugin.LifecycleProvider lifecycleProvider, + MapboxMapOptions options, + String accessToken, + String styleStringInitial, + boolean dragEnabled) { MapBoxUtils.getMapbox(context, accessToken); this.id = id; this.context = context; + this.dragEnabled = dragEnabled; this.styleStringInitial = styleStringInitial; this.mapView = new MapView(context, options); - this.symbols = new HashMap<>(); - this.lines = new HashMap<>(); - this.circles = new HashMap<>(); - this.fills = new HashMap<>(); + this.interactiveFeatureLayerIds = new HashSet<>(); + this.addedFeaturesByLayer = new HashMap(); this.density = context.getResources().getDisplayMetrics().density; this.lifecycleProvider = lifecycleProvider; + 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); - this.annotationOrder = annotationOrder; - this.annotationConsumeTapEvents = annotationConsumeTapEvents; } @Override @@ -186,71 +207,6 @@ private CameraPosition getCameraPosition() { return trackCameraPosition ? mapboxMap.getCameraPosition() : null; } - private SymbolController symbol(String symbolId) { - final SymbolController symbol = symbols.get(symbolId); - if (symbol == null) { - throw new IllegalArgumentException("Unknown symbol: " + symbolId); - } - return symbol; - } - - private LineBuilder newLineBuilder() { - return new LineBuilder(lineManager); - } - - private void removeLine(String lineId) { - final LineController lineController = lines.remove(lineId); - if (lineController != null) { - lineController.remove(lineManager); - } - } - - private LineController line(String lineId) { - final LineController line = lines.get(lineId); - if (line == null) { - throw new IllegalArgumentException("Unknown line: " + lineId); - } - return line; - } - - private CircleBuilder newCircleBuilder() { - return new CircleBuilder(circleManager); - } - - private void removeCircle(String circleId) { - final CircleController circleController = circles.remove(circleId); - if (circleController != null) { - circleController.remove(circleManager); - } - } - - private CircleController circle(String circleId) { - final CircleController circle = circles.get(circleId); - if (circle == null) { - throw new IllegalArgumentException("Unknown circle: " + circleId); - } - return circle; - } - - private FillBuilder newFillBuilder() { - return new FillBuilder(fillManager); - } - - private void removeFill(String fillId) { - final FillController fillController = fills.remove(fillId); - if (fillController != null) { - fillController.remove(fillManager); - } - } - - private FillController fill(String fillId) { - final FillController fill = fills.get(fillId); - if (fill == null) { - throw new IllegalArgumentException("Unknown fill: " + fillId); - } - return fill; - } - @Override public void onMapReady(MapboxMap mapboxMap) { this.mapboxMap = mapboxMap; @@ -262,20 +218,38 @@ public void onMapReady(MapboxMap mapboxMap) { mapboxMap.addOnCameraMoveListener(this); mapboxMap.addOnCameraIdleListener(this); - mapView.addOnStyleImageMissingListener((id) -> { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - final Bitmap bitmap = getScaledImage(id, displayMetrics.density); - if (bitmap != null) { - mapboxMap.getStyle().addImage(id, bitmap); - } - }); + if (androidGesturesManager != null) { + androidGesturesManager.setMoveGestureListener(new MoveGestureListener()); + mapView.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + androidGesturesManager.onTouchEvent(event); + + return draggedFeature != null; + } + }); + } + + mapView.addOnStyleImageMissingListener( + (id) -> { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + final Bitmap bitmap = getScaledImage(id, displayMetrics.density); + if (bitmap != null) { + mapboxMap.getStyle().addImage(id, bitmap); + } + }); + + mapView.addOnDidBecomeIdleListener(this); setStyleString(styleStringInitial); - // updateMyLocationEnabled(); } @Override public void setStyleString(String styleString) { + // clear old layer id from the location Component + clearLocationComponentLayer(); + // Check if json, url, absolute path or asset path: if (styleString == null || styleString.isEmpty()) { Log.e(TAG, "setStyleString - string empty or null"); @@ -283,11 +257,11 @@ public void setStyleString(String styleString) { mapboxMap.setStyle(new Style.Builder().fromJson(styleString), onStyleLoadedCallback); } else if (styleString.startsWith("/")) { // Absolute path - mapboxMap.setStyle(new Style.Builder().fromUri("file://" + styleString), onStyleLoadedCallback); - } else if ( - !styleString.startsWith("http://") && - !styleString.startsWith("https://")&& - !styleString.startsWith("mapbox://")) { + mapboxMap.setStyle( + new Style.Builder().fromUri("file://" + styleString), onStyleLoadedCallback); + } else if (!styleString.startsWith("http://") + && !styleString.startsWith("https://") + && !styleString.startsWith("mapbox://")) { // We are assuming that the style will be loaded from an asset here. String key = MapboxMapsPlugin.flutterAssets.getAssetFilePathByName(styleString); mapboxMap.setStyle(new Style.Builder().fromUri("asset://" + key), onStyleLoadedCallback); @@ -296,77 +270,83 @@ public void setStyleString(String styleString) { } } - Style.OnStyleLoaded onStyleLoadedCallback = new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - MapboxMapController.this.style = style; - for(String annotationType : annotationOrder) { - switch (annotationType) { - case "AnnotationType.fill": - enableFillManager(style); - break; - case "AnnotationType.line": - enableLineManager(style); - break; - case "AnnotationType.circle": - enableCircleManager(style); - break; - case "AnnotationType.symbol": - enableSymbolManager(style); - break; - default: - throw new IllegalArgumentException("Unknown annotation type: " + annotationType + ", must be either 'fill', 'line', 'circle' or 'symbol'"); - } - } - - if (myLocationEnabled) { - enableLocationComponent(style); - } - // needs to be placed after SymbolManager#addClickListener, - // is fixed with 0.6.0 of annotations plugin - mapboxMap.addOnMapClickListener(MapboxMapController.this); - mapboxMap.addOnMapLongClickListener(MapboxMapController.this); - localizationPlugin = new LocalizationPlugin(mapView, mapboxMap, style); - - methodChannel.invokeMethod("map#onStyleLoaded", null); - } - }; - - @SuppressWarnings( {"MissingPermission"}) + @SuppressWarnings({"MissingPermission"}) private void enableLocationComponent(@NonNull Style style) { if (hasLocationPermission()) { locationEngine = LocationEngineProvider.getBestLocationEngine(context); - LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) - .trackingGesturesManagement(true) - .build(); locationComponent = mapboxMap.getLocationComponent(); - locationComponent.activateLocationComponent(context, style, locationComponentOptions); + locationComponent.activateLocationComponent( + context, style, buildLocationComponentOptions(style)); locationComponent.setLocationComponentEnabled(true); // locationComponent.setRenderMode(RenderMode.COMPASS); // remove or keep default? locationComponent.setLocationEngine(locationEngine); locationComponent.setMaxAnimationFps(30); updateMyLocationTrackingMode(); - setMyLocationTrackingMode(this.myLocationTrackingMode); updateMyLocationRenderMode(); - setMyLocationRenderMode(this.myLocationRenderMode); locationComponent.addOnCameraTrackingChangedListener(this); } else { Log.e(TAG, "missing location permissions"); } } - private void onUserLocationUpdate(Location location){ - if(location==null){ + private void updateLocationComponentLayer() { + if (locationComponent != null && locationComponentRequiresUpdate()) { + locationComponent.applyStyle(buildLocationComponentOptions(style)); + } + } + + private void clearLocationComponentLayer() { + if (locationComponent != null) { + locationComponent.applyStyle(buildLocationComponentOptions(null)); + } + } + + String getLastLayerOnStyle(Style style) { + if (style != null) { + final List layers = style.getLayers(); + + if (layers.size() > 0) { + return layers.get(layers.size() - 1).getId(); + } + } + return null; + } + + /// only update if the last layer is not the mapbox-location-bearing-layer + boolean locationComponentRequiresUpdate() { + final String lastLayerId = getLastLayerOnStyle(style); + return lastLayerId != null && !lastLayerId.equals("mapbox-location-bearing-layer"); + } + + private LocationComponentOptions buildLocationComponentOptions(Style style) { + final LocationComponentOptions.Builder optionsBuilder = + LocationComponentOptions.builder(context); + optionsBuilder.trackingGesturesManagement(true); + + final String lastLayerId = getLastLayerOnStyle(style); + if (lastLayerId != null) { + optionsBuilder.layerAbove(lastLayerId); + } + return optionsBuilder.build(); + } + + private void onUserLocationUpdate(Location location) { + if (location == null) { return; } final Map userLocation = new HashMap<>(6); - userLocation.put("position", new double[]{location.getLatitude(), location.getLongitude()}); + userLocation.put("position", new double[] {location.getLatitude(), location.getLongitude()}); + userLocation.put("speed", location.getSpeed()); userLocation.put("altitude", location.getAltitude()); userLocation.put("bearing", location.getBearing()); userLocation.put("horizontalAccuracy", location.getAccuracy()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - userLocation.put("verticalAccuracy", (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? location.getVerticalAccuracyMeters() : null); + userLocation.put( + "verticalAccuracy", + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + ? location.getVerticalAccuracyMeters() + : null); } userLocation.put("timestamp", location.getTime()); @@ -375,40 +355,311 @@ private void onUserLocationUpdate(Location location){ methodChannel.invokeMethod("map#onUserLocationUpdated", arguments); } - private void enableSymbolManager(@NonNull Style style) { - if (symbolManager == null) { - symbolManager = new SymbolManager(mapView, mapboxMap, style); - symbolManager.setIconAllowOverlap(true); - symbolManager.setIconIgnorePlacement(true); - symbolManager.setTextAllowOverlap(true); - symbolManager.setTextIgnorePlacement(true); - symbolManager.addClickListener(MapboxMapController.this::onAnnotationClick); + private void addGeoJsonSource(String sourceName, String source) { + FeatureCollection featureCollection = FeatureCollection.fromJson(source); + GeoJsonSource geoJsonSource = new GeoJsonSource(sourceName, featureCollection); + addedFeaturesByLayer.put(sourceName, featureCollection); + + style.addSource(geoJsonSource); + } + + private void setGeoJsonSource(String sourceName, String geojson) { + FeatureCollection featureCollection = FeatureCollection.fromJson(geojson); + GeoJsonSource geoJsonSource = style.getSourceAs(sourceName); + addedFeaturesByLayer.put(sourceName, featureCollection); + + geoJsonSource.setGeoJson(featureCollection); + } + + private void setGeoJsonFeature(String sourceName, String geojsonFeature) { + Feature feature = Feature.fromJson(geojsonFeature); + FeatureCollection featureCollection = addedFeaturesByLayer.get(sourceName); + GeoJsonSource geoJsonSource = style.getSourceAs(sourceName); + if (featureCollection != null && geoJsonSource != null) { + final List features = featureCollection.features(); + for (int i = 0; i < features.size(); i++) { + final String id = features.get(i).id(); + if (id.equals(feature.id())) { + features.set(i, feature); + break; + } + } + + geoJsonSource.setGeoJson(featureCollection); + } + } + + private void addSymbolLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + + SymbolLayer symbolLayer = new SymbolLayer(layerName, sourceName); + symbolLayer.setProperties(properties); + if (sourceLayer != null) { + symbolLayer.setSourceLayer(sourceLayer); + } + if (minZoom != null) { + symbolLayer.setMinZoom(minZoom); + } + if (maxZoom != null) { + symbolLayer.setMaxZoom(maxZoom); + } + if (filter != null) { + symbolLayer.setFilter(filter); + } + if (belowLayerId != null) { + style.addLayerBelow(symbolLayer, belowLayerId); + } else { + style.addLayer(symbolLayer); + } + if (enableInteraction) { + interactiveFeatureLayerIds.add(layerName); + } + } + + private void addLineLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + LineLayer lineLayer = new LineLayer(layerName, sourceName); + lineLayer.setProperties(properties); + if (sourceLayer != null) { + lineLayer.setSourceLayer(sourceLayer); + } + if (minZoom != null) { + lineLayer.setMinZoom(minZoom); + } + if (maxZoom != null) { + lineLayer.setMaxZoom(maxZoom); + } + if (filter != null) { + lineLayer.setFilter(filter); + } + if (belowLayerId != null) { + style.addLayerBelow(lineLayer, belowLayerId); + } else { + style.addLayer(lineLayer); + } + if (enableInteraction) { + interactiveFeatureLayerIds.add(layerName); + } + } + + private void addFillLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + FillLayer fillLayer = new FillLayer(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 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 enableLineManager(@NonNull Style style) { - if (lineManager == null) { - lineManager = new LineManager(mapView, mapboxMap, style); - lineManager.addClickListener(MapboxMapController.this::onAnnotationClick); + private void addCircleLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + CircleLayer circleLayer = new CircleLayer(layerName, sourceName); + circleLayer.setProperties(properties); + if (sourceLayer != null) { + circleLayer.setSourceLayer(sourceLayer); + } + if (minZoom != null) { + circleLayer.setMinZoom(minZoom); + } + if (maxZoom != null) { + circleLayer.setMaxZoom(maxZoom); + } + if (filter != null) { + circleLayer.setFilter(filter); + } + if (belowLayerId != null) { + style.addLayerBelow(circleLayer, belowLayerId); + } else { + style.addLayer(circleLayer); + } + if (enableInteraction) { + interactiveFeatureLayerIds.add(layerName); } } - private void enableCircleManager(@NonNull Style style) { - if (circleManager == null) { - circleManager = new CircleManager(mapView, mapboxMap, style); - circleManager.addClickListener(MapboxMapController.this::onAnnotationClick); + private Expression parseFilter(String filter) { + JsonParser parser = new JsonParser(); + JsonElement filterJsonElement = parser.parse(filter); + return filterJsonElement.isJsonNull() ? null : Expression.Converter.convert(filterJsonElement); + } + + private void addRasterLayer( + String layerName, + String sourceName, + Float minZoom, + Float maxZoom, + String belowLayerId, + PropertyValue[] properties, + Expression filter) { + RasterLayer layer = new RasterLayer(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 void enableFillManager(@NonNull Style style) { - if (fillManager == null) { - fillManager = new FillManager(mapView, mapboxMap, style); - fillManager.addClickListener(MapboxMapController.this::onAnnotationClick); + private void addHillshadeLayer( + String layerName, + String sourceName, + Float minZoom, + Float maxZoom, + String belowLayerId, + PropertyValue[] properties, + Expression filter) { + HillshadeLayer layer = new HillshadeLayer(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 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); + } + } + Collections.reverse(layersInOrder); + + for (String id : layersInOrder) { + List features = mapboxMap.queryRenderedFeatures(in, id); + if (!features.isEmpty()) { + return features.get(0); + } + } + } + return null; + } + @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { case "map#waitForMap": if (mapboxMap != null) { @@ -417,571 +668,767 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { } mapReadyResult = result; break; - case "map#update": { - Convert.interpretMapboxMapOptions(call.argument("options"), this); - result.success(Convert.toJson(getCameraPosition())); - break; - } - case "map#updateMyLocationTrackingMode": { - int myLocationTrackingMode = call.argument("mode"); - setMyLocationTrackingMode(myLocationTrackingMode); - result.success(null); - break; - } - case "map#matchMapLanguageWithDeviceDefault": { - try { - localizationPlugin.matchMapLanguageWithDeviceDefault(); - result.success(null); - } catch (RuntimeException exception) { - Log.d(TAG, exception.toString()); - result.error("MAPBOX LOCALIZATION PLUGIN ERROR", exception.toString(), null); - } - break; - } - case "map#setMapLanguage": { - final String language = call.argument("language"); - try { - localizationPlugin.setMapLanguage(language); - result.success(null); - } catch (RuntimeException exception) { - Log.d(TAG, exception.toString()); - result.error("MAPBOX LOCALIZATION PLUGIN ERROR", exception.toString(), null); - } - break; - } - case "map#getVisibleRegion": { - Map reply = new HashMap<>(); - VisibleRegion visibleRegion = mapboxMap.getProjection().getVisibleRegion(); - reply.put("sw", Arrays.asList(visibleRegion.nearLeft.getLatitude(), visibleRegion.nearLeft.getLongitude())); - reply.put("ne", Arrays.asList(visibleRegion.farRight.getLatitude(), visibleRegion.farRight.getLongitude())); - result.success(reply); - break; - } - case "map#toScreenLocation": { - Map reply = new HashMap<>(); - PointF pointf = mapboxMap.getProjection().toScreenLocation(new LatLng(call.argument("latitude"),call.argument("longitude"))); - reply.put("x", pointf.x); - reply.put("y", pointf.y); - result.success(reply); - break; - } - case "map#toScreenLocationBatch": { - double[] param = (double[])call.argument("coordinates"); - double[] reply = new double[param.length]; - - for (int i = 0; i < param.length; i += 2) { - PointF pointf = mapboxMap.getProjection().toScreenLocation(new LatLng(param[i], param[i + 1])); - reply[i] = pointf.x; - reply[i + 1] = pointf.y; + case "map#update": + { + Convert.interpretMapboxMapOptions(call.argument("options"), this, context); + result.success(Convert.toJson(getCameraPosition())); + break; } - - result.success(reply); - break; - } - case "map#toLatLng": { - Map reply = new HashMap<>(); - LatLng latlng = mapboxMap.getProjection().fromScreenLocation(new PointF( ((Double) call.argument("x")).floatValue(), ((Double) call.argument("y")).floatValue())); - reply.put("latitude", latlng.getLatitude()); - reply.put("longitude", latlng.getLongitude()); - result.success(reply); - break; - } - case "map#getMetersPerPixelAtLatitude": { - Map reply = new HashMap<>(); - Double retVal = mapboxMap.getProjection().getMetersPerPixelAtLatitude((Double)call.argument("latitude")); - reply.put("metersperpixel", retVal); - result.success(reply); - break; - } - case "camera#move": { - final CameraUpdate cameraUpdate = Convert.toCameraUpdate(call.argument("cameraUpdate"), mapboxMap, density); - if (cameraUpdate != null) { - // camera transformation not handled yet - mapboxMap.moveCamera(cameraUpdate, new OnCameraMoveFinishedListener(){ - @Override - public void onFinish() { - super.onFinish(); - result.success(true); - } - - @Override - public void onCancel() { - super.onCancel(); - result.success(false); - } - }); - - // moveCamera(cameraUpdate); - }else { - result.success(false); + case "map#updateMyLocationTrackingMode": + { + int myLocationTrackingMode = call.argument("mode"); + setMyLocationTrackingMode(myLocationTrackingMode); + result.success(null); + break; } - break; - } - case "camera#animate": { - final CameraUpdate cameraUpdate = Convert.toCameraUpdate(call.argument("cameraUpdate"), mapboxMap, density); - final Integer duration = call.argument("duration"); - - final OnCameraMoveFinishedListener onCameraMoveFinishedListener = new OnCameraMoveFinishedListener(){ - @Override - public void onFinish() { - super.onFinish(); - result.success(true); + case "map#matchMapLanguageWithDeviceDefault": + { + try { + localizationPlugin.matchMapLanguageWithDeviceDefault(); + result.success(null); + } catch (RuntimeException exception) { + Log.d(TAG, exception.toString()); + result.error("MAPBOX LOCALIZATION PLUGIN ERROR", exception.toString(), null); } - - @Override - public void onCancel() { - super.onCancel(); - result.success(false); + break; + } + case "map#updateContentInsets": + { + HashMap insets = call.argument("bounds"); + final CameraUpdate cameraUpdate = + CameraUpdateFactory.paddingTo( + Convert.toPixels(insets.get("left"), density), + Convert.toPixels(insets.get("top"), density), + Convert.toPixels(insets.get("right"), density), + Convert.toPixels(insets.get("bottom"), density)); + + if (call.argument("animated")) { + animateCamera(cameraUpdate, null, result); + } else { + moveCamera(cameraUpdate, result); } - }; - if (cameraUpdate != null && duration != null) { - // camera transformation not handled yet - mapboxMap.animateCamera(cameraUpdate, duration, onCameraMoveFinishedListener); - } else if (cameraUpdate != null) { - // camera transformation not handled yet - mapboxMap.animateCamera(cameraUpdate, onCameraMoveFinishedListener); - } else { - result.success(false); + break; } - break; - } - case "map#queryRenderedFeatures": { - Map reply = new HashMap<>(); - List features; - - String[] layerIds = ((List) call.argument("layerIds")).toArray(new String[0]); + case "map#setMapLanguage": + { + final String language = call.argument("language"); + try { + localizationPlugin.setMapLanguage(language); + result.success(null); + } catch (RuntimeException exception) { + Log.d(TAG, exception.toString()); + result.error("MAPBOX LOCALIZATION PLUGIN ERROR", exception.toString(), null); + } + break; + } + case "map#getVisibleRegion": + { + Map reply = new HashMap<>(); + VisibleRegion visibleRegion = mapboxMap.getProjection().getVisibleRegion(); + reply.put( + "sw", + Arrays.asList( + visibleRegion.nearLeft.getLatitude(), visibleRegion.nearLeft.getLongitude())); + reply.put( + "ne", + Arrays.asList( + visibleRegion.farRight.getLatitude(), visibleRegion.farRight.getLongitude())); + result.success(reply); + break; + } + case "map#toScreenLocation": + { + Map reply = new HashMap<>(); + PointF pointf = + mapboxMap + .getProjection() + .toScreenLocation( + new LatLng(call.argument("latitude"), call.argument("longitude"))); + reply.put("x", pointf.x); + reply.put("y", pointf.y); + result.success(reply); + break; + } + case "map#toScreenLocationBatch": + { + double[] param = (double[]) call.argument("coordinates"); + double[] reply = new double[param.length]; + + for (int i = 0; i < param.length; i += 2) { + PointF pointf = + mapboxMap.getProjection().toScreenLocation(new LatLng(param[i], param[i + 1])); + reply[i] = pointf.x; + reply[i + 1] = pointf.y; + } - List filter = call.argument("filter"); - JsonElement jsonElement = filter == null ? null : new Gson().toJsonTree(filter); - JsonArray jsonArray = null; - if (jsonElement != null && jsonElement.isJsonArray()) { - jsonArray = jsonElement.getAsJsonArray(); + result.success(reply); + break; } - Expression filterExpression = jsonArray == null ? null : Expression.Converter.convert(jsonArray); - if (call.hasArgument("x")) { - Double x = call.argument("x"); - Double y = call.argument("y"); - PointF pixel = new PointF(x.floatValue(), y.floatValue()); - features = mapboxMap.queryRenderedFeatures(pixel, filterExpression, layerIds); - } else { - Double left = call.argument("left"); - Double top = call.argument("top"); - Double right = call.argument("right"); - Double bottom = call.argument("bottom"); - RectF rectF = new RectF(left.floatValue(), top.floatValue(), right.floatValue(), bottom.floatValue()); - features = mapboxMap.queryRenderedFeatures(rectF, filterExpression, layerIds); + case "map#toLatLng": + { + Map reply = new HashMap<>(); + LatLng latlng = + mapboxMap + .getProjection() + .fromScreenLocation( + new PointF( + ((Double) call.argument("x")).floatValue(), + ((Double) call.argument("y")).floatValue())); + reply.put("latitude", latlng.getLatitude()); + reply.put("longitude", latlng.getLongitude()); + result.success(reply); + break; } - List featuresJson = new ArrayList<>(); - for (Feature feature : features) { - featuresJson.add(feature.toJson()); + case "map#getMetersPerPixelAtLatitude": + { + Map reply = new HashMap<>(); + Double retVal = + mapboxMap + .getProjection() + .getMetersPerPixelAtLatitude((Double) call.argument("latitude")); + reply.put("metersperpixel", retVal); + result.success(reply); + break; } - reply.put("features", featuresJson); - result.success(reply); - break; - } - case "map#setTelemetryEnabled": { - final boolean enabled = call.argument("enabled"); - Mapbox.getTelemetry().setUserTelemetryRequestState(enabled); - result.success(null); - break; - } - case "map#getTelemetryEnabled": { - final TelemetryEnabler.State telemetryState = TelemetryEnabler.retrieveTelemetryStateFromPreferences(); - result.success(telemetryState == TelemetryEnabler.State.ENABLED); - break; - } - case "map#invalidateAmbientCache": { - OfflineManager fileSource = OfflineManager.getInstance(context); + case "camera#move": + { + final CameraUpdate cameraUpdate = + Convert.toCameraUpdate(call.argument("cameraUpdate"), mapboxMap, density); + moveCamera(cameraUpdate, result); + break; + } + case "camera#animate": + { + final CameraUpdate cameraUpdate = + Convert.toCameraUpdate(call.argument("cameraUpdate"), mapboxMap, density); + final Integer duration = call.argument("duration"); + + animateCamera(cameraUpdate, duration, result); + break; + } + case "map#queryRenderedFeatures": + { + Map reply = new HashMap<>(); + List features; - fileSource.invalidateAmbientCache(new OfflineManager.FileSourceCallback() { - @Override - public void onSuccess() { - result.success(null); - } + String[] layerIds = ((List) call.argument("layerIds")).toArray(new String[0]); - @Override - public void onError(@NonNull String message) { - result.error("MAPBOX CACHE ERROR", message, null); + List filter = call.argument("filter"); + JsonElement jsonElement = filter == null ? null : new Gson().toJsonTree(filter); + JsonArray jsonArray = null; + if (jsonElement != null && jsonElement.isJsonArray()) { + jsonArray = jsonElement.getAsJsonArray(); } - }); - break; - } - case "symbols#addAll": { - List newSymbolIds = new ArrayList(); - final List options = call.argument("options"); - List symbolOptionsList = new ArrayList(); - if (options != null) { - SymbolBuilder symbolBuilder; - for (Object o : options) { - symbolBuilder = new SymbolBuilder(); - Convert.interpretSymbolOptions(o, symbolBuilder); - symbolOptionsList.add(symbolBuilder.getSymbolOptions()); + Expression filterExpression = + jsonArray == null ? null : Expression.Converter.convert(jsonArray); + if (call.hasArgument("x")) { + Double x = call.argument("x"); + Double y = call.argument("y"); + PointF pixel = new PointF(x.floatValue(), y.floatValue()); + features = mapboxMap.queryRenderedFeatures(pixel, filterExpression, layerIds); + } else { + Double left = call.argument("left"); + Double top = call.argument("top"); + Double right = call.argument("right"); + Double bottom = call.argument("bottom"); + RectF rectF = + new RectF( + left.floatValue(), top.floatValue(), right.floatValue(), bottom.floatValue()); + features = mapboxMap.queryRenderedFeatures(rectF, filterExpression, layerIds); } - if (!symbolOptionsList.isEmpty()) { - List newSymbols = symbolManager.create(symbolOptionsList); - String symbolId; - for (Symbol symbol : newSymbols) { - symbolId = String.valueOf(symbol.getId()); - newSymbolIds.add(symbolId); - symbols.put(symbolId, new SymbolController(symbol, annotationConsumeTapEvents.contains("AnnotationType.symbol"), this)); - } + List featuresJson = new ArrayList<>(); + for (Feature feature : features) { + featuresJson.add(feature.toJson()); } + reply.put("features", featuresJson); + result.success(reply); + break; } - result.success(newSymbolIds); - break; - } - case "symbols#removeAll": { - final ArrayList symbolIds = call.argument("ids"); - SymbolController symbolController; - - List symbolList = new ArrayList(); - for(String symbolId : symbolIds){ - symbolController = symbols.remove(symbolId); - if (symbolController != null) { - symbolList.add(symbolController.getSymbol()); - } + case "map#setTelemetryEnabled": + { + final boolean enabled = call.argument("enabled"); + Mapbox.getTelemetry().setUserTelemetryRequestState(enabled); + result.success(null); + break; } - if(!symbolList.isEmpty()) { - symbolManager.delete(symbolList); + case "map#getTelemetryEnabled": + { + final TelemetryEnabler.State telemetryState = + TelemetryEnabler.retrieveTelemetryStateFromPreferences(); + result.success(telemetryState == TelemetryEnabler.State.ENABLED); + break; } - result.success(null); - break; - } - case "symbol#update": { - final String symbolId = call.argument("symbol"); - final SymbolController symbol = symbol(symbolId); - Convert.interpretSymbolOptions(call.argument("options"), symbol); - symbol.update(symbolManager); - result.success(null); - break; - } - case "symbol#getGeometry": { - final String symbolId = call.argument("symbol"); - final SymbolController symbol = symbol(symbolId); - final LatLng symbolLatLng = symbol.getGeometry(); - Map hashMapLatLng = new HashMap<>(); - hashMapLatLng.put("latitude", symbolLatLng.getLatitude()); - hashMapLatLng.put("longitude", symbolLatLng.getLongitude()); - result.success(hashMapLatLng); - } - case "symbolManager#iconAllowOverlap": { - final Boolean value = call.argument("iconAllowOverlap"); - symbolManager.setIconAllowOverlap(value); - result.success(null); - break; - } - case "symbolManager#iconIgnorePlacement": { - final Boolean value = call.argument("iconIgnorePlacement"); - symbolManager.setIconIgnorePlacement(value); - result.success(null); - break; - } - case "symbolManager#textAllowOverlap": { - final Boolean value = call.argument("textAllowOverlap"); - symbolManager.setTextAllowOverlap(value); - result.success(null); - break; - } - case "symbolManager#textIgnorePlacement": { - final Boolean iconAllowOverlap = call.argument("textIgnorePlacement"); - symbolManager.setTextIgnorePlacement(iconAllowOverlap); - result.success(null); - break; - } - case "line#add": { - final LineBuilder lineBuilder = newLineBuilder(); - Convert.interpretLineOptions(call.argument("options"), lineBuilder); - final Line line = lineBuilder.build(); - final String lineId = String.valueOf(line.getId()); - lines.put(lineId, new LineController(line, annotationConsumeTapEvents.contains("AnnotationType.line"), this)); - result.success(lineId); - break; - } - case "line#remove": { - final String lineId = call.argument("line"); - removeLine(lineId); - result.success(null); - break; - } - case "line#addAll": { - List newIds = new ArrayList(); - final List options = call.argument("options"); - List optionList = new ArrayList(); - if (options != null) { - LineBuilder builder; - for (Object o : options) { - builder = newLineBuilder(); - Convert.interpretLineOptions(o, builder); - optionList.add(builder.getLineOptions()); - } - if (!optionList.isEmpty()) { - List newLines = lineManager.create(optionList); - String id; - for (Line line : newLines) { - id = String.valueOf(line.getId()); - newIds.add(id); - lines.put(id, new LineController(line, true, this)); - } - } + case "map#invalidateAmbientCache": + { + OfflineManager fileSource = OfflineManager.getInstance(context); + + fileSource.invalidateAmbientCache( + new OfflineManager.FileSourceCallback() { + @Override + public void onSuccess() { + result.success(null); + } + + @Override + public void onError(@NonNull String message) { + result.error("MAPBOX CACHE ERROR", message, null); + } + }); + break; + } + case "source#addGeoJson": + { + final String sourceId = call.argument("sourceId"); + final String geojson = call.argument("geojson"); + addGeoJsonSource(sourceId, geojson); + result.success(null); + break; } - result.success(newIds); - break; - } - case "line#removeAll": { - final ArrayList ids = call.argument("ids"); - LineController lineController; - - List toBeRemoved = new ArrayList(); - for(String id : ids){ - lineController = lines.remove(id); - if (lineController != null) { - toBeRemoved.add(lineController.getLine()); - } + case "source#setGeoJson": + { + final String sourceId = call.argument("sourceId"); + final String geojson = call.argument("geojson"); + setGeoJsonSource(sourceId, geojson); + result.success(null); + break; } - if(!toBeRemoved.isEmpty()) { - lineManager.delete(toBeRemoved); + case "source#setFeature": + { + final String sourceId = call.argument("sourceId"); + final String geojsonFeature = call.argument("geojsonFeature"); + setGeoJsonFeature(sourceId, geojsonFeature); + result.success(null); + break; } - result.success(null); - break; - } - case "line#update": { - final String lineId = call.argument("line"); - final LineController line = line(lineId); - Convert.interpretLineOptions(call.argument("options"), line); - line.update(lineManager); - result.success(null); - break; - } - case "line#getGeometry": { - final String lineId = call.argument("line"); - final LineController line = line(lineId); - final List lineLatLngs = line.getGeometry(); - final List resultList = new ArrayList<>(); - for (LatLng latLng: lineLatLngs){ - Map hashMapLatLng = new HashMap<>(); - hashMapLatLng.put("latitude", latLng.getLatitude()); - hashMapLatLng.put("longitude", latLng.getLongitude()); - resultList.add(hashMapLatLng); + case "symbolLayer#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.interpretSymbolLayerProperties(call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + removeLayer(layerId); + addSymbolLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + + result.success(null); + break; } - result.success(resultList); - break; - } - case "circle#add": { - final CircleBuilder circleBuilder = newCircleBuilder(); - Convert.interpretCircleOptions(call.argument("options"), circleBuilder); - final Circle circle = circleBuilder.build(); - final String circleId = String.valueOf(circle.getId()); - circles.put(circleId, new CircleController(circle, annotationConsumeTapEvents.contains("AnnotationType.circle"), this)); - result.success(circleId); - break; - } - case "circle#addAll": { - List newIds = new ArrayList(); - final List options = call.argument("options"); - List optionList = new ArrayList(); - if (options != null) { - CircleBuilder builder; - for (Object o : options) { - builder = newCircleBuilder(); - Convert.interpretCircleOptions(o, builder); - optionList.add(builder.getCircleOptions()); - } - if (!optionList.isEmpty()) { - List newCircles = circleManager.create(optionList); - String id; - for (Circle circle : newCircles) { - id = String.valueOf(circle.getId()); - newIds.add(id); - circles.put(id, new CircleController(circle, true, this)); - } - } + case "lineLayer#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.interpretLineLayerProperties(call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + removeLayer(layerId); + addLineLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + + result.success(null); + break; } - result.success(newIds); - break; - } - case "circle#removeAll": { - final ArrayList ids = call.argument("ids"); - CircleController circleController; - - List toBeRemoved = new ArrayList(); - for(String id : ids){ - circleController = circles.remove(id); - if (circleController != null) { - toBeRemoved.add(circleController.getCircle()); - } + case "fillLayer#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.interpretFillLayerProperties(call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + removeLayer(layerId); + addFillLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + + result.success(null); + break; } - if(!toBeRemoved.isEmpty()) { - circleManager.delete(toBeRemoved); + 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; } - result.success(null); - break; - } - case "circle#remove": { - final String circleId = call.argument("circle"); - removeCircle(circleId); - result.success(null); - break; - } - case "circle#update": { - Log.e(TAG, "update circle"); - final String circleId = call.argument("circle"); - final CircleController circle = circle(circleId); - Convert.interpretCircleOptions(call.argument("options"), circle); - circle.update(circleManager); - result.success(null); - break; - } - case "circle#getGeometry": { - final String circleId = call.argument("circle"); - final CircleController circle = circle(circleId); - final LatLng circleLatLng = circle.getGeometry(); - Map hashMapLatLng = new HashMap<>(); - hashMapLatLng.put("latitude", circleLatLng.getLatitude()); - hashMapLatLng.put("longitude", circleLatLng.getLongitude()); - result.success(hashMapLatLng); - break; - } - case "fill#add": { - final FillBuilder fillBuilder = newFillBuilder(); - Convert.interpretFillOptions(call.argument("options"), fillBuilder); - final Fill fill = fillBuilder.build(); - final String fillId = String.valueOf(fill.getId()); - fills.put(fillId, new FillController(fill, annotationConsumeTapEvents.contains("AnnotationType.fill"), this)); - result.success(fillId); - break; - } + case "circleLayer#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.interpretCircleLayerProperties(call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + removeLayer(layerId); + addCircleLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + + result.success(null); + break; + } + case "rasterLayer#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.interpretRasterLayerProperties(call.argument("properties")); + + removeLayer(layerId); + addRasterLayer( + layerId, + sourceId, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + belowLayerId, + properties, + null); + updateLocationComponentLayer(); - case "fill#addAll": { - List newIds = new ArrayList(); - final List options = call.argument("options"); - List optionList = new ArrayList(); - if (options != null) { - FillBuilder builder; - for (Object o : options) { - builder = newFillBuilder(); - Convert.interpretFillOptions(o, builder); - optionList.add(builder.getFillOptions()); + result.success(null); + break; + } + case "hillshadeLayer#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.interpretHillshadeLayerProperties(call.argument("properties")); + addHillshadeLayer( + layerId, + sourceId, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + belowLayerId, + properties, + 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; + } + case "locationComponent#getLastLocation": + { + Log.e(TAG, "location component: getLastLocation"); + if (this.myLocationEnabled && locationComponent != null && locationEngine != null) { + Map reply = new HashMap<>(); + locationEngine.getLastLocation( + new LocationEngineCallback() { + @Override + public void onSuccess(LocationEngineResult locationEngineResult) { + Location lastLocation = locationEngineResult.getLastLocation(); + if (lastLocation != null) { + reply.put("latitude", lastLocation.getLatitude()); + reply.put("longitude", lastLocation.getLongitude()); + reply.put("altitude", lastLocation.getAltitude()); + result.success(reply); + } else { + result.error("", "", null); // ??? + } + } + + @Override + public void onFailure(@NonNull Exception exception) { + result.error("", "", null); // ??? + } + }); } - if (!optionList.isEmpty()) { - List newFills = fillManager.create(optionList); - String id; - for (Fill fill : newFills) { - id = String.valueOf(fill.getId()); - newIds.add(id); - fills.put(id, new FillController(fill, true, this)); - } + break; + } + case "style#addImage": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); } + style.addImage( + call.argument("name"), + BitmapFactory.decodeByteArray(call.argument("bytes"), 0, call.argument("length")), + call.argument("sdf")); + result.success(null); + break; } - result.success(newIds); - break; - } - case "fill#removeAll": { - final ArrayList ids = call.argument("ids"); - FillController fillController; - - List toBeRemoved = new ArrayList(); - for(String id : ids){ - fillController = fills.remove(id); - if (fillController != null) { - toBeRemoved.add(fillController.getFill()); - } + case "style#addImageSource": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + List coordinates = Convert.toLatLngList(call.argument("coordinates"), false); + style.addSource( + new ImageSource( + call.argument("imageSourceId"), + new LatLngQuad( + coordinates.get(0), + coordinates.get(1), + coordinates.get(2), + coordinates.get(3)), + BitmapFactory.decodeByteArray( + call.argument("bytes"), 0, call.argument("length")))); + result.success(null); + break; } - if(!toBeRemoved.isEmpty()) { - fillManager.delete(toBeRemoved); + 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")); + final Map properties = (Map) call.argument("properties"); + SourcePropertyConverter.addSource(id, properties, style); + result.success(null); + break; } - result.success(null); - break; - } - case "fill#remove": { - final String fillId = call.argument("fill"); - removeFill(fillId); - result.success(null); - break; - } - case "fill#update": { - Log.e(TAG, "update fill"); - final String fillId = call.argument("fill"); - final FillController fill = fill(fillId); - Convert.interpretFillOptions(call.argument("options"), fill); - fill.update(fillManager); - result.success(null); - break; - } - case "locationComponent#getLastLocation": { - Log.e(TAG, "location component: getLastLocation"); - if (this.myLocationEnabled && locationComponent != null && locationEngine != null) { - Map reply = new HashMap<>(); - locationEngine.getLastLocation(new LocationEngineCallback() { - @Override - public void onSuccess(LocationEngineResult locationEngineResult) { - Location lastLocation = locationEngineResult.getLastLocation(); - if (lastLocation != null) { - reply.put("latitude", lastLocation.getLatitude()); - reply.put("longitude", lastLocation.getLongitude()); - reply.put("altitude", lastLocation.getAltitude()); - result.success(reply); - } else { - result.error("", "", null); // ??? - } - } - @Override - public void onFailure(@NonNull Exception exception) { - result.error("", "", null); // ??? - } - }); + case "style#removeSource": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + style.removeSource((String) call.argument("sourceId")); + result.success(null); + break; } - break; - } - case "style#addImage": { - if(style==null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + case "style#addLayer": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + addRasterLayer( + call.argument("imageLayerId"), + call.argument("imageSourceId"), + call.argument("minzoom") != null + ? ((Double) call.argument("minzoom")).floatValue() + : null, + call.argument("maxzoom") != null + ? ((Double) call.argument("maxzoom")).floatValue() + : null, + null, + new PropertyValue[] {}, + null); + result.success(null); + break; } - style.addImage(call.argument("name"), BitmapFactory.decodeByteArray(call.argument("bytes"),0,call.argument("length")), call.argument("sdf")); - result.success(null); - break; - } - case "style#addImageSource": { - if (style == null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + case "style#addLayerBelow": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + addRasterLayer( + call.argument("imageLayerId"), + call.argument("imageSourceId"), + call.argument("minzoom") != null + ? ((Double) call.argument("minzoom")).floatValue() + : null, + call.argument("maxzoom") != null + ? ((Double) call.argument("maxzoom")).floatValue() + : null, + call.argument("belowLayerId"), + new PropertyValue[] {}, + null); + result.success(null); + break; } - List coordinates = Convert.toLatLngList(call.argument("coordinates")); - style.addSource(new ImageSource(call.argument("imageSourceId"), new LatLngQuad(coordinates.get(0), coordinates.get(1), coordinates.get(2), coordinates.get(3)), BitmapFactory.decodeByteArray(call.argument("bytes"), 0, call.argument("length")))); - result.success(null); - break; - } - case "style#removeImageSource": { - if (style == null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + case "style#removeLayer": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + String layerId = call.argument("layerId"); + removeLayer(layerId); + + result.success(null); + break; } - style.removeSource((String) call.argument("imageSourceId")); - result.success(null); - break; - } - case "style#addLayer": { - if (style == null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + case "style#setFilter": + { + if (style == null) { + result.error( + "STYLE IS NULL", + "The style is null. Has onStyleLoaded() already been invoked?", + null); + } + String layerId = call.argument("layerId"); + String filter = call.argument("filter"); + + Layer layer = style.getLayer(layerId); + + JsonParser parser = new JsonParser(); + JsonElement jsonElement = parser.parse(filter); + Expression expression = Expression.Converter.convert(jsonElement); + + if (layer instanceof CircleLayer) { + ((CircleLayer) layer).setFilter(expression); + } else if (layer instanceof FillExtrusionLayer) { + ((FillExtrusionLayer) layer).setFilter(expression); + } else if (layer instanceof FillLayer) { + ((FillLayer) layer).setFilter(expression); + } else if (layer instanceof HeatmapLayer) { + ((HeatmapLayer) layer).setFilter(expression); + } else if (layer instanceof LineLayer) { + ((LineLayer) layer).setFilter(expression); + } else if (layer instanceof SymbolLayer) { + ((SymbolLayer) layer).setFilter(expression); + } else { + result.error( + "INVALID LAYER TYPE", + String.format("Layer '%s' does not support filtering.", layerId), + null); + break; + } + + result.success(null); + break; } - style.addLayer(new RasterLayer(call.argument("imageLayerId"), call.argument("imageSourceId"))); - result.success(null); - break; - } - case "style#addLayerBelow": { - if (style == null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + 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; } - style.addLayerBelow(new RasterLayer(call.argument("imageLayerId"), call.argument("imageSourceId")), call.argument("belowLayerId")); - result.success(null); - break; - } - case "style#removeLayer": { - if (style == null) { - result.error("STYLE IS NULL", "The style is null. Has onStyleLoaded() already been invoked?", null); + 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; } - style.removeLayer((String) call.argument("imageLayerId")); - result.success(null); - break; - } default: result.notImplemented(); } @@ -1028,73 +1475,26 @@ public void onCameraTrackingDismissed() { } @Override - public boolean onAnnotationClick(Annotation annotation) { - if (annotation instanceof Symbol) { - final SymbolController symbolController = symbols.get(String.valueOf(annotation.getId())); - if (symbolController != null) { - return symbolController.onTap(); - } - } - - if (annotation instanceof Line) { - final LineController lineController = lines.get(String.valueOf(annotation.getId())); - if (lineController != null) { - return lineController.onTap(); - } - } - - if (annotation instanceof Circle) { - final CircleController circleController = circles.get(String.valueOf(annotation.getId())); - if (circleController != null) { - return circleController.onTap(); - } - } - if (annotation instanceof Fill) { - final FillController fillController = fills.get(String.valueOf(annotation.getId())); - if (fillController != null) { - return fillController.onTap(); - } - } - return false; - } - - @Override - public void onSymbolTapped(Symbol symbol) { - final Map arguments = new HashMap<>(2); - arguments.put("symbol", String.valueOf(symbol.getId())); - methodChannel.invokeMethod("symbol#onTap", arguments); - } - - @Override - public void onLineTapped(Line line) { - final Map arguments = new HashMap<>(2); - arguments.put("line", String.valueOf(line.getId())); - methodChannel.invokeMethod("line#onTap", arguments); - } - - @Override - public void onCircleTapped(Circle circle) { - final Map arguments = new HashMap<>(2); - arguments.put("circle", String.valueOf(circle.getId())); - methodChannel.invokeMethod("circle#onTap", arguments); - } - - @Override - public void onFillTapped(Fill fill) { - final Map arguments = new HashMap<>(2); - arguments.put("fill", String.valueOf(fill.getId())); - methodChannel.invokeMethod("fill#onTap", arguments); + public void onDidBecomeIdle() { + methodChannel.invokeMethod("map#onIdle", new HashMap<>()); } @Override public boolean onMapClick(@NonNull LatLng point) { PointF pointf = mapboxMap.getProjection().toScreenLocation(point); - final Map arguments = new HashMap<>(5); + RectF rectF = new RectF(pointf.x - 10, pointf.y - 10, pointf.x + 10, pointf.y + 10); + Feature feature = firstFeatureOnLayers(rectF); + final Map arguments = new HashMap<>(); arguments.put("x", pointf.x); arguments.put("y", pointf.y); arguments.put("lng", point.getLongitude()); arguments.put("lat", point.getLatitude()); - methodChannel.invokeMethod("map#onMapClick", arguments); + if (feature != null) { + arguments.put("id", feature.id()); + methodChannel.invokeMethod("feature#onTap", arguments); + } else { + methodChannel.invokeMethod("map#onMapClick", arguments); + } return true; } @@ -1124,29 +1524,71 @@ public void dispose() { } } + private void moveCamera(CameraUpdate cameraUpdate, MethodChannel.Result result) { + if (cameraUpdate != null) { + // camera transformation not handled yet + mapboxMap.moveCamera( + cameraUpdate, + new OnCameraMoveFinishedListener() { + @Override + public void onFinish() { + super.onFinish(); + result.success(true); + } + + @Override + public void onCancel() { + super.onCancel(); + result.success(false); + } + }); + + // moveCamera(cameraUpdate); + } else { + result.success(false); + } + } + + private void animateCamera( + CameraUpdate cameraUpdate, Integer duration, MethodChannel.Result result) { + final OnCameraMoveFinishedListener onCameraMoveFinishedListener = + new OnCameraMoveFinishedListener() { + @Override + public void onFinish() { + super.onFinish(); + result.success(true); + } + + @Override + public void onCancel() { + super.onCancel(); + result.success(false); + } + }; + if (cameraUpdate != null && duration != null) { + // camera transformation not handled yet + mapboxMap.animateCamera(cameraUpdate, duration, onCameraMoveFinishedListener); + } else if (cameraUpdate != null) { + // camera transformation not handled yet + mapboxMap.animateCamera(cameraUpdate, onCameraMoveFinishedListener); + } else { + result.success(false); + } + } + private void destroyMapViewIfNecessary() { if (mapView == null) { return; } + mapView.onStop(); + mapView.onDestroy(); + if (locationComponent != null) { locationComponent.setLocationComponentEnabled(false); } - if (symbolManager != null) { - symbolManager.onDestroy(); - } - if (lineManager != null) { - lineManager.onDestroy(); - } - if (circleManager != null) { - circleManager.onDestroy(); - } - if (fillManager != null) { - fillManager.onDestroy(); - } stopListeningForLocationUpdates(); - mapView.onDestroy(); mapView = null; } @@ -1172,7 +1614,7 @@ public void onResume(@NonNull LifecycleOwner owner) { return; } mapView.onResume(); - if(myLocationEnabled){ + if (myLocationEnabled) { startListeningForLocationUpdates(); } } @@ -1182,7 +1624,7 @@ public void onPause(@NonNull LifecycleOwner owner) { if (disposed) { return; } - mapView.onResume(); + mapView.onPause(); } @Override @@ -1206,7 +1648,7 @@ public void onDestroy(@NonNull LifecycleOwner owner) { @Override public void setCameraTargetBounds(LatLngBounds bounds) { - mapboxMap.setLatLngBoundsForCameraTarget(bounds); + this.bounds = bounds; } @Override @@ -1258,6 +1700,10 @@ public void setMyLocationEnabled(boolean myLocationEnabled) { @Override public void setMyLocationTrackingMode(int myLocationTrackingMode) { + if (mapboxMap != null) { + // ensure that location is trackable + updateMyLocationEnabled(); + } if (this.myLocationTrackingMode == myLocationTrackingMode) { return; } @@ -1284,7 +1730,7 @@ public void setLogoViewMargins(int x, int y) { @Override public void setCompassGravity(int gravity) { - switch(gravity) { + switch (gravity) { case 0: mapboxMap.getUiSettings().setCompassGravity(Gravity.TOP | Gravity.START); break; @@ -1303,8 +1749,7 @@ public void setCompassGravity(int gravity) { @Override public void setCompassViewMargins(int x, int y) { - switch(mapboxMap.getUiSettings().getCompassGravity()) - { + switch (mapboxMap.getUiSettings().getCompassGravity()) { case Gravity.TOP | Gravity.START: mapboxMap.getUiSettings().setCompassMargins(x, y, 0, 0); break; @@ -1321,50 +1766,95 @@ public void setCompassViewMargins(int x, int y) { } } + @Override + public void setAttributionButtonGravity(int gravity) { + switch (gravity) { + case 0: + mapboxMap.getUiSettings().setAttributionGravity(Gravity.TOP | Gravity.START); + break; + default: + case 1: + mapboxMap.getUiSettings().setAttributionGravity(Gravity.TOP | Gravity.END); + break; + case 2: + mapboxMap.getUiSettings().setAttributionGravity(Gravity.BOTTOM | Gravity.START); + break; + case 3: + mapboxMap.getUiSettings().setAttributionGravity(Gravity.BOTTOM | Gravity.END); + break; + } + } + @Override public void setAttributionButtonMargins(int x, int y) { - mapboxMap.getUiSettings().setAttributionMargins(0, 0, x, y); + switch (mapboxMap.getUiSettings().getAttributionGravity()) { + case Gravity.TOP | Gravity.START: + mapboxMap.getUiSettings().setAttributionMargins(x, y, 0, 0); + break; + default: + case Gravity.TOP | Gravity.END: + mapboxMap.getUiSettings().setAttributionMargins(0, y, x, 0); + break; + case Gravity.BOTTOM | Gravity.START: + mapboxMap.getUiSettings().setAttributionMargins(x, 0, 0, y); + break; + case Gravity.BOTTOM | Gravity.END: + mapboxMap.getUiSettings().setAttributionMargins(0, 0, x, y); + break; + } } private void updateMyLocationEnabled() { - if(this.locationComponent == null && myLocationEnabled){ + if (this.locationComponent == null && myLocationEnabled) { enableLocationComponent(mapboxMap.getStyle()); } - if(myLocationEnabled){ + if (myLocationEnabled) { startListeningForLocationUpdates(); - }else { + } else { stopListeningForLocationUpdates(); } - locationComponent.setLocationComponentEnabled(myLocationEnabled); + if (locationComponent != null) { + locationComponent.setLocationComponentEnabled(myLocationEnabled); + } } - private void startListeningForLocationUpdates(){ - if(locationEngineCallback == null && locationComponent!=null && locationComponent.getLocationEngine()!=null){ - locationEngineCallback = new LocationEngineCallback() { - @Override - public void onSuccess(LocationEngineResult result) { - onUserLocationUpdate(result.getLastLocation()); - } + private void startListeningForLocationUpdates() { + if (locationEngineCallback == null + && locationComponent != null + && locationComponent.getLocationEngine() != null) { + locationEngineCallback = + new LocationEngineCallback() { + @Override + public void onSuccess(LocationEngineResult result) { + onUserLocationUpdate(result.getLastLocation()); + } - @Override - public void onFailure(@NonNull Exception exception) { - } - }; - locationComponent.getLocationEngine().requestLocationUpdates(locationComponent.getLocationEngineRequest(), locationEngineCallback , null); + @Override + public void onFailure(@NonNull Exception exception) {} + }; + locationComponent + .getLocationEngine() + .requestLocationUpdates( + locationComponent.getLocationEngineRequest(), locationEngineCallback, null); } } - private void stopListeningForLocationUpdates(){ - if(locationEngineCallback != null && locationComponent!=null && locationComponent.getLocationEngine()!=null){ + private void stopListeningForLocationUpdates() { + if (locationEngineCallback != null + && locationComponent != null + && locationComponent.getLocationEngine() != null) { locationComponent.getLocationEngine().removeLocationUpdates(locationEngineCallback); locationEngineCallback = null; } } private void updateMyLocationTrackingMode() { - int[] mapboxTrackingModes = new int[] {CameraMode.NONE, CameraMode.TRACKING, CameraMode.TRACKING_COMPASS, CameraMode.TRACKING_GPS}; + int[] mapboxTrackingModes = + new int[] { + CameraMode.NONE, CameraMode.TRACKING, CameraMode.TRACKING_COMPASS, CameraMode.TRACKING_GPS + }; locationComponent.setCameraMode(mapboxTrackingModes[this.myLocationTrackingMode]); } @@ -1375,9 +1865,9 @@ private void updateMyLocationRenderMode() { private boolean hasLocationPermission() { return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED - || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED; + == PackageManager.PERMISSION_GRANTED + || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED; } private int checkSelfPermission(String permission) { @@ -1385,11 +1875,12 @@ private int checkSelfPermission(String permission) { throw new IllegalArgumentException("permission is null"); } return context.checkPermission( - permission, android.os.Process.myPid(), android.os.Process.myUid()); + permission, android.os.Process.myPid(), android.os.Process.myUid()); } /** * Tries to find highest scale image for display type + * * @param imageId * @param density * @return @@ -1420,7 +1911,7 @@ private Bitmap getScaledImage(String imageId, float density) { } stringBuilder.append(((float) i) + "x"); stringBuilder.append("/"); - stringBuilder.append(imagePathList.get(imagePathList.size()-1)); + stringBuilder.append(imagePathList.get(imagePathList.size() - 1)); assetPath = MapboxMapsPlugin.flutterAssets.getAssetFilePathByName(stringBuilder.toString()); } // Build up a list of resolution aware asset paths. @@ -1444,16 +1935,114 @@ private Bitmap getScaledImage(String imageId, float density) { return bitmap; } - /** - * Simple Listener to listen for the status of camera movements. - */ - public class OnCameraMoveFinishedListener implements MapboxMap.CancelableCallback{ + boolean onMoveBegin(MoveGestureDetector detector) { + // onMoveBegin gets called even during a move - move end is also not called unless this function + // returns + // true at least once. To avoid redundant queries only check for feature if the previous event + // was ACTION_DOWN + if (detector.getPreviousEvent().getActionMasked() == MotionEvent.ACTION_DOWN + && detector.getPointersCount() == 1) { + PointF pointf = detector.getFocalPoint(); + LatLng origin = mapboxMap.getProjection().fromScreenLocation(pointf); + RectF rectF = new RectF(pointf.x - 10, pointf.y - 10, pointf.x + 10, pointf.y + 10); + Feature feature = firstFeatureOnLayers(rectF); + if (feature != null && startDragging(feature, origin)) { + invokeFeatureDrag(pointf, "start"); + return true; + } + } + return false; + } + + private void invokeFeatureDrag(PointF pointf, String eventType) { + LatLng current = mapboxMap.getProjection().fromScreenLocation(pointf); + + final Map arguments = new HashMap<>(9); + arguments.put("id", draggedFeature.id()); + arguments.put("x", pointf.x); + arguments.put("y", pointf.y); + arguments.put("originLng", dragOrigin.getLongitude()); + arguments.put("originLat", dragOrigin.getLatitude()); + arguments.put("currentLng", current.getLongitude()); + arguments.put("currentLat", current.getLatitude()); + arguments.put("eventType", eventType); + arguments.put("deltaLng", current.getLongitude() - dragPrevious.getLongitude()); + arguments.put("deltaLat", current.getLatitude() - dragPrevious.getLatitude()); + dragPrevious = current; + methodChannel.invokeMethod("feature#onDrag", arguments); + } + + boolean onMove(MoveGestureDetector detector) { + if (draggedFeature != null) { + if (detector.getPointersCount() > 1) { + stopDragging(); + return true; + } + PointF pointf = detector.getFocalPoint(); + invokeFeatureDrag(pointf, "drag"); + return false; + } + 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"); + stopDragging(); + } + + boolean startDragging(@NonNull Feature feature, @NonNull LatLng origin) { + final boolean draggable = + feature.hasNonNullValueForProperty("draggable") + ? feature.getBooleanProperty("draggable") + : false; + if (draggable) { + draggedFeature = feature; + dragPrevious = origin; + dragOrigin = origin; + return true; + } + return false; + } + + void stopDragging() { + draggedFeature = null; + dragOrigin = null; + dragPrevious = null; + } + + /** Simple Listener to listen for the status of camera movements. */ + public class OnCameraMoveFinishedListener implements MapboxMap.CancelableCallback { + + @Override + public void onFinish() {} + + @Override + public void onCancel() {} + } + + private class MoveGestureListener implements MoveGestureDetector.OnMoveGestureListener { + + @Override + public boolean onMoveBegin(MoveGestureDetector detector) { + return MapboxMapController.this.onMoveBegin(detector); + } + @Override - public void onFinish() { + public boolean onMove(MoveGestureDetector detector, float distanceX, float distanceY) { + return MapboxMapController.this.onMove(detector); } @Override - public void onCancel() { + public void onMoveEnd(MoveGestureDetector detector, float velocityX, float velocityY) { + MapboxMapController.this.onMoveEnd(detector); } } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java index 5a1b6c605..625caf9a8 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java @@ -1,25 +1,20 @@ package com.mapbox.mapboxgl; import android.content.Context; - import com.mapbox.mapboxsdk.camera.CameraPosition; - import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; - import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.List; -import java.util.ArrayList; public class MapboxMapFactory extends PlatformViewFactory { private final BinaryMessenger messenger; private final MapboxMapsPlugin.LifecycleProvider lifecycleProvider; - public MapboxMapFactory(BinaryMessenger messenger, MapboxMapsPlugin.LifecycleProvider lifecycleProvider) { + public MapboxMapFactory( + BinaryMessenger messenger, MapboxMapsPlugin.LifecycleProvider lifecycleProvider) { super(StandardMessageCodec.INSTANCE); this.messenger = messenger; this.lifecycleProvider = lifecycleProvider; @@ -30,19 +25,17 @@ public PlatformView create(Context context, int id, Object args) { Map params = (Map) args; final MapboxMapBuilder builder = new MapboxMapBuilder(); - Convert.interpretMapboxMapOptions(params.get("options"), builder); + Convert.interpretMapboxMapOptions(params.get("options"), builder, context); if (params.containsKey("initialCameraPosition")) { CameraPosition position = Convert.toCameraPosition(params.get("initialCameraPosition")); builder.setInitialCameraPosition(position); } - if (params.containsKey("annotationOrder")) { - List annotations = Convert.toAnnotationOrder(params.get("annotationOrder")); - builder.setAnnotationOrder(annotations); + if (params.containsKey("dragEnabled")) { + boolean dragEnabled = Convert.toBoolean(params.get("dragEnabled")); + builder.setDragEnabled(dragEnabled); } - if (params.containsKey("annotationConsumeTapEvents")) { - List annotations = Convert.toAnnotationConsumeTapEvents(params.get("annotationConsumeTapEvents")); - builder.setAnnotationConsumeTapEvents(annotations); - } - return builder.build(id, context, messenger, lifecycleProvider, (String) params.get("accessToken")); + + return builder.build( + id, context, messenger, lifecycleProvider, (String) params.get("accessToken")); } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapOptionsSink.java index 7072b4b70..bcc8e6b48 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapOptionsSink.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapOptionsSink.java @@ -6,11 +6,10 @@ import com.mapbox.mapboxsdk.geometry.LatLngBounds; -/** - * Receiver of MapboxMap configuration options. - */ +/** Receiver of MapboxMap configuration options. */ interface MapboxMapOptionsSink { - void setCameraTargetBounds(LatLngBounds bounds); //todo: dddd replace with CameraPosition.Builder target + void setCameraTargetBounds( + LatLngBounds bounds); // todo: dddd replace with CameraPosition.Builder target void setCompassEnabled(boolean compassEnabled); @@ -41,5 +40,7 @@ interface MapboxMapOptionsSink { void setCompassViewMargins(int x, int y); + void setAttributionButtonGravity(int gravity); + void setAttributionButtonMargins(int x, int y); } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java index 7014b1e41..8eac1fdfe 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java @@ -7,19 +7,16 @@ import android.app.Activity; import android.app.Application; import android.os.Bundle; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; - import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; 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 @@ -44,19 +41,23 @@ public MapboxMapsPlugin() { public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { flutterAssets = binding.getFlutterAssets(); - MethodChannel methodChannel = new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/mapbox_gl"); + MethodChannel methodChannel = + new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/mapbox_gl"); methodChannel.setMethodCallHandler(new GlobalMethodHandler(binding)); binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/mapbox_gl", new MapboxMapFactory(binding.getBinaryMessenger(), new LifecycleProvider() { - @Nullable - @Override - public Lifecycle getLifecycle() { - return lifecycle; - } - })); + .getPlatformViewRegistry() + .registerViewFactory( + "plugins.flutter.io/mapbox_gl", + new MapboxMapFactory( + binding.getBinaryMessenger(), + new LifecycleProvider() { + @Nullable + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + })); } @Override @@ -84,45 +85,8 @@ 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 { + implements Application.ActivityLifecycleCallbacks, LifecycleOwner, LifecycleProvider { private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); private final int registrarActivityHashCode; @@ -207,9 +171,9 @@ public static class FlutterLifecycleAdapter { */ @NonNull public static Lifecycle getActivityLifecycle( - @NonNull ActivityPluginBinding activityPluginBinding) { + @NonNull ActivityPluginBinding activityPluginBinding) { HiddenLifecycleReference reference = - (HiddenLifecycleReference) activityPluginBinding.getLifecycle(); + (HiddenLifecycleReference) activityPluginBinding.getLifecycle(); return reference.getLifecycle(); } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/OfflineChannelHandlerImpl.java b/android/src/main/java/com/mapbox/mapboxgl/OfflineChannelHandlerImpl.java index 322376c3a..64164f792 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/OfflineChannelHandlerImpl.java +++ b/android/src/main/java/com/mapbox/mapboxgl/OfflineChannelHandlerImpl.java @@ -1,58 +1,55 @@ package com.mapbox.mapboxgl; import androidx.annotation.Nullable; - import com.google.gson.Gson; - -import java.util.HashMap; -import java.util.Map; - import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; +import java.util.HashMap; +import java.util.Map; public class OfflineChannelHandlerImpl implements EventChannel.StreamHandler { - private EventChannel.EventSink sink; - private Gson gson = new Gson(); - - OfflineChannelHandlerImpl(BinaryMessenger messenger, String channelName) { - EventChannel eventChannel = new EventChannel(messenger, channelName); - eventChannel.setStreamHandler(this); - } - - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - sink = events; - } - - @Override - public void onCancel(Object arguments) { - sink = null; - } - - void onError(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (sink == null) return; - sink.error(errorCode, errorMessage, errorDetails); - } - - void onSuccess() { - if (sink == null) return; - Map body = new HashMap<>(); - body.put("status", "success"); - sink.success(gson.toJson(body)); - } - - void onStart() { - if (sink == null) return; - Map body = new HashMap<>(); - body.put("status", "start"); - sink.success(gson.toJson(body)); - } - - void onProgress(double progress) { - if (sink == null) return; - Map body = new HashMap<>(); - body.put("status", "progress"); - body.put("progress", progress); - sink.success(gson.toJson(body)); - } + private EventChannel.EventSink sink; + private Gson gson = new Gson(); + + OfflineChannelHandlerImpl(BinaryMessenger messenger, String channelName) { + EventChannel eventChannel = new EventChannel(messenger, channelName); + eventChannel.setStreamHandler(this); + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + sink = events; + } + + @Override + public void onCancel(Object arguments) { + sink = null; + } + + void onError(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (sink == null) return; + sink.error(errorCode, errorMessage, errorDetails); + } + + void onSuccess() { + if (sink == null) return; + Map body = new HashMap<>(); + body.put("status", "success"); + sink.success(gson.toJson(body)); + } + + void onStart() { + if (sink == null) return; + Map body = new HashMap<>(); + body.put("status", "start"); + sink.success(gson.toJson(body)); + } + + void onProgress(double progress) { + if (sink == null) return; + Map body = new HashMap<>(); + body.put("status", "progress"); + body.put("progress", progress); + sink.success(gson.toJson(body)); + } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/OfflineManagerUtils.java b/android/src/main/java/com/mapbox/mapboxgl/OfflineManagerUtils.java index e451e7022..92e469a09 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/OfflineManagerUtils.java +++ b/android/src/main/java/com/mapbox/mapboxgl/OfflineManagerUtils.java @@ -2,293 +2,334 @@ import android.content.Context; import android.util.Log; - import com.google.gson.Gson; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; import com.mapbox.mapboxsdk.offline.OfflineManager; import com.mapbox.mapboxsdk.offline.OfflineRegion; import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition; -import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition; import com.mapbox.mapboxsdk.offline.OfflineRegionError; import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; - -import java.util.Arrays; +import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition; +import io.flutter.plugin.common.MethodChannel; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import io.flutter.plugin.common.MethodChannel; - abstract class OfflineManagerUtils { - private static final String TAG = "OfflineManagerUtils"; - - static void mergeRegions(MethodChannel.Result result, Context context, String path) { - OfflineManager.getInstance(context).mergeOfflineRegions(path, new OfflineManager.MergeOfflineRegionsCallback() { - public void onMerge(OfflineRegion[] offlineRegions) { + private static final String TAG = "OfflineManagerUtils"; + + static void mergeRegions(MethodChannel.Result result, Context context, String path) { + OfflineManager.getInstance(context) + .mergeOfflineRegions( + path, + new OfflineManager.MergeOfflineRegionsCallback() { + public void onMerge(OfflineRegion[] offlineRegions) { if (result == null) return; List> regionsArgs = new ArrayList<>(); for (OfflineRegion offlineRegion : offlineRegions) { - regionsArgs.add(offlineRegionToMap(offlineRegion)); + regionsArgs.add(offlineRegionToMap(offlineRegion)); } String json = new Gson().toJson(regionsArgs); result.success(json); - } + } - public void onError(String error) { + public void onError(String error) { if (result == null) return; result.error("mergeOfflineRegions Error", error, null); - } - }); + } + }); + } + + static void setOfflineTileCountLimit(MethodChannel.Result result, Context context, long limit) { + OfflineManager.getInstance(context).setOfflineMapboxTileCountLimit(limit); + result.success(null); + } + + static void downloadRegion( + MethodChannel.Result result, + Context context, + Map definitionMap, + Map metadataMap, + OfflineChannelHandlerImpl channelHandler) { + float pixelDensity = context.getResources().getDisplayMetrics().density; + OfflineRegionDefinition definition = mapToRegionDefinition(definitionMap, pixelDensity); + String metadata = "{}"; + if (metadataMap != null) { + metadata = new Gson().toJson(metadataMap); } - - static void setOfflineTileCountLimit(MethodChannel.Result result, Context context, long limit){ - OfflineManager.getInstance(context).setOfflineMapboxTileCountLimit(limit); - result.success(null); - } - - static void downloadRegion( - MethodChannel.Result result, - Context context, - Map definitionMap, - Map metadataMap, - OfflineChannelHandlerImpl channelHandler - ) { - float pixelDensity = context.getResources().getDisplayMetrics().density; - OfflineRegionDefinition definition = mapToRegionDefinition(definitionMap, pixelDensity); - String metadata = "{}"; - if (metadataMap != null) { - metadata = new Gson().toJson(metadataMap); - } - AtomicBoolean isComplete = new AtomicBoolean(false); - //Download region - OfflineManager.getInstance(context).createOfflineRegion(definition, metadata.getBytes(), new OfflineManager.CreateOfflineRegionCallback() { - private OfflineRegion _offlineRegion; - - @Override - public void onCreate(OfflineRegion offlineRegion) { + AtomicBoolean isComplete = new AtomicBoolean(false); + // Download region + OfflineManager.getInstance(context) + .createOfflineRegion( + definition, + metadata.getBytes(), + new OfflineManager.CreateOfflineRegionCallback() { + private OfflineRegion _offlineRegion; + + @Override + public void onCreate(OfflineRegion offlineRegion) { Map regionData = offlineRegionToMap(offlineRegion); result.success(new Gson().toJson(regionData)); _offlineRegion = offlineRegion; - //Start downloading region + // Start downloading region _offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE); channelHandler.onStart(); - //Observe downloading state - OfflineRegion.OfflineRegionObserver observer = new OfflineRegion.OfflineRegionObserver() { - @Override - public void onStatusChanged(OfflineRegionStatus status) { - //Calculate progress of downloading - double progress = calculateDownloadingProgress(status.getRequiredResourceCount(), status.getCompletedResourceCount()); - //Check if downloading is complete + // Observe downloading state + OfflineRegion.OfflineRegionObserver observer = + new OfflineRegion.OfflineRegionObserver() { + @Override + public void onStatusChanged(OfflineRegionStatus status) { + // Calculate progress of + // downloading + double progress = + calculateDownloadingProgress( + status.getRequiredResourceCount(), + status.getCompletedResourceCount()); + // Check if downloading is + // complete if (status.isComplete()) { - Log.i(TAG, "Region downloaded successfully."); - //Reset downloading state - _offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); - //This can be called multiple times, and result can be called only once, so there is need to prevent it - if (isComplete.get()) return; - isComplete.set(true); - channelHandler.onSuccess(); + Log.i(TAG, "Region " + "downloaded " + "successfully."); + // Reset downloading state + _offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); + // This can be called + // multiple times, and + // result can be called + // only once, + // so there is need to + // prevent it + if (isComplete.get()) return; + isComplete.set(true); + channelHandler.onSuccess(); } else { - Log.i(TAG, "Region download progress = " + progress); - channelHandler.onProgress(progress); + Log.i(TAG, "Region " + "download " + "progress = " + progress); + channelHandler.onProgress(progress); } - } + } - @Override - public void onError(OfflineRegionError error) { + @Override + public void onError(OfflineRegionError error) { Log.e(TAG, "onError reason: " + error.getReason()); Log.e(TAG, "onError message: " + error.getMessage()); - //Reset downloading state + // Reset downloading state _offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); isComplete.set(true); - channelHandler.onError("Downloading error", error.getMessage(), error.getReason()); - } - - @Override - public void mapboxTileCountLimitExceeded(long limit) { - Log.e(TAG, "Mapbox tile count limit exceeded: " + limit); - //Reset downloading state + channelHandler.onError( + "Downloading error", error.getMessage(), error.getReason()); + } + + @Override + public void mapboxTileCountLimitExceeded(long limit) { + Log.e(TAG, "Mapbox tile count" + " limit exceeded: " + limit); + // Reset downloading state _offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); isComplete.set(true); - channelHandler.onError("mapboxTileCountLimitExceeded", "Mapbox tile count limit exceeded: " + limit, null); - //Mapbox even after crash and not downloading fully region still keeps part of it in database, so we have to remove it + channelHandler.onError( + "mapboxTileCountLimitExceeded", + "Mapbox tile count " + "limit " + "exceeded: " + limit, + null); + // Mapbox even after crash + // and not downloading fully + // region still keeps part + // of it in database, so we + // have to remove it deleteRegion(null, context, _offlineRegion.getID()); - } - }; + } + }; _offlineRegion.setObserver(observer); - } - - /** - * This will be call if given region definition is invalid - * @param error - **/ - @Override - public void onError(String error) { + } + + /** + * This will be call if given region definition is invalid + * + * @param error + */ + @Override + public void onError(String error) { Log.e(TAG, "Error: " + error); - //Reset downloading state + // Reset downloading state _offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); channelHandler.onError("mapboxInvalidRegionDefinition", error, null); result.error("mapboxInvalidRegionDefinition", error, null); - } - }); - } - - static void regionsList(MethodChannel.Result result, Context context) { - OfflineManager.getInstance(context).listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() { - @Override - public void onList(OfflineRegion[] offlineRegions) { + } + }); + } + + static void regionsList(MethodChannel.Result result, Context context) { + OfflineManager.getInstance(context) + .listOfflineRegions( + new OfflineManager.ListOfflineRegionsCallback() { + @Override + public void onList(OfflineRegion[] offlineRegions) { List> regionsArgs = new ArrayList<>(); for (OfflineRegion offlineRegion : offlineRegions) { - regionsArgs.add(offlineRegionToMap(offlineRegion)); + regionsArgs.add(offlineRegionToMap(offlineRegion)); } result.success(new Gson().toJson(regionsArgs)); - } + } - @Override - public void onError(String error) { + @Override + public void onError(String error) { result.error("RegionListError", error, null); - } - }); - } - - static void updateRegionMetadata(MethodChannel.Result result, Context context, long id, Map metadataMap) { - OfflineManager.getInstance(context).listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() { - @Override - public void onList(OfflineRegion[] offlineRegions) { + } + }); + } + + static void updateRegionMetadata( + MethodChannel.Result result, Context context, long id, Map metadataMap) { + OfflineManager.getInstance(context) + .listOfflineRegions( + new OfflineManager.ListOfflineRegionsCallback() { + @Override + public void onList(OfflineRegion[] offlineRegions) { for (OfflineRegion offlineRegion : offlineRegions) { - if (offlineRegion.getID() != id) continue; - - String metadata = "{}"; - if (metadataMap != null) { - metadata = new Gson().toJson(metadataMap); - } - offlineRegion.updateMetadata(metadata.getBytes(), new OfflineRegion.OfflineRegionUpdateMetadataCallback() { + if (offlineRegion.getID() != id) continue; + + String metadata = "{}"; + if (metadataMap != null) { + metadata = new Gson().toJson(metadataMap); + } + offlineRegion.updateMetadata( + metadata.getBytes(), + new OfflineRegion.OfflineRegionUpdateMetadataCallback() { @Override public void onUpdate(byte[] metadataBytes) { - Map regionData = offlineRegionToMap(offlineRegion); - regionData.put("metadata", metadataBytesToMap(metadataBytes)); + Map regionData = offlineRegionToMap(offlineRegion); + regionData.put("metadata", metadataBytesToMap(metadataBytes)); - if (result == null) return; - result.success(new Gson().toJson(regionData)); + if (result == null) return; + result.success(new Gson().toJson(regionData)); } @Override public void onError(String error) { - if (result == null) return; - result.error("UpdateMetadataError", error, null); + if (result == null) return; + result.error("UpdateMetadataError", error, null); } - }); - return; + }); + return; } if (result == null) return; - result.error("UpdateMetadataError", "There is no region with given id to update.", null); - } - - @Override - public void onError(String error) { + result.error( + "UpdateMetadataError", + "There is no " + "region with given id to " + "update.", + null); + } + + @Override + public void onError(String error) { if (result == null) return; result.error("RegionListError", error, null); - } - }); - } - - static void deleteRegion(MethodChannel.Result result, Context context, long id) { - OfflineManager.getInstance(context).listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() { - @Override - public void onList(OfflineRegion[] offlineRegions) { + } + }); + } + + static void deleteRegion(MethodChannel.Result result, Context context, long id) { + OfflineManager.getInstance(context) + .listOfflineRegions( + new OfflineManager.ListOfflineRegionsCallback() { + @Override + public void onList(OfflineRegion[] offlineRegions) { for (OfflineRegion offlineRegion : offlineRegions) { - if (offlineRegion.getID() != id) continue; + if (offlineRegion.getID() != id) continue; - offlineRegion.delete(new OfflineRegion.OfflineRegionDeleteCallback() { + offlineRegion.delete( + new OfflineRegion.OfflineRegionDeleteCallback() { @Override public void onDelete() { - if (result == null) return; - result.success(null); + if (result == null) return; + result.success(null); } @Override public void onError(String error) { - if (result == null) return; - result.error("DeleteRegionError", error, null); + if (result == null) return; + result.error("DeleteRegionError", error, null); } - }); - return; + }); + return; } if (result == null) return; - result.error("DeleteRegionError", "There is no region with given id to delete.", null); - } - - @Override - public void onError(String error) { + result.error( + "DeleteRegionError", + "There is no " + "region with given id to " + "delete.", + null); + } + + @Override + public void onError(String error) { if (result == null) return; result.error("RegionListError", error, null); - } - }); - } - - private static double calculateDownloadingProgress(long requiredResourceCount, long completedResourceCount) { - return requiredResourceCount > 0 - ? (100.0 * completedResourceCount / requiredResourceCount) : - 0.0; - } - - private static OfflineRegionDefinition mapToRegionDefinition(Map map, float pixelDensity) { - for (Map.Entry entry : map.entrySet()) { - Log.d(TAG, entry.getKey()); - Log.d(TAG, entry.getValue().toString()); - } - // Create a bounding box for the offline region - return new OfflineTilePyramidRegionDefinition( - (String) map.get("mapStyleUrl"), - listToBounds((List>) map.get("bounds")), - ((Number) map.get("minZoom")).doubleValue(), - ((Number) map.get("maxZoom")).doubleValue(), - pixelDensity, - (Boolean) map.get("includeIdeographs") - ); + } + }); + } + + private static double calculateDownloadingProgress( + long requiredResourceCount, long completedResourceCount) { + return requiredResourceCount > 0 + ? (100.0 * completedResourceCount / requiredResourceCount) + : 0.0; + } + + private static OfflineRegionDefinition mapToRegionDefinition( + Map map, float pixelDensity) { + for (Map.Entry entry : map.entrySet()) { + Log.d(TAG, entry.getKey()); + Log.d(TAG, entry.getValue().toString()); } - - private static LatLngBounds listToBounds(List> bounds) { - return new LatLngBounds.Builder() - .include(new LatLng(bounds.get(1).get(0), bounds.get(1).get(1))) //Northeast - .include(new LatLng(bounds.get(0).get(0), bounds.get(0).get(1))) //Southwest - .build(); - } - - private static Map offlineRegionToMap(OfflineRegion region) { - Map result = new HashMap(); - result.put("id", region.getID()); - result.put("definition", offlineRegionDefinitionToMap(region.getDefinition())); - result.put("metadata", metadataBytesToMap(region.getMetadata())); - return result; - } - - private static Map offlineRegionDefinitionToMap(OfflineRegionDefinition definition) { - Map result = new HashMap(); - result.put("mapStyleUrl", definition.getStyleURL()); - result.put("bounds", boundsToList(definition.getBounds())); - result.put("minZoom", definition.getMinZoom()); - result.put("maxZoom", definition.getMaxZoom()); - result.put("includeIdeographs", definition.getIncludeIdeographs()); - return result; - } - - private static List> boundsToList(LatLngBounds bounds) { - List> boundsList = new ArrayList<>(); - List northeast = Arrays.asList(bounds.getLatNorth(), bounds.getLonEast()); - List southwest = Arrays.asList(bounds.getLatSouth(), bounds.getLonWest()); - boundsList.add(southwest); - boundsList.add(northeast); - return boundsList; - } - - private static Map metadataBytesToMap(byte[] metadataBytes) { - if (metadataBytes != null) { - return new Gson().fromJson(new String(metadataBytes), HashMap.class); - } - return new HashMap(); + // Create a bounding box for the offline region + return new OfflineTilePyramidRegionDefinition( + (String) map.get("mapStyleUrl"), + listToBounds((List>) map.get("bounds")), + ((Number) map.get("minZoom")).doubleValue(), + ((Number) map.get("maxZoom")).doubleValue(), + pixelDensity, + (Boolean) map.get("includeIdeographs")); + } + + private static LatLngBounds listToBounds(List> bounds) { + return new LatLngBounds.Builder() + .include(new LatLng(bounds.get(1).get(0), bounds.get(1).get(1))) // Northeast + .include(new LatLng(bounds.get(0).get(0), bounds.get(0).get(1))) // Southwest + .build(); + } + + private static Map offlineRegionToMap(OfflineRegion region) { + Map result = new HashMap(); + result.put("id", region.getID()); + result.put("definition", offlineRegionDefinitionToMap(region.getDefinition())); + result.put("metadata", metadataBytesToMap(region.getMetadata())); + return result; + } + + private static Map offlineRegionDefinitionToMap( + OfflineRegionDefinition definition) { + Map result = new HashMap(); + result.put("mapStyleUrl", definition.getStyleURL()); + result.put("bounds", boundsToList(definition.getBounds())); + result.put("minZoom", definition.getMinZoom()); + result.put("maxZoom", definition.getMaxZoom()); + result.put("includeIdeographs", definition.getIncludeIdeographs()); + return result; + } + + private static List> boundsToList(LatLngBounds bounds) { + List> boundsList = new ArrayList<>(); + List northeast = Arrays.asList(bounds.getLatNorth(), bounds.getLonEast()); + List southwest = Arrays.asList(bounds.getLatSouth(), bounds.getLonWest()); + boundsList.add(southwest); + boundsList.add(northeast); + return boundsList; + } + + private static Map metadataBytesToMap(byte[] metadataBytes) { + if (metadataBytes != null) { + return new Gson().fromJson(new String(metadataBytes), HashMap.class); } + return new HashMap(); + } } diff --git a/android/src/main/java/com/mapbox/mapboxgl/OnCircleTappedListener.java b/android/src/main/java/com/mapbox/mapboxgl/OnCircleTappedListener.java deleted file mode 100644 index c2988d596..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/OnCircleTappedListener.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.plugins.annotation.Circle; - -interface OnCircleTappedListener { - void onCircleTapped(Circle circle); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java b/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java deleted file mode 100644 index 27eb86425..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.plugins.annotation.Fill; - -public interface OnFillTappedListener { - void onFillTapped(Fill fill); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/OnLineTappedListener.java b/android/src/main/java/com/mapbox/mapboxgl/OnLineTappedListener.java deleted file mode 100644 index 325f5277c..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/OnLineTappedListener.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.plugins.annotation.Line; - -interface OnLineTappedListener { - void onLineTapped(Line line); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/OnSymbolTappedListener.java b/android/src/main/java/com/mapbox/mapboxgl/OnSymbolTappedListener.java deleted file mode 100644 index 9e567e74a..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/OnSymbolTappedListener.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.plugins.annotation.Symbol; - -interface OnSymbolTappedListener { - void onSymbolTapped(Symbol symbol); -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/SourcePropertyConverter.java b/android/src/main/java/com/mapbox/mapboxgl/SourcePropertyConverter.java new file mode 100644 index 000000000..308ffab01 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/SourcePropertyConverter.java @@ -0,0 +1,220 @@ +package com.mapbox.mapboxgl; + +import android.net.Uri; +import com.google.gson.Gson; +import com.mapbox.geojson.FeatureCollection; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngQuad; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; +import com.mapbox.mapboxsdk.style.sources.ImageSource; +import com.mapbox.mapboxsdk.style.sources.RasterDemSource; +import com.mapbox.mapboxsdk.style.sources.RasterSource; +import com.mapbox.mapboxsdk.style.sources.Source; +import com.mapbox.mapboxsdk.style.sources.TileSet; +import com.mapbox.mapboxsdk.style.sources.VectorSource; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class SourcePropertyConverter { + private static final String TAG = "SourcePropertyConverter"; + + static TileSet buildTileset(Map data) { + final Object tiles = data.get("tiles"); + + // options are only valid with tiles + if (tiles == null) { + return null; + } + + final TileSet tileSet = + new TileSet("2.1.0", (String[]) Convert.toList(tiles).toArray(new String[0])); + + final Object bounds = data.get("bounds"); + if (bounds != null) { + List boundsFloat = new ArrayList(); + for (Object item : Convert.toList(bounds)) { + boundsFloat.add(Convert.toFloat(item)); + } + tileSet.setBounds(boundsFloat.toArray(new Float[0])); + } + + final Object scheme = data.get("scheme"); + if (scheme != null) { + tileSet.setScheme(Convert.toString(scheme)); + } + + final Object minzoom = data.get("minzoom"); + if (minzoom != null) { + tileSet.setMinZoom(Convert.toFloat(minzoom)); + } + + final Object maxzoom = data.get("maxzoom"); + if (maxzoom != null) { + tileSet.setMaxZoom(Convert.toFloat(maxzoom)); + } + + final Object attribution = data.get("attribution"); + if (attribution != null) { + tileSet.setAttribution(Convert.toString(attribution)); + } + return tileSet; + } + + static GeoJsonOptions buildGeojsonOptions(Map data) { + GeoJsonOptions options = new GeoJsonOptions(); + + final Object buffer = data.get("buffer"); + if (buffer != null) { + options = options.withBuffer(Convert.toInt(buffer)); + } + + final Object cluster = data.get("cluster"); + if (cluster != null) { + options = options.withCluster(Convert.toBoolean(cluster)); + } + + final Object clusterMaxZoom = data.get("clusterMaxZoom"); + if (clusterMaxZoom != null) { + options = options.withClusterMaxZoom(Convert.toInt(clusterMaxZoom)); + } + + final Object clusterRadius = data.get("clusterRadius"); + if (clusterRadius != null) { + options = options.withClusterRadius(Convert.toInt(clusterRadius)); + } + + final Object lineMetrics = data.get("lineMetrics"); + if (lineMetrics != null) { + options = options.withLineMetrics(Convert.toBoolean(lineMetrics)); + } + + final Object maxZoom = data.get("maxZoom"); + if (maxZoom != null) { + options = options.withMaxZoom(Convert.toInt(maxZoom)); + } + + final Object minZoom = data.get("minZoom"); + if (minZoom != null) { + options = options.withMinZoom(Convert.toInt(minZoom)); + } + + final Object tolerance = data.get("tolerance"); + if (tolerance != null) { + options = options.withTolerance(Convert.toFloat(tolerance)); + } + return options; + } + + static GeoJsonSource buildGeojsonSource(String id, Map properties) { + final Object data = properties.get("data"); + final GeoJsonOptions options = buildGeojsonOptions(properties); + if (data != null) { + if (data instanceof String) { + try { + final URI uri = new URI(Convert.toString(data)); + return new GeoJsonSource(id, uri, options); + } catch (URISyntaxException e) { + } + } else { + Gson gson = new Gson(); + String geojson = gson.toJson(data); + final FeatureCollection featureCollection = FeatureCollection.fromJson(geojson); + return new GeoJsonSource(id, featureCollection, options); + } + } + return null; + } + + static ImageSource buildImageSource(String id, Map properties) { + final Object url = properties.get("url"); + List coordinates = Convert.toLatLngList(properties.get("coordinates"), true); + final LatLngQuad quad = + new LatLngQuad( + coordinates.get(0), coordinates.get(1), coordinates.get(2), coordinates.get(3)); + try { + final URI uri = new URI(Convert.toString(url)); + return new ImageSource(id, quad, uri); + } catch (URISyntaxException e) { + } + return null; + } + + static VectorSource buildVectorSource(String id, Map properties) { + final Object url = properties.get("url"); + if (url != null) { + final Uri uri = Uri.parse(Convert.toString(url)); + + if (uri != null) { + return new VectorSource(id, uri); + } + return null; + } + + final TileSet tileSet = buildTileset(properties); + return tileSet != null ? new VectorSource(id, tileSet) : null; + } + + static RasterSource buildRasterSource(String id, Map properties) { + final Object url = properties.get("url"); + if (url != null) { + try { + final URI uri = new URI(Convert.toString(url)); + return new RasterSource(id, uri); + } catch (URISyntaxException e) { + } + } + + final TileSet tileSet = buildTileset(properties); + return tileSet != null ? new RasterSource(id, tileSet) : null; + } + + static RasterDemSource buildRasterDemSource(String id, Map properties) { + final Object url = properties.get("url"); + if (url != null) { + try { + final URI uri = new URI(Convert.toString(url)); + return new RasterDemSource(id, uri); + } catch (URISyntaxException e) { + } + } + + final TileSet tileSet = buildTileset(properties); + return tileSet != null ? new RasterDemSource(id, tileSet) : null; + } + + static void addSource(String id, Map properties, Style style) { + final Object type = properties.get("type"); + Source source = null; + + if (type != null) { + switch (Convert.toString(type)) { + case "vector": + source = buildVectorSource(id, properties); + break; + case "raster": + source = buildRasterSource(id, properties); + break; + case "raster-dem": + source = buildRasterDemSource(id, properties); + break; + case "image": + source = buildImageSource(id, properties); + break; + case "geojson": + source = buildGeojsonSource(id, properties); + break; + default: + // unsupported source type + } + } + + if (source != null) { + style.addSource(source); + } + } +} diff --git a/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java deleted file mode 100644 index 93e2718af..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java +++ /dev/null @@ -1,168 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Symbol; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions; - -class SymbolBuilder implements SymbolOptionsSink { - private final SymbolOptions symbolOptions; - private static boolean customImage; - - SymbolBuilder() { - this.symbolOptions = new SymbolOptions(); - } - - public SymbolOptions getSymbolOptions(){ - return this.symbolOptions; - } - - @Override - public void setIconSize(float iconSize) { - symbolOptions.withIconSize(iconSize); - } - - @Override - public void setIconImage(String iconImage) { - symbolOptions.withIconImage(iconImage); - } - - @Override - public void setIconRotate(float iconRotate) { - symbolOptions.withIconRotate(iconRotate); - } - - @Override - public void setIconOffset(float[] iconOffset) { - symbolOptions.withIconOffset(new Float[] {iconOffset[0], iconOffset[1]}); - } - - @Override - public void setIconAnchor(String iconAnchor) { - symbolOptions.withIconAnchor(iconAnchor); - } - - @Override - public void setFontNames(String[] fontNames) { symbolOptions.withTextFont(fontNames); } - - @Override - public void setTextField(String textField) { - symbolOptions.withTextField(textField); - } - - @Override - public void setTextSize(float textSize) { - symbolOptions.withTextSize(textSize); - } - - @Override - public void setTextMaxWidth(float textMaxWidth) { - symbolOptions.withTextMaxWidth(textMaxWidth); - } - - @Override - public void setTextLetterSpacing(float textLetterSpacing) { - symbolOptions.withTextLetterSpacing(textLetterSpacing); - } - - @Override - public void setTextJustify(String textJustify) { - symbolOptions.withTextJustify(textJustify); - } - - @Override - public void setTextAnchor(String textAnchor) { - symbolOptions.withTextAnchor(textAnchor); - } - - @Override - public void setTextRotate(float textRotate) { - symbolOptions.withTextRotate(textRotate); - } - - @Override - public void setTextTransform(String textTransform) { - symbolOptions.withTextTransform(textTransform); - } - - @Override - public void setTextOffset(float[] textOffset) { - symbolOptions.withTextOffset(new Float[] {textOffset[0], textOffset[1]}); - } - - @Override - public void setIconOpacity(float iconOpacity) { - symbolOptions.withIconOpacity(iconOpacity); - } - - @Override - public void setIconColor(String iconColor) { - symbolOptions.withIconColor(iconColor); - } - - @Override - public void setIconHaloColor(String iconHaloColor) { - symbolOptions.withIconHaloColor(iconHaloColor); - } - - @Override - public void setIconHaloWidth(float iconHaloWidth) { - symbolOptions.withIconHaloWidth(iconHaloWidth); - } - - @Override - public void setIconHaloBlur(float iconHaloBlur) { - symbolOptions.withIconHaloBlur(iconHaloBlur); - } - - @Override - public void setTextOpacity(float textOpacity) { - symbolOptions.withTextOpacity(textOpacity); - } - - @Override - public void setTextColor(String textColor) { - symbolOptions.withTextColor(textColor); - } - - @Override - public void setTextHaloColor(String textHaloColor) { - symbolOptions.withTextHaloColor(textHaloColor); - } - - @Override - public void setTextHaloWidth(float textHaloWidth) { - symbolOptions.withTextHaloWidth(textHaloWidth); - } - - @Override - public void setTextHaloBlur(float textHaloBlur) { - symbolOptions.withTextHaloBlur(textHaloBlur); - } - - @Override - public void setGeometry(LatLng geometry) { - symbolOptions.withGeometry(Point.fromLngLat(geometry.getLongitude(), geometry.getLatitude())); - } - - @Override - public void setSymbolSortKey(float symbolSortKey) { - symbolOptions.withSymbolSortKey(symbolSortKey); - } - - @Override - public void setDraggable(boolean draggable) { - symbolOptions.withDraggable(draggable); - } - - public boolean getCustomImage() { - return this.customImage; - } -} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java deleted file mode 100644 index 348779527..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java +++ /dev/null @@ -1,191 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import android.graphics.Color; -import android.graphics.PointF; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.plugins.annotation.Symbol; -import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; - -/** - * Controller of a single Symbol on the map. - */ -class SymbolController implements SymbolOptionsSink { - private final Symbol symbol; - private final OnSymbolTappedListener onTappedListener; - private boolean consumeTapEvents; - - SymbolController(Symbol symbol, boolean consumeTapEvents, OnSymbolTappedListener onTappedListener) { - this.symbol = symbol; - this.consumeTapEvents = consumeTapEvents; - this.onTappedListener = onTappedListener; - } - - boolean onTap() { - if (onTappedListener != null) { - onTappedListener.onSymbolTapped(symbol); - } - return consumeTapEvents; - } - - public Symbol getSymbol(){ - return this.symbol; - } - - void remove(SymbolManager symbolManager) { - symbolManager.delete(symbol); - } - - @Override - public void setIconSize(float iconSize) { - symbol.setIconSize(iconSize); - } - - @Override - public void setIconImage(String iconImage) { - symbol.setIconImage(iconImage); - } - - @Override - public void setIconRotate(float iconRotate) { - symbol.setIconRotate(iconRotate); - } - - @Override - public void setIconOffset(float[] iconOffset) { - symbol.setIconOffset(new PointF(iconOffset[0], iconOffset[1])); - } - - @Override - public void setIconAnchor(String iconAnchor) { - symbol.setIconAnchor(iconAnchor); - } - - @Override - public void setFontNames(String[] fontNames) { symbol.setTextFont(fontNames); } - - @Override - public void setTextField(String textField) { - symbol.setTextField(textField); - } - - @Override - public void setTextSize(float textSize) { - symbol.setTextSize(textSize); - } - - @Override - public void setTextMaxWidth(float textMaxWidth) { - symbol.setTextMaxWidth(textMaxWidth); - } - - @Override - public void setTextLetterSpacing(float textLetterSpacing) { - symbol.setTextLetterSpacing(textLetterSpacing); - } - - @Override - public void setTextJustify(String textJustify) { - symbol.setTextJustify(textJustify); - } - - @Override - public void setTextAnchor(String textAnchor) { - symbol.setTextAnchor(textAnchor); - } - - @Override - public void setTextRotate(float textRotate) { - symbol.setTextRotate(textRotate); - } - - @Override - public void setTextTransform(String textTransform) { - symbol.setTextTransform(textTransform); - } - - @Override - public void setTextOffset(float[] textOffset) { - symbol.setTextOffset(new PointF(textOffset[0], textOffset[1])); - } - - @Override - public void setIconOpacity(float iconOpacity) { - symbol.setIconOpacity(iconOpacity); - } - - @Override - public void setIconColor(String iconColor) { - symbol.setIconColor(Color.parseColor(iconColor)); - } - - @Override - public void setIconHaloColor(String iconHaloColor) { - symbol.setIconHaloColor(Color.parseColor(iconHaloColor)); - } - - @Override - public void setIconHaloWidth(float iconHaloWidth) { - symbol.setIconHaloWidth(iconHaloWidth); - } - - @Override - public void setIconHaloBlur(float iconHaloBlur) { - symbol.setIconHaloBlur(iconHaloBlur); - } - - @Override - public void setTextOpacity(float textOpacity) { - symbol.setTextOpacity(textOpacity); - } - - @Override - public void setTextColor(String textColor) { - symbol.setTextColor(Color.parseColor(textColor)); - } - - @Override - public void setTextHaloColor(String textHaloColor) { - symbol.setTextHaloColor(Color.parseColor(textHaloColor)); - } - - @Override - public void setTextHaloWidth(float textHaloWidth) { - symbol.setTextHaloWidth(textHaloWidth); - } - - @Override - public void setTextHaloBlur(float textHaloBlur) { - symbol.setTextHaloBlur(textHaloBlur); - } - - @Override - public void setSymbolSortKey(float symbolSortKey) { - symbol.setSymbolSortKey(symbolSortKey); - } - - @Override - public void setGeometry(LatLng geometry) { - symbol.setGeometry(Point.fromLngLat(geometry.getLongitude(), geometry.getLatitude())); - } - - public LatLng getGeometry() { - Point point = symbol.getGeometry(); - return new LatLng(point.latitude(), point.longitude()); - } - - @Override - public void setDraggable(boolean draggable) { - symbol.setDraggable(draggable); - } - - public void update(SymbolManager symbolManager) { - symbolManager.update(symbol); - } -} diff --git a/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java deleted file mode 100644 index ee52e8672..000000000 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java +++ /dev/null @@ -1,72 +0,0 @@ -// This file is generated. - -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.mapbox.mapboxgl; - -import com.mapbox.mapboxsdk.geometry.LatLng; - -/** - * Receiver of Symbol configuration options. - */ -interface SymbolOptionsSink { - - void setIconSize(float iconSize); - - void setIconImage(String iconImage); - - void setIconRotate(float iconRotate); - - void setIconOffset(float[] iconOffset); - - void setIconAnchor(String iconAnchor); - - void setFontNames(String[] fontNames); - - void setTextField(String textField); - - void setTextSize(float textSize); - - void setTextMaxWidth(float textMaxWidth); - - void setTextLetterSpacing(float textLetterSpacing); - - void setTextJustify(String textJustify); - - void setTextAnchor(String textAnchor); - - void setTextRotate(float textRotate); - - void setTextTransform(String textTransform); - - void setTextOffset(float[] textOffset); - - void setIconOpacity(float iconOpacity); - - void setIconColor(String iconColor); - - void setIconHaloColor(String iconHaloColor); - - void setIconHaloWidth(float iconHaloWidth); - - void setIconHaloBlur(float iconHaloBlur); - - void setTextOpacity(float textOpacity); - - void setTextColor(String textColor); - - void setTextHaloColor(String textHaloColor); - - void setTextHaloWidth(float textHaloWidth); - - void setTextHaloBlur(float textHaloBlur); - - void setGeometry(LatLng geometry); - - void setSymbolSortKey(float symbolSortKey); - - void setDraggable(boolean draggable); - -} diff --git a/doc/RUNNING_EXAMPLE_CODE.md b/doc/RUNNING_EXAMPLE_CODE.md new file mode 100644 index 000000000..8f4852541 --- /dev/null +++ b/doc/RUNNING_EXAMPLE_CODE.md @@ -0,0 +1,58 @@ +# Running the example code + +- You'll find the example code in the `example` folder of this repository + +## Secret Mapbox access token + +A secret access token with the `Downloads: Read` scope is required for the underlying Mapbox SDKs to be downloaded. +Information on setting it up is available in the Mapbox documentation: +[Android](https://docs.mapbox.com/android/maps/guides/install/), +[iOS](https://docs.mapbox.com/ios/maps/guides/install/). + +If the properly configured token is not present, +the build process fails with one the following errors *(for Android/iOS respectively)*: + +``` +* What went wrong: +A problem occurred evaluating project ':mapbox_gl'. +> SDK Registry token is null. See README.md for more information. +``` + +``` +[!] Error installing Mapbox-iOS-SDK +curl: (22) The requested URL returned error: 401 Unauthorized +``` + +## iOS +- Clone this repo +- Open `example/ios/Runner.xcworkspace` in XCode +- On the left, select Runner and change the Bundle identifier on the `general` tab to something of your own. + +![XCode bundle identier](img/xcode-bundle-identifier.png) + +- On the `signing & capabilities` tab change the team to your own. +- Close Xcode +- run `flutter build ios --dart-define ACCESS_TOKEN=sk.xxxxxxxxxxxxxxxxxxxxx` to make sure the project builds from the command line. The `ACCESS_TOKEN` is the Mapbox token you've obtained in the `Secret Mapbox access token` step described above. +- The build command output should be something like: + +```terminal +Warning: You are using these overridden dependencies: +! mapbox_gl_platform_interface 0.15.0 from path ../mapbox_gl_platform_interface +! mapbox_gl_web 0.15.0 from path ../mapbox_gl_web +Running "flutter pub get" in example... 1,918ms +Building com.xxx.mapboxtest for device (ios-release)... +Updating project for Xcode compatibility. +Upgrading project.pbxproj +Upgrading Runner.xcscheme +Signing iOS app for device deployment using developer identity: "Apple Development: xxx@xxxxx.xx (XXXXXXX)" +Running pod install... 2,950ms +Running Xcode build... + └─Compiling, linking and signing... 15.1s +Xcode build done. 98.2s +Built /Development/flutter/maps/example/build/ios/iphoneos/Runner.app. +``` + +- Open `example/ios/Runner.xcworkspace` again in XCode +- Run the project connected iPhone or iOS Emulator + +![XCode bundle identief](img/example-iphone.jpeg) \ No newline at end of file diff --git a/doc/img/example-iphone.jpeg b/doc/img/example-iphone.jpeg new file mode 100644 index 000000000..8adf22267 Binary files /dev/null and b/doc/img/example-iphone.jpeg differ diff --git a/doc/img/xcode-bundle-identifier.png b/doc/img/xcode-bundle-identifier.png new file mode 100644 index 000000000..e008995f6 Binary files /dev/null and b/doc/img/xcode-bundle-identifier.png differ 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 6d6819d3d..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,11 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 29 - + namespace "com.mapbox.mapboxglexample" + compileSdkVersion 35 + ndkVersion "25.1.8937393" + lintOptions { disable 'InvalidPackage' } @@ -34,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 28 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -68,4 +67,12 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.0-alpha4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4' + constraints { + implementation('com.google.android.gms:play-services-location') { + version { + strictly "16.0.0" + } + because 'location: 4.2.3 does not specify version play-services-location' + } + } } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 32dd13477..519763b2e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,14 +3,16 @@ + @drawable/launch_background + diff --git a/example/android/build.gradle b/example/android/build.gradle index e0d7ae2c1..ad89b89e3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,18 +1,7 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -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 1b855228d..15de90249 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Jun 03 09:18:08 CEST 2020 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-5.6.4-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5a2f14fb1..4f97b7cc6 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,15 +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 flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +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/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 8b03e2375..1345a4034 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -f3c426b614f84ca0c48298bfc9934d56 \ No newline at end of file +03fcf93a7807ed5ca7e1593e013555e0 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e..8d4492f97 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 4e9b84d6a..d523a681f 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -29,10 +29,16 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do + use_frameworks! + use_modular_headers! + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| + installer.pods_project.build_configurations.each do |config| + config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" + end installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4de76e90b..846740d33 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -166,7 +166,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -250,18 +250,20 @@ "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/Mapbox.framework", "${BUILT_PRODUCTS_DIR}/MapboxAnnotationExtension/MapboxAnnotationExtension.framework", "${BUILT_PRODUCTS_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework", + "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/location/location.framework", "${BUILT_PRODUCTS_DIR}/mapbox_gl/mapbox_gl.framework", - "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mapbox.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxAnnotationExtension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/location.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mapbox_gl.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -358,7 +360,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -437,7 +439,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -486,7 +488,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 25ea137df..3db53b6e1 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ 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/annotation_order_maps.dart b/example/lib/annotation_order_maps.dart index d0f66e4ab..6a08f2fe7 100644 --- a/example/lib/annotation_order_maps.dart +++ b/example/lib/annotation_order_maps.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter/rendering.dart'; // ignore: unnecessary_import import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; diff --git a/example/lib/click_annotations.dart b/example/lib/click_annotations.dart new file mode 100644 index 000000000..77a1876ac --- /dev/null +++ b/example/lib/click_annotations.dart @@ -0,0 +1,168 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:mapbox_gl_example/main.dart'; + +import 'page.dart'; + +class ClickAnnotationPage extends ExamplePage { + ClickAnnotationPage() + : super(const Icon(Icons.check_circle), 'Annotation tap'); + + @override + Widget build(BuildContext context) { + return const ClickAnnotationBody(); + } +} + +class ClickAnnotationBody extends StatefulWidget { + const ClickAnnotationBody(); + + @override + State createState() => ClickAnnotationBodyState(); +} + +class ClickAnnotationBodyState extends State { + ClickAnnotationBodyState(); + static const LatLng center = const LatLng(-33.88, 151.16); + bool overlapping = false; + + MapboxMapController? controller; + + void _onMapCreated(MapboxMapController controller) { + this.controller = controller; + controller.onFillTapped.add(_onFillTapped); + controller.onCircleTapped.add(_onCircleTapped); + controller.onLineTapped.add(_onLineTapped); + controller.onSymbolTapped.add(_onSymbolTapped); + } + + @override + void dispose() { + controller?.onFillTapped.remove(_onFillTapped); + controller?.onCircleTapped.remove(_onCircleTapped); + controller?.onLineTapped.remove(_onLineTapped); + controller?.onSymbolTapped.remove(_onSymbolTapped); + super.dispose(); + } + + _showSnackBar(String type, String id) { + final snackBar = SnackBar( + content: Text('Tapped $type $id', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + backgroundColor: Theme.of(context).primaryColor); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + void _onFillTapped(Fill fill) { + _showSnackBar('fill', fill.id); + } + + void _onCircleTapped(Circle circle) { + _showSnackBar('circle', circle.id); + } + + void _onLineTapped(Line line) { + _showSnackBar('line', line.id); + } + + void _onSymbolTapped(Symbol symbol) { + _showSnackBar('symbol', symbol.id); + } + + void _onStyleLoaded() { + controller!.addCircle( + CircleOptions( + geometry: LatLng(-33.881979408447314, 151.171361438502117), + circleStrokeColor: "#00FF00", + circleStrokeWidth: 2, + circleRadius: 16, + ), + ); + controller!.addCircle( + CircleOptions( + geometry: LatLng(-33.894372606072309, 151.17576679759523), + circleStrokeColor: "#00FF00", + circleStrokeWidth: 2, + circleRadius: 30, + ), + ); + controller!.addSymbol( + SymbolOptions( + geometry: LatLng(-33.894372606072309, 151.17576679759523), + iconImage: "fast-food-15", + iconSize: 2), + ); + controller!.addLine( + LineOptions( + geometry: [ + LatLng(-33.874867744475786, 151.170627211986584), + LatLng(-33.881979408447314, 151.171361438502117), + LatLng(-33.887058805548882, 151.175032571079726), + LatLng(-33.894372606072309, 151.17576679759523), + LatLng(-33.900060683994681, 151.15765587687909), + ], + lineColor: "#0000FF", + lineWidth: 20, + ), + ); + + controller!.addFill( + FillOptions( + geometry: [ + [ + LatLng(-33.901517742631846, 151.178099204457737), + LatLng(-33.872845324482071, 151.179025547977773), + LatLng(-33.868230472039514, 151.147000529140399), + LatLng(-33.883172899638311, 151.150838238009328), + LatLng(-33.894158309528244, 151.14223647675135), + LatLng(-33.904812805307806, 151.155999294764086), + LatLng(-33.901517742631846, 151.178099204457737), + ], + ], + fillColor: "#FF0000", + fillOutlineColor: "#000000", + ), + ); + } + + @override + Widget build(BuildContext context) { + 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/custom_marker.dart b/example/lib/custom_marker.dart index f2fb48e95..c81937141 100644 --- a/example/lib/custom_marker.dart +++ b/example/lib/custom_marker.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart'; // ignore: unnecessary_import import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; @@ -66,8 +66,8 @@ class CustomMarkerState extends State { coordinates.add(markerState.getCoordinate()); } - _mapController.toScreenLocationBatch(coordinates).then((points){ - _markerStates.asMap().forEach((i, value){ + _mapController.toScreenLocationBatch(coordinates).then((points) { + _markerStates.asMap().forEach((i, value) { _markerStates[i].updatePosition(points[i]); }); }); @@ -75,35 +75,33 @@ class CustomMarkerState extends State { void _addMarker(Point point, LatLng coordinates) { setState(() { - _markers.add(Marker(_rnd.nextInt(100000).toString(), coordinates, point, _addMarkerStates)); + _markers.add(Marker(_rnd.nextInt(100000).toString(), coordinates, point, + _addMarkerStates)); }); } @override Widget build(BuildContext context) { return new Scaffold( - body: Stack( - children: [ - MapboxMap( - accessToken: MapsDemo.ACCESS_TOKEN, - trackCameraPosition: true, - onMapCreated: _onMapCreated, - onMapLongClick: _onMapLongClickCallback, - onCameraIdle: _onCameraIdleCallback, - onStyleLoadedCallback: _onStyleLoadedCallback, - initialCameraPosition: const CameraPosition(target: LatLng(35.0, 135.0), zoom: 5), - ), - IgnorePointer( - ignoring: true, - child: - Stack( - children: _markers, - ) - ) - ] - ), + body: Stack(children: [ + MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + trackCameraPosition: true, + onMapCreated: _onMapCreated, + onMapLongClick: _onMapLongClickCallback, + onCameraIdle: _onCameraIdleCallback, + onStyleLoadedCallback: _onStyleLoadedCallback, + initialCameraPosition: + const CameraPosition(target: LatLng(35.0, 135.0), zoom: 5), + ), + IgnorePointer( + ignoring: true, + child: Stack( + children: _markers, + )) + ]), floatingActionButton: FloatingActionButton( - onPressed: (){ + onPressed: () { //_measurePerformance(); // Generate random markers @@ -116,7 +114,8 @@ class CustomMarkerState extends State { _mapController.toScreenLocationBatch(param).then((value) { for (var i = 0; i < randomMarkerNum; i++) { - var point = Point(value[i].x as double, value[i].y as double); + var point = + Point(value[i].x as double, value[i].y as double); _addMarker(point, param[i]); } }); @@ -146,7 +145,8 @@ class CustomMarkerState extends State { sw.start(); var list = >>[]; for (var j = 0; j < batch; j++) { - var p = _mapController.toScreenLocation(LatLng(j.toDouble() % 80, j.toDouble() % 300)); + var p = _mapController + .toScreenLocation(LatLng(j.toDouble() % 80, j.toDouble() % 300)); list.add(p); } Future.wait(list); @@ -170,9 +170,9 @@ class CustomMarkerState extends State { sw.reset(); } - print('batch=$batch,primitive=${results[batch]![0] / trial}ms, batch=${results[batch]![1] / trial}ms'); + print( + 'batch=$batch,primitive=${results[batch]![0] / trial}ms, batch=${results[batch]![1] / trial}ms'); } - } } @@ -181,7 +181,9 @@ class Marker extends StatefulWidget { final LatLng _coordinate; final void Function(_MarkerState) _addMarkerState; - Marker(String key, this._coordinate, this._initialPosition, this._addMarkerState) : super(key: Key(key)); + Marker( + String key, this._coordinate, this._initialPosition, this._addMarkerState) + : super(key: Key(key)); @override State createState() { @@ -230,16 +232,13 @@ class _MarkerState extends State with TickerProviderStateMixin { ratio = Platform.isIOS ? 1.0 : MediaQuery.of(context).devicePixelRatio; } - return - Positioned( - left: _position.x / ratio - _iconSize / 2, - top: _position.y / ratio - _iconSize / 2, - child: - RotationTransition( - turns: _animation, - child: - Image.asset('assets/symbols/2.0x/custom-icon.png', height: _iconSize)) - ); + return Positioned( + left: _position.x / ratio - _iconSize / 2, + top: _position.y / ratio - _iconSize / 2, + child: RotationTransition( + turns: _animation, + child: Image.asset('assets/symbols/2.0x/custom-icon.png', + height: _iconSize))); } void updatePosition(Point point) { @@ -252,4 +251,3 @@ class _MarkerState extends State with TickerProviderStateMixin { return (widget as Marker)._coordinate; } } - diff --git a/example/lib/full_map.dart b/example/lib/full_map.dart index 5b6bdcdb7..1e0944437 100644 --- a/example/lib/full_map.dart +++ b/example/lib/full_map.dart @@ -22,21 +22,38 @@ class FullMap extends StatefulWidget { class FullMapState extends State { MapboxMapController? mapController; + var isLight = true; - void _onMapCreated(MapboxMapController controller) { + _onMapCreated(MapboxMapController controller) { mapController = controller; } + _onStyleLoadedCallback() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Style loaded :)"), + backgroundColor: Theme.of(context).primaryColor, + duration: Duration(seconds: 1), + )); + } + @override Widget build(BuildContext context) { return new Scaffold( + floatingActionButton: Padding( + padding: const EdgeInsets.all(32.0), + child: FloatingActionButton( + child: Icon(Icons.swap_horiz), + onPressed: () => setState( + () => isLight = !isLight, + ), + ), + ), body: MapboxMap( - accessToken: MapsDemo.ACCESS_TOKEN, - onMapCreated: _onMapCreated, - initialCameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), - onStyleLoadedCallback: onStyleLoadedCallback, - )); + styleString: isLight ? MapboxStyles.LIGHT : MapboxStyles.DARK, + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), + onStyleLoadedCallback: _onStyleLoadedCallback, + )); } - - void onStyleLoadedCallback() {} } diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart index 9338cdbe8..48574c614 100644 --- a/example/lib/generated_plugin_registrant.dart +++ b/example/lib/generated_plugin_registrant.dart @@ -2,7 +2,9 @@ // Generated file. Do not edit. // +// ignore_for_file: directives_ordering // ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages import 'package:location_web/location_web.dart'; import 'package:mapbox_gl_web/mapbox_gl_web.dart'; diff --git a/example/lib/layer.dart b/example/lib/layer.dart new file mode 100644 index 000000000..bdcd8828e --- /dev/null +++ b/example/lib/layer.dart @@ -0,0 +1,296 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:mapbox_gl_example/main.dart'; +import 'package:mapbox_gl_example/page.dart'; + +class LayerPage extends ExamplePage { + LayerPage() : super(const Icon(Icons.share), 'Layer'); + + @override + Widget build(BuildContext context) => LayerBody(); +} + +class LayerBody extends StatefulWidget { + @override + State createState() => LayerState(); +} + +class LayerState extends State { + static final LatLng center = const LatLng(-33.86711, 151.1947171); + + late MapboxMapController controller; + Timer? bikeTimer; + Timer? filterTimer; + Timer? visibilityTimer; + int filteredId = 0; + bool isVisible = true; + + @override + Widget build(BuildContext context) { + return MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + dragEnabled: false, + myLocationEnabled: true, + onMapCreated: _onMapCreated, + onMapClick: (point, latLong) => + print(point.toString() + latLong.toString()), + onStyleLoadedCallback: _onStyleLoadedCallback, + initialCameraPosition: CameraPosition( + target: center, + zoom: 11.0, + ), + annotationOrder: const [], + ); + } + + void _onMapCreated(MapboxMapController controller) { + this.controller = controller; + + controller.onFeatureTapped.add(onFeatureTap); + } + + void onFeatureTap(dynamic featureId, Point point, LatLng latLng) { + final snackBar = SnackBar( + content: Text( + 'Tapped feature with id $featureId', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + backgroundColor: Theme.of(context).primaryColor, + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + void _onStyleLoadedCallback() async { + await controller.addGeoJsonSource("points", _points); + await controller.addGeoJsonSource("moving", _movingFeature(0)); + + //new style of adding sources + await controller.addSource("fills", GeojsonSourceProperties(data: _fills)); + + await controller.addFillLayer( + "fills", + "fills", + FillLayerProperties(fillColor: [ + Expressions.interpolate, + ['exponential', 0.5], + [Expressions.zoom], + 11, + 'red', + 18, + 'green' + ], fillOpacity: 0.4), + belowLayerId: "water", + filter: ['==', 'id', filteredId], + ); + + await controller.addLineLayer( + "fills", + "lines", + LineLayerProperties( + lineColor: Colors.lightBlue.toHexStringRGB(), + lineWidth: [ + Expressions.interpolate, + ["linear"], + [Expressions.zoom], + 11.0, + 2.0, + 20.0, + 10.0 + ]), + ); + + await controller.addCircleLayer( + "fills", + "circles", + CircleLayerProperties( + circleRadius: 4, + circleColor: Colors.blue.toHexStringRGB(), + ), + ); + + await controller.addSymbolLayer( + "points", + "symbols", + SymbolLayerProperties( + iconImage: "{type}-15", + iconSize: 2, + iconAllowOverlap: true, + ), + ); + + await controller.addSymbolLayer( + "moving", + "moving", + SymbolLayerProperties( + textField: [Expressions.get, "name"], + textHaloWidth: 1, + textSize: 10, + textHaloColor: Colors.white.toHexStringRGB(), + textOffset: [ + Expressions.literal, + [0, 2] + ], + iconImage: "bicycle-15", + iconSize: 2, + iconAllowOverlap: true, + textAllowOverlap: true, + ), + minzoom: 11, + ); + + bikeTimer = Timer.periodic(Duration(milliseconds: 10), (t) { + controller.setGeoJsonSource("moving", _movingFeature(t.tick / 2000)); + }); + + filterTimer = Timer.periodic(Duration(seconds: 3), (t) { + 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(); + } +} + +Map _movingFeature(double t) { + List makeLatLong(double t) { + final angle = t * 2 * pi; + const r = 0.025; + const center_x = 151.1849; + const center_y = -33.8748; + return [ + center_x + r * sin(angle), + center_y + r * cos(angle), + ]; + } + + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"name": "POGAČAR Tadej"}, + "id": 10, + "geometry": {"type": "Point", "coordinates": makeLatLong(t)} + }, + { + "type": "Feature", + "properties": {"name": "VAN AERT Wout"}, + "id": 11, + "geometry": {"type": "Point", "coordinates": makeLatLong(t + 0.15)} + }, + ] + }; +} + +final _fills = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 0, // web currently only supports number ids + "properties": {'id': 0}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [151.178099204457737, -33.901517742631846], + [151.179025547977773, -33.872845324482071], + [151.147000529140399, -33.868230472039514], + [151.150838238009328, -33.883172899638311], + [151.14223647675135, -33.894158309528244], + [151.155999294764086, -33.904812805307806], + [151.178099204457737, -33.901517742631846] + ], + [ + [151.162657925954278, -33.879168932438581], + [151.155323416087612, -33.890737666431583], + [151.173659690754278, -33.897637567778119], + [151.162657925954278, -33.879168932438581] + ] + ] + } + }, + { + "type": "Feature", + "id": 1, + "properties": {'id': 1}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [151.18735077583878, -33.891143558434102], + [151.197374605989864, -33.878357032551868], + [151.213021560372084, -33.886475683791488], + [151.204953599518745, -33.899463918807818], + [151.18735077583878, -33.891143558434102] + ] + ] + } + } + ] +}; + +const _points = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 2, + "properties": { + "type": "restaurant", + }, + "geometry": { + "type": "Point", + "coordinates": [151.184913929732943, -33.874874486427181] + } + }, + { + "type": "Feature", + "id": 3, + "properties": { + "type": "airport", + }, + "geometry": { + "type": "Point", + "coordinates": [151.215730044667879, -33.874616048776858] + } + }, + { + "type": "Feature", + "id": 4, + "properties": { + "type": "bakery", + }, + "geometry": { + "type": "Point", + "coordinates": [151.228803547973598, -33.892188026142584] + } + }, + { + "type": "Feature", + "id": 5, + "properties": { + "type": "college", + }, + "geometry": { + "type": "Point", + "coordinates": [151.186470299174118, -33.902781145804774] + } + } + ] +}; diff --git a/example/lib/line.dart b/example/lib/line.dart index 7ea958734..cc8e541df 100644 --- a/example/lib/line.dart +++ b/example/lib/line.dart @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; @@ -34,6 +36,7 @@ class LineBodyState extends State { MapboxMapController? controller; int _lineCount = 0; Line? _selectedLine; + final String _linePatternImage = "assets/fill/cat_silhouette_pattern.png"; void _onMapCreated(MapboxMapController controller) { this.controller = controller; @@ -46,26 +49,27 @@ class LineBodyState extends State { super.dispose(); } - void _onLineTapped(Line line) { - if (_selectedLine != null) { - _updateSelectedLine( - const LineOptions( - lineWidth: 28.0, - ), - ); - } + /// Adds an asset image to the currently displayed style + Future addImageFromAsset(String name, String assetName) async { + final ByteData bytes = await rootBundle.load(assetName); + final Uint8List list = bytes.buffer.asUint8List(); + return controller!.addImage(name, list); + } + + _onLineTapped(Line line) async { + await _updateSelectedLine( + LineOptions(lineColor: "#ff0000"), + ); setState(() { _selectedLine = line; }); - _updateSelectedLine( - LineOptions( - // linecolor: , - ), + await _updateSelectedLine( + LineOptions(lineColor: "#ffe100"), ); } - void _updateSelectedLine(LineOptions changes) { - controller!.updateLine(_selectedLine!, changes); + _updateSelectedLine(LineOptions changes) async { + if (_selectedLine != null) controller!.updateLine(_selectedLine!, changes); } void _add() { @@ -87,6 +91,17 @@ class LineBodyState extends State { }); } + _move() async { + final currentStart = _selectedLine!.options.geometry![0]; + final currentEnd = _selectedLine!.options.geometry![1]; + final end = + LatLng(currentEnd.latitude + 0.001, currentEnd.longitude + 0.001); + final start = + LatLng(currentStart.latitude - 0.001, currentStart.longitude - 0.001); + await controller! + .updateLine(_selectedLine!, LineOptions(geometry: [start, end])); + } + void _remove() { controller!.removeLine(_selectedLine!); setState(() { @@ -95,6 +110,14 @@ class LineBodyState extends State { }); } + Future _changeLinePattern() async { + String? current = + _selectedLine!.options.linePattern == null ? "assetImage" : null; + await _updateSelectedLine( + LineOptions(linePattern: current), + ); + } + Future _changeAlpha() async { double? current = _selectedLine!.options.lineOpacity; if (current == null) { @@ -102,7 +125,7 @@ class LineBodyState extends State { current = 1.0; } - _updateSelectedLine( + await _updateSelectedLine( LineOptions(lineOpacity: current < 0.1 ? 1.0 : current * 0.75), ); } @@ -113,13 +136,14 @@ class LineBodyState extends State { // default value current = 1.0; } - _updateSelectedLine( + await _updateSelectedLine( LineOptions(lineOpacity: current == 0.0 ? 1.0 : 0.0), ); } - void onStyleLoadedCallback() { - controller!.addLine( + _onStyleLoadedCallback() async { + addImageFromAsset("assetImage", _linePatternImage); + await controller!.addLine( LineOptions( geometry: [LatLng(37.4220, -122.0841), LatLng(37.4240, -122.0941)], lineColor: "#ff0000", @@ -137,12 +161,11 @@ class LineBodyState extends State { children: [ Center( child: SizedBox( - width: 300.0, - height: 200.0, + height: 400.0, child: MapboxMap( accessToken: MapsDemo.ACCESS_TOKEN, onMapCreated: _onMapCreated, - onStyleLoadedCallback: onStyleLoadedCallback, + onStyleLoadedCallback: _onStyleLoadedCallback, initialCameraPosition: const CameraPosition( target: LatLng(-33.852, 151.211), zoom: 11.0, @@ -155,9 +178,9 @@ class LineBodyState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Row( + Column( children: [ - Column( + Row( children: [ TextButton( child: const Text('add'), @@ -167,9 +190,23 @@ class LineBodyState extends State { child: const Text('remove'), onPressed: (_selectedLine == null) ? null : _remove, ), + TextButton( + child: const Text('move'), + onPressed: (_selectedLine == null) + ? null + : () async { + await _move(); + }, + ), + TextButton( + child: const Text('change line-pattern'), + onPressed: (_selectedLine == null) + ? null + : _changeLinePattern, + ), ], ), - Column( + Row( children: [ TextButton( child: const Text('change alpha'), @@ -196,7 +233,7 @@ class LineBodyState extends State { ], ), ], - ) + ), ], ), ), diff --git a/example/lib/local_style.dart b/example/lib/local_style.dart index c851b94a7..d55f907c3 100644 --- a/example/lib/local_style.dart +++ b/example/lib/local_style.dart @@ -8,8 +8,7 @@ import 'main.dart'; import 'page.dart'; class LocalStylePage extends ExamplePage { - LocalStylePage() - : super(const Icon(Icons.map), 'Local style'); + LocalStylePage() : super(const Icon(Icons.map), 'Local style'); @override Widget build(BuildContext context) { @@ -35,7 +34,8 @@ class LocalStyleState extends State { getApplicationDocumentsDirectory().then((dir) async { String documentDir = dir.path; String stylesDir = '$documentDir/styles'; - String styleJSON = '{"version":8,"name":"Basic","constants":{},"sources":{"mapillary":{"type":"vector","tiles":["https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt"],"attribution":"© Mapillary, CC BY","maxzoom":14}},"sprite":"","glyphs":"","layers":[{"id":"background","type":"background","paint":{"background-color":"rgba(135, 149, 154, 1)"}},{"id":"water","type":"fill","source":"mapbox","source-layer":"water","paint":{"fill-color":"rgba(108, 148, 120, 1)"}}]}'; + String styleJSON = + '{"version":8,"name":"Basic","constants":{},"sources":{"mapillary":{"type":"vector","tiles":["https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt"],"attribution":"© Mapillary, CC BY","maxzoom":14}},"sprite":"","glyphs":"","layers":[{"id":"background","type":"background","paint":{"background-color":"rgba(135, 149, 154, 1)"}},{"id":"water","type":"fill","source":"mapbox","source-layer":"water","paint":{"fill-color":"rgba(108, 148, 120, 1)"}}]}'; await new Directory(stylesDir).create(recursive: true); @@ -49,7 +49,6 @@ class LocalStyleState extends State { }); } - void _onMapCreated(MapboxMapController controller) { mapController = controller; } @@ -63,14 +62,13 @@ class LocalStyleState extends State { } return new Scaffold( - body: MapboxMap( - accessToken: MapsDemo.ACCESS_TOKEN, - styleString: styleAbsoluteFilePath, - onMapCreated: _onMapCreated, - initialCameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), - onStyleLoadedCallback: onStyleLoadedCallback, - ) - ); + body: MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + styleString: styleAbsoluteFilePath, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), + onStyleLoadedCallback: onStyleLoadedCallback, + )); } void onStyleLoadedCallback() {} diff --git a/example/lib/main.dart b/example/lib/main.dart index de06ee3ee..e28938b2c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,27 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +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/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 '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 'sources.dart'; +import 'take_snapshot.dart'; final List _allPages = [ MapUiPage(), @@ -33,6 +40,7 @@ final List _allPages = [ PlaceSourcePage(), LinePage(), LocalStylePage(), + LayerPage(), PlaceCirclePage(), PlaceFillPage(), ScrollingMapPage(), @@ -40,11 +48,46 @@ final List _allPages = [ AnnotationOrderPage(), CustomMarkerPage(), BatchAddPage(), + TakeSnapPage(), + ClickAnnotationPage(), + Sources() ]; -class MapsDemo extends StatelessWidget { - //FIXME: Add your Mapbox access token here - static const String ACCESS_TOKEN = "YOUR_TOKEN_HERE"; +class MapsDemo extends StatefulWidget { + // FIXME: You need to pass in your access token via the command line argument + // --dart-define=ACCESS_TOKEN=ADD_YOUR_TOKEN_HERE + // It is also possible to pass it in while running the app via an IDE by + // passing the same args there. + // + // Alternatively you can replace `String.fromEnvironment("ACCESS_TOKEN")` + // in the following line with your access token directly. + static const String ACCESS_TOKEN = String.fromEnvironment("ACCESS_TOKEN"); + + @override + State createState() => _MapsDemoState(); +} + +class _MapsDemoState extends State { + @override + void initState() { + super.initState(); + } + + /// Determine the android version of the phone and turn off HybridComposition + /// on older sdk versions to improve performance for these + /// + /// !!! Hybrid composition is currently broken do no use !!! + Future initHybridComposition() async { + if (!kIsWeb && Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + final sdkVersion = androidInfo.version.sdkInt; + if (sdkVersion != null && sdkVersion >= 29) { + MapboxMap.useHybridComposition = true; + } else { + MapboxMap.useHybridComposition = false; + } + } + } void _pushPage(BuildContext context, ExamplePage page) async { if (!kIsWeb) { @@ -65,12 +108,43 @@ class MapsDemo extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('MapboxMaps examples')), - body: ListView.builder( - itemCount: _allPages.length, - itemBuilder: (_, int index) => ListTile( - leading: _allPages[index].leading, - title: Text(_allPages[index].title), - onTap: () => _pushPage(context, _allPages[index]), + body: MapsDemo.ACCESS_TOKEN.isEmpty || + MapsDemo.ACCESS_TOKEN.contains("YOUR_TOKEN") + ? buildAccessTokenWarning() + : ListView.separated( + itemCount: _allPages.length, + separatorBuilder: (BuildContext context, int index) => + const Divider(height: 1), + itemBuilder: (_, int index) => ListTile( + leading: _allPages[index].leading, + title: Text(_allPages[index].title), + onTap: () => _pushPage(context, _allPages[index]), + ), + ), + ); + } + + Widget buildAccessTokenWarning() { + return Container( + color: Colors.red[900], + child: SizedBox.expand( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Please pass in your access token with", + "--dart-define=ACCESS_TOKEN=ADD_YOUR_TOKEN_HERE", + "passed into flutter run or add it to args in vscode's launch.json", + ] + .map((text) => Padding( + padding: EdgeInsets.all(8), + child: Text(text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white)), + )) + .toList(), ), ), ); diff --git a/example/lib/map_ui.dart b/example/lib/map_ui.dart index 9db427f34..5be6b2bd8 100644 --- a/example/lib/map_ui.dart +++ b/example/lib/map_ui.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; @@ -40,9 +41,10 @@ class MapUiBodyState extends State { ); MapboxMapController? mapController; - CameraPosition? _position = _kInitialPosition; + CameraPosition _position = _kInitialPosition; bool _isMoving = false; bool _compassEnabled = true; + bool _mapExpanded = true; CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; int _styleStringIndex = 0; @@ -60,6 +62,7 @@ class MapUiBodyState extends State { ]; bool _rotateGesturesEnabled = true; bool _scrollGesturesEnabled = true; + bool? _doubleClickToZoomEnabled; bool _tiltGesturesEnabled = true; bool _zoomGesturesEnabled = true; bool _myLocationEnabled = true; @@ -80,7 +83,8 @@ class MapUiBodyState extends State { } void _extractMapInfo() { - _position = mapController!.cameraPosition; + final position = mapController!.cameraPosition; + if (position != null) _position = position; _isMoving = mapController!.isCameraMoving; } @@ -124,6 +128,17 @@ class MapUiBodyState extends State { ); } + Widget _mapSizeToggler() { + return TextButton( + child: Text('${_mapExpanded ? 'shrink' : 'expand'} map'), + onPressed: () { + setState(() { + _mapExpanded = !_mapExpanded; + }); + }, + ); + } + Widget _compassToggler() { return TextButton( child: Text('${_compassEnabled ? 'disable' : 'enable'} compasss'), @@ -201,6 +216,28 @@ class MapUiBodyState extends State { ); } + Widget _doubleClickToZoomToggler() { + final stateInfo = _doubleClickToZoomEnabled == null + ? "disable" + : _doubleClickToZoomEnabled! + ? 'unset' + : 'enable'; + return TextButton( + child: Text('$stateInfo double click to zoom'), + onPressed: () { + setState(() { + if (_doubleClickToZoomEnabled == null) { + _doubleClickToZoomEnabled = false; + } else if (!_doubleClickToZoomEnabled!) { + _doubleClickToZoomEnabled = true; + } else { + _doubleClickToZoomEnabled = null; + } + }); + }, + ); + } + Widget _tiltToggler() { return TextButton( child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), @@ -269,10 +306,11 @@ class MapUiBodyState extends State { } _drawFill(List features) async { - Map feature = features[0]; - if (feature['geometry']['type'] == 'Polygon') { - var coordinates = feature['geometry']['coordinates']; - List> geometry = coordinates + Map? feature = + features.firstWhereOrNull((f) => f['geometry']['type'] == 'Polygon'); + + if (feature != null) { + List> geometry = feature['geometry']['coordinates'] .map( (ll) => ll.map((l) => LatLng(l[1], l[0])).toList().cast()) .toList() @@ -304,6 +342,7 @@ class MapUiBodyState extends State { scrollGesturesEnabled: _scrollGesturesEnabled, tiltGesturesEnabled: _tiltGesturesEnabled, zoomGesturesEnabled: _zoomGesturesEnabled, + doubleClickZoomEnabled: _doubleClickToZoomEnabled, myLocationEnabled: _myLocationEnabled, myLocationTrackingMode: _myLocationTrackingMode, myLocationRenderMode: MyLocationRenderMode.GPS, @@ -312,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) { @@ -352,53 +391,51 @@ class MapUiBodyState extends State { }, ); - final List columnChildren = [ - Padding( - padding: const EdgeInsets.all(10.0), - child: Center( + final List listViewChildren = []; + + if (mapController != null) { + listViewChildren.addAll( + [ + Text('camera bearing: ${_position.bearing}'), + Text('camera target: ${_position.target.latitude.toStringAsFixed(4)},' + '${_position.target.longitude.toStringAsFixed(4)}'), + Text('camera zoom: ${_position.zoom}'), + Text('camera tilt: ${_position.tilt}'), + Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), + _mapSizeToggler(), + _queryFilterToggler(), + _compassToggler(), + _myLocationTrackingModeCycler(), + _latLngBoundsToggler(), + _setStyleToSatellite(), + _zoomBoundsToggler(), + _rotateToggler(), + _scrollToggler(), + _doubleClickToZoomToggler(), + _tiltToggler(), + _zoomToggler(), + _myLocationToggler(), + _telemetryToggler(), + _visibleRegionGetter(), + ], + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( child: SizedBox( - width: 300.0, + width: _mapExpanded ? null : 300.0, height: 200.0, child: mapboxMap, ), ), - ), - ]; - - if (mapController != null) { - columnChildren.add( Expanded( child: ListView( - children: [ - Text('camera bearing: ${_position!.bearing}'), - Text( - 'camera target: ${_position!.target.latitude.toStringAsFixed(4)},' - '${_position!.target.longitude.toStringAsFixed(4)}'), - Text('camera zoom: ${_position!.zoom}'), - Text('camera tilt: ${_position!.tilt}'), - Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), - _queryFilterToggler(), - _compassToggler(), - _myLocationTrackingModeCycler(), - _latLngBoundsToggler(), - _setStyleToSatellite(), - _zoomBoundsToggler(), - _rotateToggler(), - _scrollToggler(), - _tiltToggler(), - _zoomToggler(), - _myLocationToggler(), - _telemetryToggler(), - _visibleRegionGetter(), - ], + children: listViewChildren, ), - ), - ); - } - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: columnChildren, + ) + ], ); } diff --git a/example/lib/place_fill.dart b/example/lib/place_fill.dart index 74c6187e1..5b6420968 100644 --- a/example/lib/place_fill.dart +++ b/example/lib/place_fill.dart @@ -34,6 +34,22 @@ class PlaceFillBodyState extends State { static final LatLng center = const LatLng(-33.86711, 151.1947171); final String _fillPatternImage = "assets/fill/cat_silhouette_pattern.png"; + final List> _defaultGeometry = [ + [ + LatLng(-33.719, 151.150), + LatLng(-33.858, 151.150), + LatLng(-33.866, 151.401), + LatLng(-33.747, 151.328), + LatLng(-33.719, 151.150), + ], + [ + LatLng(-33.762, 151.250), + LatLng(-33.827, 151.250), + LatLng(-33.833, 151.347), + LatLng(-33.762, 151.250), + ] + ]; + MapboxMapController? controller; int _fillCount = 0; Fill? _selectedFill; @@ -41,6 +57,27 @@ class PlaceFillBodyState extends State { void _onMapCreated(MapboxMapController controller) { this.controller = controller; controller.onFillTapped.add(_onFillTapped); + this.controller!.onFeatureDrag.add(_onFeatureDrag); + } + + void _onFeatureDrag(id, + {required current, + required delta, + required origin, + required point, + required eventType}) { + DragEventType type = eventType; + switch (type) { + case DragEventType.start: + // TODO: Handle this case. + break; + case DragEventType.drag: + // TODO: Handle this case. + break; + case DragEventType.end: + // TODO: Handle this case. + break; + } } void _onStyleLoaded() { @@ -72,21 +109,10 @@ class PlaceFillBodyState extends State { void _add() { controller!.addFill( - FillOptions(geometry: [ - [ - LatLng(-33.719, 151.150), - LatLng(-33.858, 151.150), - LatLng(-33.866, 151.401), - LatLng(-33.747, 151.328), - LatLng(-33.719, 151.150), - ], - [ - LatLng(-33.762, 151.250), - LatLng(-33.827, 151.250), - LatLng(-33.833, 151.347), - LatLng(-33.762, 151.250), - ] - ], fillColor: "#FF0000", fillOutlineColor: "#FF0000"), + FillOptions( + geometry: _defaultGeometry, + fillColor: "#FF0000", + fillOutlineColor: "#FF0000"), ); setState(() { _fillCount += 1; @@ -102,7 +128,20 @@ class PlaceFillBodyState extends State { } void _changePosition() { - //TODO: Implement change position. + List>? geometry = _selectedFill!.options.geometry; + + if (geometry == null) { + geometry = _defaultGeometry; + } + + _updateSelectedFill(FillOptions( + geometry: geometry + .map((list) => list + .map( + // Move to right with 0.1 degree on longitude + (latLng) => LatLng(latLng.latitude, latLng.longitude + 0.1)) + .toList()) + .toList())); } void _changeDraggable() { diff --git a/example/lib/place_source.dart b/example/lib/place_source.dart index 1a92855fc..428dc74aa 100644 --- a/example/lib/place_source.dart +++ b/example/lib/place_source.dart @@ -35,6 +35,7 @@ class PlaceSymbolBodyState extends State { static const LAYER_ID = 'sydney_layer'; bool sourceAdded = false; + bool layerAdded = false; late MapboxMapController controller; void _onMapCreated(MapboxMapController controller) { @@ -63,20 +64,47 @@ 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.removeImageSource(imageSourceId); + return controller.removeSource(imageSourceId); } Future addLayer(String imageLayerId, String imageSourceId) { - return controller.addLayer(imageLayerId, imageSourceId); + if (layerAdded) { + removeLayer(imageLayerId); + } + setState(() => layerAdded = true); + return controller.addImageLayer(imageLayerId, imageSourceId); } Future addLayerBelow( String imageLayerId, String imageSourceId, String belowLayerId) { - return controller.addLayerBelow(imageLayerId, imageSourceId, belowLayerId); + if (layerAdded) { + removeLayer(imageLayerId); + } + setState(() => layerAdded = true); + return controller.addImageLayerBelow( + imageLayerId, imageSourceId, belowLayerId); } Future removeLayer(String imageLayerId) { + setState(() => layerAdded = false); return controller.removeLayer(imageLayerId); } @@ -86,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, ), ), ), @@ -105,55 +130,62 @@ class PlaceSymbolBodyState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Row( + Column( children: [ - Column( - children: [ - TextButton( - child: const Text('Add source (asset image)'), - onPressed: sourceAdded - ? null - : () { - addImageSourceFromAsset( - SOURCE_ID, 'assets/sydney.png') - .then((value) { - setState(() => sourceAdded = true); - }); - }, - ), - TextButton( - child: const Text('Remove source (asset image)'), - onPressed: sourceAdded - ? () async { - await removeLayer(LAYER_ID); - removeImageSource(SOURCE_ID).then((value) { - setState(() => sourceAdded = false); - }); - } - : null, - ), - TextButton( - child: const Text('Show layer'), - onPressed: sourceAdded - ? () => addLayer(LAYER_ID, SOURCE_ID) - : null, - ), - TextButton( - child: const Text('Show layer below water'), - onPressed: sourceAdded - ? () => - addLayerBelow(LAYER_ID, SOURCE_ID, 'water') - : null, - ), - TextButton( - child: const Text('Hide layer'), - onPressed: - sourceAdded ? () => removeLayer(LAYER_ID) : null, - ), - ], + TextButton( + child: const Text('Add source (asset image)'), + onPressed: sourceAdded + ? null + : () { + addImageSourceFromAsset( + SOURCE_ID, 'assets/sydney.png') + .then((value) { + setState(() => sourceAdded = true); + }); + }, + ), + 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 + ? () async { + await removeLayer(LAYER_ID); + removeImageSource(SOURCE_ID).then((value) { + setState(() => sourceAdded = false); + }); + } + : null, + ), + TextButton( + child: const Text('Show layer'), + onPressed: sourceAdded + ? () => addLayer(LAYER_ID, SOURCE_ID) + : null, + ), + TextButton( + child: const Text('Show layer below water'), + onPressed: sourceAdded + ? () => addLayerBelow(LAYER_ID, SOURCE_ID, 'water') + : null, + ), + TextButton( + child: const Text('Hide layer'), + onPressed: + sourceAdded ? () => removeLayer(LAYER_ID) : null, ), ], - ) + ), ], ), ), diff --git a/example/lib/place_symbol.dart b/example/lib/place_symbol.dart index 1a0996641..c4aa68eb9 100644 --- a/example/lib/place_symbol.dart +++ b/example/lib/place_symbol.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; +import 'dart:async'; // ignore: unnecessary_import import 'dart:core'; import 'dart:math'; import 'dart:typed_data'; @@ -87,8 +87,8 @@ class PlaceSymbolBodyState extends State { ); } - void _updateSelectedSymbol(SymbolOptions changes) { - controller!.updateSymbol(_selectedSymbol!, changes); + void _updateSelectedSymbol(SymbolOptions changes) async { + await controller!.updateSymbol(_selectedSymbol!, changes); } void _add(String iconImage) { @@ -126,6 +126,8 @@ class PlaceSymbolBodyState extends State { ) : SymbolOptions( geometry: geometry, + textField: 'Airport', + textOffset: Offset(0, 0.8), iconImage: iconImage, ); } @@ -273,7 +275,8 @@ class PlaceSymbolBodyState extends State { setState(() { _iconAllowOverlap = !_iconAllowOverlap; }); - controller!.setSymbolIconAllowOverlap(_iconAllowOverlap); + await controller!.setSymbolIconAllowOverlap(_iconAllowOverlap); + await controller!.setSymbolTextAllowOverlap(_iconAllowOverlap); } @override @@ -284,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/scrolling_map.dart b/example/lib/scrolling_map.dart index c40561735..a412a1727 100644 --- a/example/lib/scrolling_map.dart +++ b/example/lib/scrolling_map.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter/rendering.dart'; // ignore: unnecessary_import import 'package:mapbox_gl/mapbox_gl.dart'; import 'main.dart'; diff --git a/example/lib/sources.dart b/example/lib/sources.dart new file mode 100644 index 000000000..04e4c9e76 --- /dev/null +++ b/example/lib/sources.dart @@ -0,0 +1,367 @@ +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'; +import 'page.dart'; + +class StyleInfo { + final String name; + final String baseStyle; + final Future Function(MapboxMapController) addDetails; + final CameraPosition position; + + const StyleInfo( + {required this.name, + required this.baseStyle, + required this.addDetails, + required this.position}); +} + +class Sources extends ExamplePage { + Sources() : super(const Icon(Icons.map), 'Various Sources'); + + @override + Widget build(BuildContext context) { + return const FullMap(); + } +} + +class FullMap extends StatefulWidget { + const FullMap(); + + @override + State createState() => FullMapState(); +} + +class FullMapState extends State { + MapboxMapController? controller; + final watercolorRasterId = "watercolorRaster"; + int selectedStyleId = 0; + + _onMapCreated(MapboxMapController controller) { + this.controller = controller; + } + + static Future addRaster(MapboxMapController controller) async { + await controller.addSource( + "watercolor", + RasterSourceProperties( + tiles: [ + 'https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg' + ], + tileSize: 256, + attribution: + 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA'), + ); + await controller.addLayer( + "watercolor", "watercolor", RasterLayerProperties()); + } + + static Future addGeojsonCluster(MapboxMapController controller) async { + await controller.addSource( + "earthquakes", + GeojsonSourceProperties( + data: + 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson', + cluster: true, + clusterMaxZoom: 14, // Max zoom to cluster points on + clusterRadius: + 50 // Radius of each cluster when clustering points (defaults to 50) + )); + await controller.addLayer( + "earthquakes", + "earthquakes-circles", + CircleLayerProperties(circleColor: [ + Expressions.step, + [Expressions.get, 'point_count'], + '#51bbd6', + 100, + '#f1f075', + 750, + '#f28cb1' + ], circleRadius: [ + Expressions.step, + [Expressions.get, 'point_count'], + 20, + 100, + 30, + 750, + 40 + ])); + await controller.addLayer( + "earthquakes", + "earthquakes-count", + SymbolLayerProperties( + textField: [Expressions.get, 'point_count_abbreviated'], + textFont: ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], + textSize: 12, + )); + } + + 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", + VectorSourceProperties( + url: "mapbox://mapbox.mapbox-terrain-v2", + )); + + await controller.addLayer( + "terrain", + "contour", + LineLayerProperties( + lineColor: "#ff69b4", + lineWidth: 1, + lineCap: "round", + lineJoin: "round", + ), + sourceLayer: "contour"); + } + + static Future addImage(MapboxMapController controller) async { + await controller.addSource( + "radar", + ImageSourceProperties( + url: "https://docs.mapbox.com/mapbox-gl-js/assets/radar.gif", + coordinates: [ + [-80.425, 46.437], + [-71.516, 46.437], + [-71.516, 37.936], + [-80.425, 37.936] + ])); + + await controller.addRasterLayer( + "radar", + "radar", + RasterLayerProperties(rasterFadeDuration: 0), + ); + } + + static Future addVideo(MapboxMapController controller) async { + await controller.addSource( + "video", + VideoSourceProperties(urls: [ + 'https://static-assets.mapbox.com/mapbox-gl-js/drone.mp4', + 'https://static-assets.mapbox.com/mapbox-gl-js/drone.webm' + ], coordinates: [ + [-122.51596391201019, 37.56238816766053], + [-122.51467645168304, 37.56410183312965], + [-122.51309394836426, 37.563391708549425], + [-122.51423120498657, 37.56161849366671] + ])); + + await controller.addRasterLayer( + "video", + "video", + RasterLayerProperties(), + ); + } + + static Future addDem(MapboxMapController controller) async { + await controller.addSource( + "dem", + RasterDemSourceProperties( + url: "mapbox://mapbox.mapbox-terrain-dem-v1")); + + await controller.addLayer( + "dem", + "hillshade", + HillshadeLayerProperties( + hillshadeExaggeration: 1, + hillshadeShadowColor: Colors.blue.toHexStringRGB()), + ); + } + + static const _stylesAndLoaders = [ + StyleInfo( + name: "Vector", + baseStyle: MapboxStyles.LIGHT, + addDetails: addVector, + position: CameraPosition(target: LatLng(33.3832, -118.4333), zoom: 12), + ), + StyleInfo( + name: "Dem", + baseStyle: MapboxStyles.EMPTY, + addDetails: addDem, + position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 8), + ), + StyleInfo( + name: "Geojson cluster", + baseStyle: MapboxStyles.LIGHT, + 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, + addDetails: addRaster, + position: CameraPosition(target: LatLng(40, -100), zoom: 3), + ), + StyleInfo( + name: "Image", + baseStyle: MapboxStyles.DARK, + addDetails: addImage, + position: CameraPosition(target: LatLng(43, -75), zoom: 6), + ), + //video only supported on web + if (kIsWeb) + StyleInfo( + name: "Video", + baseStyle: MapboxStyles.SATELLITE, + addDetails: addVideo, + position: CameraPosition( + target: LatLng(37.562984, -122.514426), zoom: 17, bearing: -96), + ), + ]; + + _onStyleLoadedCallback() async { + final styleInfo = _stylesAndLoaders[selectedStyleId]; + styleInfo.addDetails(controller!); + controller! + .animateCamera(CameraUpdate.newCameraPosition(styleInfo.position)); + } + + @override + Widget build(BuildContext context) { + final styleInfo = _stylesAndLoaders[selectedStyleId]; + final nextName = + _stylesAndLoaders[(selectedStyleId + 1) % _stylesAndLoaders.length] + .name; + return new Scaffold( + floatingActionButton: Padding( + padding: const EdgeInsets.all(32.0), + child: FloatingActionButton.extended( + icon: Icon(Icons.swap_horiz), + label: SizedBox( + width: 120, child: Center(child: Text("To $nextName"))), + onPressed: () => setState( + () => selectedStyleId = + (selectedStyleId + 1) % _stylesAndLoaders.length, + ), + ), + ), + body: Stack( + children: [ + MapboxMap( + styleString: styleInfo.baseStyle, + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + initialCameraPosition: styleInfo.position, + onStyleLoadedCallback: _onStyleLoadedCallback, + ), + Container( + padding: EdgeInsets.all(8), + alignment: Alignment.topCenter, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Current source ${styleInfo.name}", + textScaleFactor: 1.4, + ), + ), + ), + ), + ], + )); + } +} 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 f8ca7a68e..bb0d73dcd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,10 +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: ^9.0.0 dependency_overrides: mapbox_gl_platform_interface: @@ -52,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 0ece3caab..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/Constants.swift b/ios/Classes/Constants.swift index f4acaeb6c..a640be6f9 100644 --- a/ios/Classes/Constants.swift +++ b/ios/Classes/Constants.swift @@ -16,16 +16,16 @@ class Constants { "top-left": MGLIconAnchor.topLeft, "top-right": MGLIconAnchor.topRight, "bottom-left": MGLIconAnchor.bottomLeft, - "bottom-right": MGLIconAnchor.bottomRight + "bottom-right": MGLIconAnchor.bottomRight, ] - + static let symbolTextJustificationMapping = [ "auto": MGLTextJustification.auto, "center": MGLTextJustification.center, "left": MGLTextJustification.left, - "right": MGLTextJustification.right + "right": MGLTextJustification.right, ] - + static let symbolTextAnchorMapping = [ "center": MGLTextAnchor.center, "left": MGLTextAnchor.left, @@ -35,18 +35,18 @@ class Constants { "top-left": MGLTextAnchor.topLeft, "top-right": MGLTextAnchor.topRight, "bottom-left": MGLTextAnchor.bottomLeft, - "bottom-right": MGLTextAnchor.bottomRight + "bottom-right": MGLTextAnchor.bottomRight, ] - + static let symbolTextTransformationMapping = [ "none": MGLTextTransform.none, "lowercase": MGLTextTransform.lowercase, - "uppercase": MGLTextTransform.uppercase + "uppercase": MGLTextTransform.uppercase, ] - + static let lineJoinMapping = [ "bevel": MGLLineJoin.bevel, "miter": MGLLineJoin.miter, - "round": MGLLineJoin.round + "round": MGLLineJoin.round, ] } diff --git a/ios/Classes/Convert.swift b/ios/Classes/Convert.swift index bcaed1638..fb1a521c5 100644 --- a/ios/Classes/Convert.swift +++ b/ios/Classes/Convert.swift @@ -5,13 +5,17 @@ class Convert { class func interpretMapboxMapOptions(options: Any?, delegate: MapboxMapOptionsSink) { guard let options = options as? [String: Any] else { return } if let cameraTargetBounds = options["cameraTargetBounds"] as? [[[Double]]] { - delegate.setCameraTargetBounds(bounds: MGLCoordinateBounds.fromArray(cameraTargetBounds[0])) + delegate + .setCameraTargetBounds(bounds: MGLCoordinateBounds.fromArray(cameraTargetBounds[0])) } if let compassEnabled = options["compassEnabled"] as? Bool { delegate.setCompassEnabled(compassEnabled: compassEnabled) } if let minMaxZoomPreference = options["minMaxZoomPreference"] as? [Double] { - delegate.setMinMaxZoomPreference(min: minMaxZoomPreference[0], max: minMaxZoomPreference[1]) + delegate.setMinMaxZoomPreference( + min: minMaxZoomPreference[0], + max: minMaxZoomPreference[1] + ) } if let styleString = options["styleString"] as? String { delegate.setStyleString(styleString: styleString) @@ -34,29 +38,43 @@ class Convert { if let myLocationEnabled = options["myLocationEnabled"] as? Bool { delegate.setMyLocationEnabled(myLocationEnabled: myLocationEnabled) } - if let myLocationTrackingMode = options["myLocationTrackingMode"] as? UInt, let trackingMode = MGLUserTrackingMode(rawValue: myLocationTrackingMode) { + if let myLocationTrackingMode = options["myLocationTrackingMode"] as? UInt, + let trackingMode = MGLUserTrackingMode(rawValue: myLocationTrackingMode) + { delegate.setMyLocationTrackingMode(myLocationTrackingMode: trackingMode) } - if let myLocationRenderMode = options["myLocationRenderMode"] as? Int, let renderMode = MyLocationRenderMode(rawValue: myLocationRenderMode) { + if let myLocationRenderMode = options["myLocationRenderMode"] as? Int, + let renderMode = MyLocationRenderMode(rawValue: myLocationRenderMode) + { delegate.setMyLocationRenderMode(myLocationRenderMode: renderMode) } if let logoViewMargins = options["logoViewMargins"] as? [Double] { delegate.setLogoViewMargins(x: logoViewMargins[0], y: logoViewMargins[1]) } - if let compassViewPosition = options["compassViewPosition"] as? UInt, let position = MGLOrnamentPosition(rawValue: compassViewPosition) { + if let compassViewPosition = options["compassViewPosition"] as? UInt, + let position = MGLOrnamentPosition(rawValue: compassViewPosition) + { delegate.setCompassViewPosition(position: position) } if let compassViewMargins = options["compassViewMargins"] as? [Double] { delegate.setCompassViewMargins(x: compassViewMargins[0], y: compassViewMargins[1]) } if let attributionButtonMargins = options["attributionButtonMargins"] as? [Double] { - delegate.setAttributionButtonMargins(x: attributionButtonMargins[0], y: attributionButtonMargins[1]) + delegate.setAttributionButtonMargins( + x: attributionButtonMargins[0], + y: attributionButtonMargins[1] + ) + } + if let attributionButtonPosition = options["attributionButtonPosition"] as? UInt, + let position = MGLOrnamentPosition(rawValue: attributionButtonPosition) + { + delegate.setAttributionButtonPosition(position: position) } } - + class func parseCameraUpdate(cameraUpdate: [Any], mapView: MGLMapView) -> MGLMapCamera? { guard let type = cameraUpdate[0] as? String else { return nil } - switch (type) { + switch type { case "newCameraPosition": guard let cameraPosition = cameraUpdate[1] as? [String: Any] else { return nil } return MGLMapCamera.fromDict(cameraPosition, mapView: mapView) @@ -71,14 +89,27 @@ class Convert { guard let paddingTop = cameraUpdate[3] as? CGFloat else { return nil } guard let paddingRight = cameraUpdate[4] as? CGFloat else { return nil } guard let paddingBottom = cameraUpdate[5] as? CGFloat else { return nil } - return mapView.cameraThatFitsCoordinateBounds(MGLCoordinateBounds.fromArray(bounds), edgePadding: UIEdgeInsets.init(top: paddingTop, left: paddingLeft, bottom: paddingBottom, right: paddingRight)) + return mapView.cameraThatFitsCoordinateBounds( + MGLCoordinateBounds.fromArray(bounds), + edgePadding: UIEdgeInsets( + top: paddingTop, + left: paddingLeft, + bottom: paddingBottom, + right: paddingRight + ) + ) case "newLatLngZoom": guard let coordinate = cameraUpdate[1] as? [Double] else { return nil } guard let zoom = cameraUpdate[2] as? Double else { return nil } let camera = mapView.camera camera.centerCoordinate = CLLocationCoordinate2D.fromArray(coordinate) let altitude = getAltitude(zoom: zoom, mapView: mapView) - return MGLMapCamera(lookingAtCenter: camera.centerCoordinate, altitude: altitude, pitch: camera.pitch, heading: camera.heading) + return MGLMapCamera( + lookingAtCenter: camera.centerCoordinate, + altitude: altitude, + pitch: camera.pitch, + heading: camera.heading + ) case "scrollBy": guard let x = cameraUpdate[1] as? CGFloat else { return nil } guard let y = cameraUpdate[2] as? CGFloat else { return nil } @@ -91,12 +122,13 @@ class Convert { guard let zoomBy = cameraUpdate[1] as? Double else { return nil } let camera = mapView.camera let zoom = getZoom(mapView: mapView) - let altitude = getAltitude(zoom: zoom+zoomBy, mapView: mapView) + let altitude = getAltitude(zoom: zoom + zoomBy, mapView: mapView) camera.altitude = altitude - if (cameraUpdate.count == 2) { + if cameraUpdate.count == 2 { return camera } else { - guard let point = cameraUpdate[2] as? [CGFloat], point.count == 2 else { return nil } + guard let point = cameraUpdate[2] as? [CGFloat], + point.count == 2 else { return nil } let movedPoint = CGPoint(x: point[0], y: point[1]) camera.centerCoordinate = mapView.convert(movedPoint, toCoordinateFrom: mapView) return camera @@ -134,225 +166,35 @@ class Convert { } return nil } - + class func getZoom(mapView: MGLMapView) -> Double { - return MGLZoomLevelForAltitude(mapView.camera.altitude, mapView.camera.pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + return MGLZoomLevelForAltitude( + mapView.camera.altitude, + mapView.camera.pitch, + mapView.camera.centerCoordinate.latitude, + mapView.frame.size + ) } - + class func getAltitude(zoom: Double, mapView: MGLMapView) -> Double { - return MGLAltitudeForZoomLevel(zoom, mapView.camera.pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + return MGLAltitudeForZoomLevel( + zoom, + mapView.camera.pitch, + mapView.camera.centerCoordinate.latitude, + mapView.frame.size + ) } - class func interpretSymbolOptions(options: Any?, delegate: MGLSymbolStyleAnnotation) { - guard let options = options as? [String: Any] else { return } - if let iconSize = options["iconSize"] as? CGFloat { - delegate.iconScale = iconSize - } - if let iconImage = options["iconImage"] as? String { - delegate.iconImageName = iconImage - } - if let iconRotate = options["iconRotate"] as? CGFloat { - delegate.iconRotation = iconRotate - } - if let iconOffset = options["iconOffset"] as? [Double] { - delegate.iconOffset = CGVector(dx: iconOffset[0], dy: iconOffset[1]) - } - if let iconAnchorStr = options["iconAnchor"] as? String { - if let iconAnchor = Constants.symbolIconAnchorMapping[iconAnchorStr] { - delegate.iconAnchor = iconAnchor - } else { - delegate.iconAnchor = MGLIconAnchor.center - } - } - if let iconOpacity = options["iconOpacity"] as? CGFloat { - delegate.iconOpacity = iconOpacity - } - if let iconColor = options["iconColor"] as? String { - delegate.iconColor = UIColor(hexString: iconColor) ?? UIColor.black - } - if let iconHaloColor = options["iconHaloColor"] as? String { - delegate.iconHaloColor = UIColor(hexString: iconHaloColor) ?? UIColor.white - } - if let iconHaloWidth = options["iconHaloWidth"] as? CGFloat { - delegate.iconHaloWidth = iconHaloWidth - } - if let iconHaloBlur = options["iconHaloBlur"] as? CGFloat { - delegate.iconHaloBlur = iconHaloBlur - } - if let fontNames = options["fontNames"] as? [String] { - delegate.fontNames = fontNames - } - if let textField = options["textField"] as? String { - delegate.text = textField - } - if let textSize = options["textSize"] as? CGFloat { - delegate.textFontSize = textSize - } - if let textMaxWidth = options["textMaxWidth"] as? CGFloat { - delegate.maximumTextWidth = textMaxWidth - } - if let textLetterSpacing = options["textLetterSpacing"] as? CGFloat { - delegate.textLetterSpacing = textLetterSpacing - } - if let textJustify = options["textJustify"] as? String { - if let textJustifaction = Constants.symbolTextJustificationMapping[textJustify] { - delegate.textJustification = textJustifaction - } else { - delegate.textJustification = MGLTextJustification.center - } - } - if let textRadialOffset = options["textRadialOffset"] as? CGFloat { - delegate.textRadialOffset = textRadialOffset - } - if let textAnchorStr = options["textAnchor"] as? String { - if let textAnchor = Constants.symbolTextAnchorMapping[textAnchorStr] { - delegate.textAnchor = textAnchor - } else { - delegate.textAnchor = MGLTextAnchor.center - } - } - if let textRotate = options["textRotate"] as? CGFloat { - delegate.textRotation = textRotate - } - if let textTransform = options["textTransform"] as? String { - if let textTransformation = Constants.symbolTextTransformationMapping[textTransform] { - delegate.textTransform = textTransformation - } else { - delegate.textTransform = MGLTextTransform.none - } - } - if let textTranslate = options["textTranslate"] as? [Double] { - delegate.textTranslation = CGVector(dx: textTranslate[0], dy: textTranslate[1]) - } - if let textOffset = options["textOffset"] as? [Double] { - delegate.textOffset = CGVector(dx: textOffset[0], dy: textOffset[1]) - } - if let textOpacity = options["textOpacity"] as? CGFloat { - delegate.textOpacity = textOpacity - } - if let textColor = options["textColor"] as? String { - delegate.textColor = UIColor(hexString: textColor) ?? UIColor.black - } - if let textHaloColor = options["textHaloColor"] as? String { - delegate.textHaloColor = UIColor(hexString: textHaloColor) ?? UIColor.white - } - if let textHaloWidth = options["textHaloWidth"] as? CGFloat { - delegate.textHaloWidth = textHaloWidth - } - if let textHaloBlur = options["textHaloBlur"] as? CGFloat { - delegate.textHaloBlur = textHaloBlur - } - if let geometry = options["geometry"] as? [Double] { - // We cannot set the geometry directy on the annotation so calculate - // the difference and update the coordinate using the delta. - let currCoord = delegate.feature.coordinate - let newCoord = CLLocationCoordinate2DMake(geometry[0], geometry[1]) - let delta = CGVector(dx: newCoord.longitude - currCoord.longitude, dy: newCoord.latitude - currCoord.latitude) - delegate.updateGeometryCoordinates(withDelta: delta) - } - if let zIndex = options["zIndex"] as? Int { - delegate.symbolSortKey = zIndex - } - if let draggable = options["draggable"] as? Bool { - delegate.isDraggable = draggable - } - } - - class func interpretCircleOptions(options: Any?, delegate: MGLCircleStyleAnnotation) { - guard let options = options as? [String: Any] else { return } - if let circleRadius = options["circleRadius"] as? CGFloat { - delegate.circleRadius = circleRadius - } - if let circleColor = options["circleColor"] as? String { - delegate.circleColor = UIColor(hexString: circleColor) ?? UIColor.black - } - if let circleBlur = options["circleBlur"] as? CGFloat { - delegate.circleBlur = circleBlur - } - if let circleOpacity = options["circleOpacity"] as? CGFloat { - delegate.circleOpacity = circleOpacity - } - if let circleStrokeWidth = options["circleStrokeWidth"] as? CGFloat { - delegate.circleStrokeWidth = circleStrokeWidth - } - if let circleStrokeColor = options["circleStrokeColor"] as? String { - delegate.circleStrokeColor = UIColor(hexString: circleStrokeColor) ?? UIColor.black - } - if let circleStrokeOpacity = options["circleStrokeOpacity"] as? CGFloat { - delegate.circleStrokeOpacity = circleStrokeOpacity - } - if let geometry = options["geometry"] as? [Double] { - delegate.center = CLLocationCoordinate2DMake(geometry[0], geometry[1]) - } - if let draggable = options["draggable"] as? Bool { - delegate.isDraggable = draggable - } - } - - class func interpretLineOptions(options: Any?, delegate: MGLLineStyleAnnotation) { - guard let options = options as? [String: Any] else { return } - if let lineJoinStr = options["lineJoin"] as? String { - if let lineJoin = Constants.lineJoinMapping[lineJoinStr] { - delegate.lineJoin = lineJoin - } else { - delegate.lineJoin = MGLLineJoin.miter - } - } - if let lineOpacity = options["lineOpacity"] as? CGFloat { - delegate.lineOpacity = lineOpacity - } - if let lineColor = options["lineColor"] as? String { - delegate.lineColor = UIColor(hexString: lineColor) ?? UIColor.black - } - if let lineWidth = options["lineWidth"] as? CGFloat { - delegate.lineWidth = lineWidth - } - if let lineGapWidth = options["lineGapWidth"] as? CGFloat { - delegate.lineGapWidth = lineGapWidth - } - if let lineOffset = options["lineOffset"] as? CGFloat { - delegate.lineOffset = lineOffset - } - if let lineBlur = options["lineBlur"] as? CGFloat { - delegate.lineBlur = lineBlur - } - if let linePattern = options["linePattern"] as? String { - delegate.linePattern = linePattern - } - if let draggable = options["draggable"] as? Bool { - delegate.isDraggable = draggable - } - } - - class func interpretFillOptions(options: Any?, delegate: MGLPolygonStyleAnnotation) { - guard let options = options as? [String: Any] else { return } - if let fillOpacity = options["fillOpacity"] as? CGFloat { - delegate.fillOpacity = fillOpacity - } - if let fillColor = options["fillColor"] as? String { - delegate.fillColor = UIColor(hexString: fillColor) ?? UIColor.black - } - if let fillOutlineColor = options["fillOutlineColor"] as? String { - delegate.fillOutlineColor = UIColor(hexString: fillOutlineColor) ?? UIColor.black - } - if let fillPattern = options["fillPattern"] as? String { - delegate.fillPattern = fillPattern - } - if let draggable = options["draggable"] as? Bool { - delegate.isDraggable = draggable - } - } + class func getCoordinates(options: Any?) -> [CLLocationCoordinate2D] { + var coordinates: [CLLocationCoordinate2D] = [] - class func toPolygons(geometry: [[[Double]]]) -> [MGLPolygonFeature] { - var polygons:[MGLPolygonFeature] = [] - for lineString in geometry { - var linearRing: [CLLocationCoordinate2D] = [] - for coordinate in lineString { - linearRing.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) + if let options = options as? [String: Any], + let geometry = options["geometry"] as? [[Double]], geometry.count > 0 + { + for coordinate in geometry { + coordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) } - let polygon = MGLPolygonFeature(coordinates: linearRing, count: UInt(linearRing.count)) - polygons.append(polygon) } - return polygons + return coordinates } } diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift index f433d3ba6..9d1d0374a 100644 --- a/ios/Classes/Extensions.swift +++ b/ios/Classes/Extensions.swift @@ -2,62 +2,73 @@ import Mapbox extension MGLMapCamera { func toDict(mapView: MGLMapView) -> [String: Any] { - let zoom = MGLZoomLevelForAltitude(self.altitude, self.pitch, self.centerCoordinate.latitude, mapView.frame.size) - return ["bearing": self.heading, - "target": self.centerCoordinate.toArray(), - "tilt": self.pitch, + let zoom = MGLZoomLevelForAltitude( + altitude, + pitch, + centerCoordinate.latitude, + mapView.frame.size + ) + return ["bearing": heading, + "target": centerCoordinate.toArray(), + "tilt": pitch, "zoom": zoom] } + static func fromDict(_ dict: [String: Any], mapView: MGLMapView) -> MGLMapCamera? { guard let target = dict["target"] as? [Double], - let zoom = dict["zoom"] as? Double, - let tilt = dict["tilt"] as? CGFloat, - let bearing = dict["bearing"] as? Double else { return nil } + let zoom = dict["zoom"] as? Double, + let tilt = dict["tilt"] as? CGFloat, + let bearing = dict["bearing"] as? Double else { return nil } let location = CLLocationCoordinate2D.fromArray(target) let altitude = MGLAltitudeForZoomLevel(zoom, tilt, location.latitude, mapView.frame.size) - return MGLMapCamera(lookingAtCenter: location, altitude: altitude, pitch: tilt, heading: bearing) + return MGLMapCamera( + lookingAtCenter: location, + altitude: altitude, + pitch: tilt, + heading: bearing + ) } } extension CLLocation { func toDict() -> [String: Any]? { - return ["position": self.coordinate.toArray(), - "altitude": self.altitude, - "bearing": self.course, - "speed": self.speed, - "horizontalAccuracy": self.horizontalAccuracy, - "verticalAccuracy": self.verticalAccuracy, - "timestamp": Int(self.timestamp.timeIntervalSince1970 * 1000) - ] + return ["position": coordinate.toArray(), + "altitude": altitude, + "bearing": course, + "speed": speed, + "horizontalAccuracy": horizontalAccuracy, + "verticalAccuracy": verticalAccuracy, + "timestamp": Int(timestamp.timeIntervalSince1970 * 1000)] } } extension CLHeading { func toDict() -> [String: Any]? { - return ["magneticHeading": self.magneticHeading, - "trueHeading": self.trueHeading, - "headingAccuracy": self.headingAccuracy, - "x": self.x, - "y": self.y, - "z": self.z, - "timestamp": Int(self.timestamp.timeIntervalSince1970 * 1000), - ] + return ["magneticHeading": magneticHeading, + "trueHeading": trueHeading, + "headingAccuracy": headingAccuracy, + "x": x, + "y": y, + "z": z, + "timestamp": Int(timestamp.timeIntervalSince1970 * 1000)] } } extension CLLocationCoordinate2D { - func toArray() -> [Double] { - return [self.latitude, self.longitude] + func toArray() -> [Double] { + return [latitude, longitude] } + static func fromArray(_ array: [Double]) -> CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: array[0], longitude: array[1]) } } extension MGLCoordinateBounds { - func toArray() -> [[Double]] { - return [self.sw.toArray(), self.ne.toArray()] + func toArray() -> [[Double]] { + return [sw.toArray(), ne.toArray()] } + static func fromArray(_ array: [[Double]]) -> MGLCoordinateBounds { let southwest = CLLocationCoordinate2D.fromArray(array[0]) let northeast = CLLocationCoordinate2D.fromArray(array[1]) @@ -70,17 +81,21 @@ extension UIImage { // Add the trailing slash in path if missing. let path = imagePath.hasSuffix("/") ? imagePath : "\(imagePath)/" // Build scale dependant image path. - let scale = UIScreen.main.scale + var scale = UIScreen.main.scale var absolutePath = "\(path)\(scale)x/\(imageName)" // Check if the image exists, if not try a an unscaled path. if Bundle.main.path(forResource: absolutePath, ofType: nil) == nil { absolutePath = "\(path)\(imageName)" + } else { + // found asset with higher resolution - increase scale even further to compensate + scale *= scale } // Load image if it exists. if let path = Bundle.main.path(forResource: absolutePath, ofType: nil) { - let imageUrl: URL = URL(fileURLWithPath: path) - if let imageData: Data = try? Data(contentsOf: imageUrl), - let image: UIImage = UIImage(data: imageData, scale: UIScreen.main.scale) { + let imageUrl = URL(fileURLWithPath: path) + if let imageData: Data = try? Data(contentsOf: imageUrl), + let image = UIImage(data: imageData, scale: scale) + { return image } } @@ -88,48 +103,46 @@ extension UIImage { } } -extension UIColor { - public convenience init?(hexString: String) { +public extension UIColor { + convenience init?(hexString: String) { let r, g, b, a: CGFloat - + if hexString.hasPrefix("#") { let start = hexString.index(hexString.startIndex, offsetBy: 1) let hexColor = hexString[start...] - + let scanner = Scanner(string: String(hexColor)) var hexNumber: UInt64 = 0 - + if hexColor.count == 6 { if scanner.scanHexInt64(&hexNumber) { - r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 - g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 - b = CGFloat(hexNumber & 0x0000ff) / 255 + r = CGFloat((hexNumber & 0xFF0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00FF00) >> 8) / 255 + b = CGFloat(hexNumber & 0x0000FF) / 255 a = 255 - + self.init(red: r, green: g, blue: b, alpha: a) return } - } - else if hexColor.count == 8 { + } else if hexColor.count == 8 { if scanner.scanHexInt64(&hexNumber) { - a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - r = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 - g = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 - b = CGFloat(hexNumber & 0x000000ff) / 255 - + a = CGFloat((hexNumber & 0xFF00_0000) >> 24) / 255 + r = CGFloat((hexNumber & 0x00FF_0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x0000_FF00) >> 8) / 255 + b = CGFloat(hexNumber & 0x0000_00FF) / 255 + self.init(red: r, green: g, blue: b, alpha: a) return } } } - + return nil } } - extension Array { var tail: Array { - return Array(self.dropFirst()) + return Array(dropFirst()) } } diff --git a/ios/Classes/LayerPropertyConverter.swift b/ios/Classes/LayerPropertyConverter.swift new file mode 100644 index 000000000..b711adcb9 --- /dev/null +++ b/ios/Classes/LayerPropertyConverter.swift @@ -0,0 +1,418 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +import Mapbox +import MapboxAnnotationExtension + +class LayerPropertyConverter { + class func addSymbolProperties(symbolLayer: MGLSymbolStyleLayer, properties: [String: String]) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "icon-opacity": + symbolLayer.iconOpacity = expression + case "icon-color": + symbolLayer.iconColor = expression + case "icon-halo-color": + symbolLayer.iconHaloColor = expression + case "icon-halo-width": + symbolLayer.iconHaloWidth = expression + case "icon-halo-blur": + symbolLayer.iconHaloBlur = expression + case "icon-translate": + symbolLayer.iconTranslation = expression + case "icon-translate-anchor": + symbolLayer.iconTranslationAnchor = expression + case "text-opacity": + symbolLayer.textOpacity = expression + case "text-color": + symbolLayer.textColor = expression + case "text-halo-color": + symbolLayer.textHaloColor = expression + case "text-halo-width": + symbolLayer.textHaloWidth = expression + case "text-halo-blur": + symbolLayer.textHaloBlur = expression + case "text-translate": + symbolLayer.textTranslation = expression + case "text-translate-anchor": + symbolLayer.textTranslationAnchor = expression + case "symbol-placement": + symbolLayer.symbolPlacement = expression + case "symbol-spacing": + symbolLayer.symbolSpacing = expression + case "symbol-avoid-edges": + symbolLayer.symbolAvoidsEdges = expression + case "symbol-sort-key": + symbolLayer.symbolSortKey = expression + case "symbol-z-order": + symbolLayer.symbolZOrder = expression + case "icon-allow-overlap": + symbolLayer.iconAllowsOverlap = expression + case "icon-ignore-placement": + symbolLayer.iconIgnoresPlacement = expression + case "icon-optional": + symbolLayer.iconOptional = expression + case "icon-rotation-alignment": + symbolLayer.iconRotationAlignment = expression + case "icon-size": + symbolLayer.iconScale = expression + case "icon-text-fit": + symbolLayer.iconTextFit = expression + case "icon-text-fit-padding": + symbolLayer.iconTextFitPadding = expression + case "icon-image": + symbolLayer.iconImageName = expression + case "icon-rotate": + symbolLayer.iconRotation = expression + case "icon-padding": + symbolLayer.iconPadding = expression + case "icon-keep-upright": + symbolLayer.keepsIconUpright = expression + case "icon-offset": + symbolLayer.iconOffset = expression + case "icon-anchor": + symbolLayer.iconAnchor = expression + case "icon-pitch-alignment": + symbolLayer.iconPitchAlignment = expression + case "text-pitch-alignment": + symbolLayer.textPitchAlignment = expression + case "text-rotation-alignment": + symbolLayer.textRotationAlignment = expression + case "text-field": + symbolLayer.text = expression + case "text-font": + symbolLayer.textFontNames = expression + case "text-size": + symbolLayer.textFontSize = expression + case "text-max-width": + symbolLayer.maximumTextWidth = expression + case "text-line-height": + symbolLayer.textLineHeight = expression + case "text-letter-spacing": + symbolLayer.textLetterSpacing = expression + case "text-justify": + symbolLayer.textJustification = expression + case "text-radial-offset": + symbolLayer.textRadialOffset = expression + case "text-variable-anchor": + symbolLayer.textVariableAnchor = expression + case "text-anchor": + symbolLayer.textAnchor = expression + case "text-max-angle": + symbolLayer.maximumTextAngle = expression + case "text-writing-mode": + symbolLayer.textWritingModes = expression + case "text-rotate": + symbolLayer.textRotation = expression + case "text-padding": + symbolLayer.textPadding = expression + case "text-keep-upright": + symbolLayer.keepsTextUpright = expression + case "text-transform": + symbolLayer.textTransform = expression + case "text-offset": + symbolLayer.textOffset = expression + case "text-allow-overlap": + symbolLayer.textAllowsOverlap = expression + case "text-ignore-placement": + symbolLayer.textIgnoresPlacement = expression + case "text-optional": + symbolLayer.textOptional = expression + case "visibility": + symbolLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + class func addCircleProperties(circleLayer: MGLCircleStyleLayer, properties: [String: String]) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "circle-radius": + circleLayer.circleRadius = expression + case "circle-color": + circleLayer.circleColor = expression + case "circle-blur": + circleLayer.circleBlur = expression + case "circle-opacity": + circleLayer.circleOpacity = expression + case "circle-translate": + circleLayer.circleTranslation = expression + case "circle-translate-anchor": + circleLayer.circleTranslationAnchor = expression + case "circle-pitch-scale": + circleLayer.circleScaleAlignment = expression + case "circle-pitch-alignment": + circleLayer.circlePitchAlignment = expression + case "circle-stroke-width": + circleLayer.circleStrokeWidth = expression + case "circle-stroke-color": + circleLayer.circleStrokeColor = expression + case "circle-stroke-opacity": + circleLayer.circleStrokeOpacity = expression + case "circle-sort-key": + circleLayer.circleSortKey = expression + case "visibility": + circleLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + class func addLineProperties(lineLayer: MGLLineStyleLayer, properties: [String: String]) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "line-opacity": + lineLayer.lineOpacity = expression + case "line-color": + lineLayer.lineColor = expression + case "line-translate": + lineLayer.lineTranslation = expression + case "line-translate-anchor": + lineLayer.lineTranslationAnchor = expression + case "line-width": + lineLayer.lineWidth = expression + case "line-gap-width": + lineLayer.lineGapWidth = expression + case "line-offset": + lineLayer.lineOffset = expression + case "line-blur": + lineLayer.lineBlur = expression + case "line-dasharray": + lineLayer.lineDashPattern = expression + case "line-pattern": + lineLayer.linePattern = expression + case "line-gradient": + lineLayer.lineGradient = expression + case "line-cap": + lineLayer.lineCap = expression + case "line-join": + lineLayer.lineJoin = expression + case "line-miter-limit": + lineLayer.lineMiterLimit = expression + case "line-round-limit": + lineLayer.lineRoundLimit = expression + case "line-sort-key": + lineLayer.lineSortKey = expression + case "visibility": + lineLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + class func addFillProperties(fillLayer: MGLFillStyleLayer, properties: [String: String]) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "fill-antialias": + fillLayer.fillAntialiased = expression + case "fill-opacity": + fillLayer.fillOpacity = expression + case "fill-color": + fillLayer.fillColor = expression + case "fill-outline-color": + fillLayer.fillOutlineColor = expression + case "fill-translate": + fillLayer.fillTranslation = expression + case "fill-translate-anchor": + fillLayer.fillTranslationAnchor = expression + case "fill-pattern": + fillLayer.fillPattern = expression + case "fill-sort-key": + fillLayer.fillSortKey = expression + case "visibility": + fillLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + 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( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "raster-opacity": + rasterLayer.rasterOpacity = expression + case "raster-hue-rotate": + rasterLayer.rasterHueRotation = expression + case "raster-brightness-min": + rasterLayer.minimumRasterBrightness = expression + case "raster-brightness-max": + rasterLayer.maximumRasterBrightness = expression + case "raster-saturation": + rasterLayer.rasterSaturation = expression + case "raster-contrast": + rasterLayer.rasterContrast = expression + case "raster-resampling": + rasterLayer.rasterResamplingMode = expression + case "raster-fade-duration": + rasterLayer.rasterFadeDuration = expression + case "visibility": + rasterLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + class func addHillshadeProperties( + hillshadeLayer: MGLHillshadeStyleLayer, + properties: [String: String] + ) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression( + propertyName: propertyName, + expression: propertyValue + ) + switch propertyName { + case "hillshade-illumination-direction": + hillshadeLayer.hillshadeIlluminationDirection = expression + case "hillshade-illumination-anchor": + hillshadeLayer.hillshadeIlluminationAnchor = expression + case "hillshade-exaggeration": + hillshadeLayer.hillshadeExaggeration = expression + case "hillshade-shadow-color": + hillshadeLayer.hillshadeShadowColor = expression + case "hillshade-highlight-color": + hillshadeLayer.hillshadeHighlightColor = expression + case "hillshade-accent-color": + hillshadeLayer.hillshadeAccentColor = expression + case "visibility": + hillshadeLayer.isVisible = propertyValue == "visible" + + default: + break + } + } + } + + 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? + { + let isColor = propertyName.contains("color") + + do { + let json = try JSONSerialization.jsonObject( + with: expression.data(using: .utf8)!, + options: .fragmentsAllowed + ) + // this is required because NSExpression.init(mglJSONObject: json) fails to create + // a proper Expression if the data of is a hexString + if isColor { + if let color = json as? String { + return NSExpression(forConstantValue: UIColor(hexString: color)) + } + } + // this is required because NSExpression.init(mglJSONObject: json) fails to create + // a proper Expression if the data of a literal is an array + if let offset = json as? [Any] { + if offset.count == 2, offset.first is String, offset.first as? String == "literal" { + if let vector = offset.last as? [Any] { + if vector.count == 2 { + if let x = vector.first as? Double, let y = vector.last as? Double { + return NSExpression( + forConstantValue: NSValue(cgVector: CGVector(dx: x, + dy: y)) + ) + } + } + } + } + } + return NSExpression(mglJSONObject: json) + } catch {} + return nil + } +} diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 73482bbb7..4af7ff3b4 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -1,59 +1,79 @@ import Flutter -import UIKit import Mapbox import MapboxAnnotationExtension +import UIKit -class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, MapboxMapOptionsSink, MGLAnnotationControllerDelegate { - +class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, MapboxMapOptionsSink, + UIGestureRecognizerDelegate +{ private var registrar: FlutterPluginRegistrar private var channel: FlutterMethodChannel? - + private var mapView: MGLMapView private var isMapReady = false + private var dragEnabled = true + private var isFirstStyleLoad = true + private var onStyleLoadedCalled = false private var mapReadyResult: FlutterResult? - + private var previousDragCoordinate: CLLocationCoordinate2D? + private var originDragCoordinate: CLLocationCoordinate2D? + private var dragFeature: MGLFeature? + private var initialTilt: CGFloat? private var cameraTargetBounds: MGLCoordinateBounds? private var trackCameraPosition = false private var myLocationEnabled = false + private var scrollingEnabled = true - private var symbolAnnotationController: MGLSymbolAnnotationController? - private var circleAnnotationController: MGLCircleAnnotationController? - private var lineAnnotationController: MGLLineAnnotationController? - private var fillAnnotationController: MGLPolygonAnnotationController? - - private var annotationOrder = [String]() - private var annotationConsumeTapEvents = [String]() + private var interactiveFeatureLayerIds = Set() + private var addedShapesByLayer = [String: MGLShape]() func view() -> UIView { return mapView } - - init(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, registrar: FlutterPluginRegistrar) { + + init( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any?, + registrar: FlutterPluginRegistrar + ) { if let args = args as? [String: Any] { - if let token = args["accessToken"] as? String { + if let token = args["accessToken"] as? String? { MGLAccountManager.accessToken = token } } mapView = MGLMapView(frame: frame) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.registrar = registrar - + super.init() - - channel = FlutterMethodChannel(name: "plugins.flutter.io/mapbox_maps_\(viewId)", binaryMessenger: registrar.messenger()) - channel!.setMethodCallHandler{ [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } - + + channel = FlutterMethodChannel( + name: "plugins.flutter.io/mapbox_maps_\(viewId)", + binaryMessenger: registrar.messenger() + ) + channel! + .setMethodCallHandler { [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } + mapView.delegate = self - - let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:))) + + let singleTap = UITapGestureRecognizer( + target: self, + action: #selector(handleMapTap(sender:)) + ) for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer { singleTap.require(toFail: recognizer) } mapView.addGestureRecognizer(singleTap) - - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleMapLongPress(sender:))) - for recognizer in mapView.gestureRecognizers! where recognizer is UILongPressGestureRecognizer { + + let longPress = UILongPressGestureRecognizer( + target: self, + action: #selector(handleMapLongPress(sender:)) + ) + for recognizer in mapView.gestureRecognizers! + where recognizer is UILongPressGestureRecognizer + { longPress.require(toFail: recognizer) } mapView.addGestureRecognizer(longPress) @@ -61,36 +81,64 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma if let args = args as? [String: Any] { Convert.interpretMapboxMapOptions(options: args["options"], delegate: self) if let initialCameraPosition = args["initialCameraPosition"] as? [String: Any], - let camera = MGLMapCamera.fromDict(initialCameraPosition, mapView: mapView), - let zoom = initialCameraPosition["zoom"] as? Double { - mapView.setCenter(camera.centerCoordinate, zoomLevel: zoom, direction: camera.heading, animated: false) + let camera = MGLMapCamera.fromDict(initialCameraPosition, mapView: mapView), + let zoom = initialCameraPosition["zoom"] as? Double + { + mapView.setCenter( + camera.centerCoordinate, + zoomLevel: zoom, + direction: camera.heading, + animated: false + ) initialTilt = camera.pitch } - if let annotationOrderArg = args["annotationOrder"] as? [String] { - annotationOrder = annotationOrderArg - } - if let annotationConsumeTapEventsArg = args["annotationConsumeTapEvents"] as? [String] { - annotationConsumeTapEvents = annotationConsumeTapEventsArg - } if let onAttributionClickOverride = args["onAttributionClickOverride"] as? Bool { - if onAttributionClickOverride { + if onAttributionClickOverride { setupAttribution(mapView) } } + + if let enabled = args["dragEnabled"] as? Bool { + dragEnabled = enabled + } + } + if dragEnabled { + let pan = UIPanGestureRecognizer( + target: self, + action: #selector(handleMapPan(sender:)) + ) + pan.delegate = self + mapView.addGestureRecognizer(pan) } } - func removeAllForController(controller: MGLAnnotationController, ids: [String]){ + func removeAllForController(controller: MGLAnnotationController, ids: [String]) { let idSet = Set(ids) let annotations = controller.styleAnnotations() controller.removeStyleAnnotations(annotations.filter { idSet.contains($0.identifier) }) } - + + func gestureRecognizer( + _: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer + ) -> Bool { + return true + } + func onMethodCall(methodCall: FlutterMethodCall, result: @escaping FlutterResult) { - switch(methodCall.method) { + switch methodCall.method { case "map#waitForMap": if isMapReady { result(nil) + // only call map#onStyleLoaded here if isMapReady has happend and isFirstStyleLoad is true + if isFirstStyleLoad { + isFirstStyleLoad = false + + if let channel = channel { + onStyleLoadedCalled = true + channel.invokeMethod("map#onStyleLoaded", arguments: nil) + } + } } else { mapReadyResult = result } @@ -103,17 +151,19 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma result(nil) } case "map#invalidateAmbientCache": - MGLOfflineStorage.shared.invalidateAmbientCache{ - (error) in + MGLOfflineStorage.shared.invalidateAmbientCache { + error in if let error = error { result(error) - } else{ + } else { result(nil) } } case "map#updateMyLocationTrackingMode": guard let arguments = methodCall.arguments as? [String: Any] else { return } - if let myLocationTrackingMode = arguments["mode"] as? UInt, let trackingMode = MGLUserTrackingMode(rawValue: myLocationTrackingMode) { + if let myLocationTrackingMode = arguments["mode"] as? UInt, + let trackingMode = MGLUserTrackingMode(rawValue: myLocationTrackingMode) + { setMyLocationTrackingMode(myLocationTrackingMode: trackingMode) } result(nil) @@ -126,17 +176,30 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma guard let arguments = methodCall.arguments as? [String: Any] else { return } if let bounds = arguments["bounds"] as? [String: Any], - let top = bounds["top"] as? CGFloat, - let left = bounds["left"] as? CGFloat, - let bottom = bounds["bottom"] as? CGFloat, - let right = bounds["right"] as? CGFloat, - let animated = arguments["animated"] as? Bool { - mapView.setContentInset(UIEdgeInsets(top: top, left: left, bottom: bottom, right: right), animated: animated) { + let top = bounds["top"] as? CGFloat, + let left = bounds["left"] as? CGFloat, + let bottom = bounds["bottom"] as? CGFloat, + let right = bounds["right"] as? CGFloat, + let animated = arguments["animated"] as? Bool + { + mapView.setContentInset( + UIEdgeInsets(top: top, left: left, bottom: bottom, right: right), + animated: animated + ) { result(nil) } } else { result(nil) } + case "locationComponent#getLastLocation": + var reply = [String: NSObject]() + if let loc = mapView.userLocation?.location?.coordinate { + reply["latitude"] = loc.latitude as NSObject + reply["longitude"] = loc.longitude as NSObject + result(reply) + } else { + result(nil) + } case "map#setMapLanguage": guard let arguments = methodCall.arguments as? [String: Any] else { return } if let localIdentifier = arguments["language"] as? String, let style = mapView.style { @@ -146,27 +209,44 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma result(nil) case "map#queryRenderedFeatures": guard let arguments = methodCall.arguments as? [String: Any] else { return } - let layerIds = arguments["layerIds"] as? Set + var styleLayerIdentifiers: Set? + if let layerIds = arguments["layerIds"] as? [String] { + styleLayerIdentifiers = Set(layerIds) + } var filterExpression: NSPredicate? if let filter = arguments["filter"] as? [Any] { filterExpression = NSPredicate(mglJSONObject: filter) } var reply = [String: NSObject]() - var features:[MGLFeature] = [] + var features: [MGLFeature] = [] if let x = arguments["x"] as? Double, let y = arguments["y"] as? Double { - features = mapView.visibleFeatures(at: CGPoint(x: x, y: y), styleLayerIdentifiers: layerIds, predicate: filterExpression) + features = mapView.visibleFeatures( + at: CGPoint(x: x, y: y), + styleLayerIdentifiers: styleLayerIdentifiers, + predicate: filterExpression + ) } - 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 { - features = mapView.visibleFeatures(in: CGRect(x: left, y: top, width: right, height: bottom), styleLayerIdentifiers: layerIds, predicate: filterExpression) + if let top = arguments["top"] as? Double, + let left = arguments["left"] 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: width, height: height), + styleLayerIdentifiers: styleLayerIdentifiers, + predicate: filterExpression + ) } var featuresJson = [String]() for feature in features { let dictionary = feature.geoJSONDictionary() - if let theJSONData = try? JSONSerialization.data(withJSONObject: dictionary, options: []), - let theJSONText = String(data: theJSONData, encoding: .ascii) { + if let theJSONData = try? JSONSerialization.data( + withJSONObject: dictionary, + options: [] + ), + let theJSONText = String(data: theJSONData, encoding: .ascii) + { featuresJson.append(theJSONText) } } @@ -203,7 +283,8 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma Array( UnsafeBufferPointer( start: $0.baseAddress!.assumingMemoryBound(to: Double.self), - count:Int(data.elementCount)) + count: Int(data.elementCount) + ) ) } var reply: [Double] = Array(repeating: 0.0, count: latLngs.count) @@ -214,20 +295,24 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma reply[i + 1] = Double(returnVal.y) } result(FlutterStandardTypedData( - float64: Data(bytes: &reply, count: reply.count * 8) )) + float64: Data(bytes: &reply, count: reply.count * 8) + )) case "map#getMetersPerPixelAtLatitude": - guard let arguments = methodCall.arguments as? [String: Any] else { return } - var reply = [String: NSObject]() - guard let latitude = arguments["latitude"] as? Double else { return } - let returnVal = mapView.metersPerPoint(atLatitude:latitude) - reply["metersperpixel"] = returnVal as NSObject - result(reply) + guard let arguments = methodCall.arguments as? [String: Any] else { return } + var reply = [String: NSObject]() + guard let latitude = arguments["latitude"] as? Double else { return } + let returnVal = mapView.metersPerPoint(atLatitude: latitude) + reply["metersperpixel"] = returnVal as NSObject + result(reply) case "map#toLatLng": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let x = arguments["x"] as? Double else { return } guard let y = arguments["y"] as? Double else { return } - let screenPoint: CGPoint = CGPoint(x: x, y:y) - let coordinates: CLLocationCoordinate2D = mapView.convert(screenPoint, toCoordinateFrom: mapView) + let screenPoint = CGPoint(x: x, y: y) + let coordinates: CLLocationCoordinate2D = mapView.convert( + screenPoint, + toCoordinateFrom: mapView + ) var reply = [String: NSObject]() reply["latitude"] = coordinates.latitude as NSObject reply["longitude"] = coordinates.longitude as NSObject @@ -235,589 +320,772 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case "camera#move": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let cameraUpdate = arguments["cameraUpdate"] as? [Any] else { return } - if let camera = Convert.parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) { + if let camera = Convert + .parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) + { mapView.setCamera(camera, animated: false) } result(nil) case "camera#animate": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let cameraUpdate = arguments["cameraUpdate"] as? [Any] else { return } - if let camera = Convert.parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) { + if let camera = Convert + .parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) + { if let duration = arguments["duration"] as? TimeInterval { - mapView.setCamera(camera, withDuration: TimeInterval(duration / 1000), - animationTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)) + mapView.setCamera(camera, withDuration: TimeInterval(duration / 1000), + animationTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName + .easeInEaseOut)) result(nil) + } else { + mapView.setCamera(camera, animated: true) } - mapView.setCamera(camera, animated: true) } result(nil) - case "symbols#addAll": - guard let symbolAnnotationController = symbolAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - - if let options = arguments["options"] as? [[String: Any]] { - var symbols: [MGLSymbolStyleAnnotation] = []; - for o in options { - if let symbol = getSymbolForOptions(options: o) { - symbols.append(symbol) - } - } - if !symbols.isEmpty { - symbolAnnotationController.addStyleAnnotations(symbols) - symbolAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.symbol") - } - result(symbols.map { $0.identifier }) - } else { - result(nil) - } - case "symbol#update": - guard let symbolAnnotationController = symbolAnnotationController else { return } + case "symbolLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let symbolId = arguments["symbol"] as? String else { return } - - for symbol in symbolAnnotationController.styleAnnotations(){ - if symbol.identifier == symbolId { - Convert.interpretSymbolOptions(options: arguments["options"], delegate: symbol as! MGLSymbolStyleAnnotation) - // Load (updated) icon image from asset if an icon name is supplied. - if let options = arguments["options"] as? [String: Any], - let iconImage = options["iconImage"] as? String { - addIconImageToMap(iconImageName: iconImage) - } - symbolAnnotationController.updateStyleAnnotation(symbol) - break; - } - } - result(nil) - case "symbols#removeAll": - guard let symbolAnnotationController = symbolAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let symbolIds = arguments["ids"] as? [String] else { return } - - removeAllForController(controller:symbolAnnotationController, ids:symbolIds) - result(nil) + 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 - case "symbol#getGeometry": - guard let symbolAnnotationController = symbolAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let symbolId = arguments["symbol"] as? String else { return } - - var reply: [String:Double]? = nil - for symbol in symbolAnnotationController.styleAnnotations(){ - if symbol.identifier == symbolId { - if let geometry = symbol.geoJSONDictionary["geometry"] as? [String: Any], - let coordinates = geometry["coordinates"] as? [Double] { - reply = ["latitude": coordinates[1], "longitude": coordinates[0]] - } - break; - } + removeLayer(layerId: layerId) + let addResult = addSymbolLayer( + 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) } - result(reply) - case "symbolManager#iconAllowOverlap": - guard let symbolAnnotationController = symbolAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let iconAllowOverlap = arguments["iconAllowOverlap"] as? Bool else { return } - symbolAnnotationController.iconAllowsOverlap = iconAllowOverlap - result(nil) - case "symbolManager#iconIgnorePlacement": - guard let symbolAnnotationController = symbolAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let iconIgnorePlacement = arguments["iconIgnorePlacement"] as? Bool else { return } - - symbolAnnotationController.iconIgnoresPlacement = iconIgnorePlacement - result(nil) - case "symbolManager#textAllowOverlap": - guard let symbolAnnotationController = symbolAnnotationController else { return } + case "lineLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let textAllowOverlap = arguments["textAllowOverlap"] as? Bool 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 - symbolAnnotationController.textAllowsOverlap = textAllowOverlap - result(nil) - case "symbolManager#textIgnorePlacement": - result(FlutterMethodNotImplemented) - case "circle#add": - guard let circleAnnotationController = circleAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - if let options = arguments["options"] as? [String: Any], - let geometry = options["geometry"] as? [Double] { - // Convert geometry to coordinate and create circle. - let coordinate = CLLocationCoordinate2DMake(geometry[0], geometry[1]) - let circle = MGLCircleStyleAnnotation(center: coordinate) - Convert.interpretCircleOptions(options: arguments["options"], delegate: circle) - circleAnnotationController.addStyleAnnotation(circle) - circleAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.circle") - result(circle.identifier) - } else { - result(nil) + removeLayer(layerId: layerId) + let addResult = addLineLayer( + 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 "circle#addAll": - guard let circleAnnotationController = circleAnnotationController else { return } + case "fillLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - var identifier: String? = nil - if let allOptions = arguments["options"] as? [[String: Any]]{ - var circles: [MGLCircleStyleAnnotation] = []; - - for options in allOptions { - if let geometry = options["geometry"] as? [Double] { - guard geometry.count > 0 else { break } - - let coordinate = CLLocationCoordinate2DMake(geometry[0], geometry[1]) - let circle = MGLCircleStyleAnnotation(center: coordinate) - Convert.interpretCircleOptions(options: options, delegate: circle) - circles.append(circle) - } - } - if !circles.isEmpty { - circleAnnotationController.addStyleAnnotations(circles) - } - result(circles.map { $0.identifier }) - } - else { - result(nil) - } - - case "circle#update": - guard let circleAnnotationController = circleAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let circleId = arguments["circle"] as? String else { return } - - for circle in circleAnnotationController.styleAnnotations() { - if circle.identifier == circleId { - Convert.interpretCircleOptions(options: arguments["options"], delegate: circle as! MGLCircleStyleAnnotation) - circleAnnotationController.updateStyleAnnotation(circle) - break; - } - } - result(nil) - case "circle#remove": - guard let circleAnnotationController = circleAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let circleId = arguments["circle"] as? String else { return } - - for circle in circleAnnotationController.styleAnnotations() { - if circle.identifier == circleId { - circleAnnotationController.removeStyleAnnotation(circle) - break; - } + 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 = addFillLayer( + 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) } - result(nil) - case "circle#removeAll": - guard let circleAnnotationController = circleAnnotationController else { return } + case "fillExtrusionLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let ids = arguments["ids"] as? [String] else { return } - - removeAllForController(controller:circleAnnotationController, ids:ids) - result(nil) + 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 - - case "line#add": - guard let lineAnnotationController = lineAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - if let options = arguments["options"] as? [String: Any], - let geometry = options["geometry"] as? [[Double]] { - // Convert geometry to coordinate and create a line. - var lineCoordinates: [CLLocationCoordinate2D] = [] - for coordinate in geometry { - lineCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) - } - let line = MGLLineStyleAnnotation(coordinates: lineCoordinates, count: UInt(lineCoordinates.count)) - Convert.interpretLineOptions(options: arguments["options"], delegate: line) - lineAnnotationController.addStyleAnnotation(line) - lineAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.line") - result(line.identifier) - } else { - result(nil) - } - - case "line#addAll": - guard let lineAnnotationController = lineAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - var identifier: String? = nil - if let allOptions = arguments["options"] as? [[String: Any]]{ - var lines: [MGLLineStyleAnnotation] = []; - - for options in allOptions { - if let geometry = options["geometry"] as? [[Double]] { - guard geometry.count > 0 else { break } - // Convert geometry to coordinate and create a line. - var lineCoordinates: [CLLocationCoordinate2D] = [] - for coordinate in geometry { - lineCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) - } - let line = MGLLineStyleAnnotation(coordinates: lineCoordinates, count: UInt(lineCoordinates.count)) - Convert.interpretLineOptions(options: options, delegate: line) - lines.append(line) - } - } - if !lines.isEmpty { - lineAnnotationController.addStyleAnnotations(lines) - } - result(lines.map { $0.identifier }) - } - else { - result(nil) + 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 "line#update": - guard let lineAnnotationController = lineAnnotationController else { return } + case "circleLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let lineId = arguments["line"] as? String else { return } - - for line in lineAnnotationController.styleAnnotations() { - if line.identifier == lineId { - Convert.interpretLineOptions(options: arguments["options"], delegate: line as! MGLLineStyleAnnotation) - lineAnnotationController.updateStyleAnnotation(line) - break; - } - } - result(nil) - case "line#remove": - guard let lineAnnotationController = lineAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let lineId = arguments["line"] as? String else { return } - - for line in lineAnnotationController.styleAnnotations() { - if line.identifier == lineId { - lineAnnotationController.removeStyleAnnotation(line) - break; - } + 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 = addCircleLayer( + 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) } - result(nil) - case "line#removeAll": - guard let lineAnnotationController = lineAnnotationController else { return } + case "hillshadeLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let ids = arguments["ids"] as? [String] 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 - removeAllForController(controller:lineAnnotationController, ids:ids) + removeLayer(layerId: layerId) + addHillshadeLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + properties: properties + ) result(nil) - case "line#getGeometry": - guard let lineAnnotationController = lineAnnotationController else { return } + case "heatmapLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let lineId = arguments["line"] as? String else { return } - - var reply: [Any]? = nil - for line in lineAnnotationController.styleAnnotations() { - if line.identifier == lineId { - if let geometry = line.geoJSONDictionary["geometry"] as? [String: Any], - let coordinates = geometry["coordinates"] as? [[Double]] { - reply = coordinates.map { [ "latitude": $0[1], "longitude": $0[0] ] } - } - break; - } - } - result(reply) - case "fill#add": - guard let fillAnnotationController = fillAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - var identifier: String? = nil - if let options = arguments["options"] as? [String: Any], - let geometry = options["geometry"] as? [[[Double]]] { - guard geometry.count > 0 else { break } - // Convert geometry to coordinate and interior polygonc. - var fillCoordinates: [CLLocationCoordinate2D] = [] - for coordinate in geometry[0] { - fillCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) - } - let polygons = Convert.toPolygons(geometry: geometry.tail) - let fill = MGLPolygonStyleAnnotation(coordinates: fillCoordinates, count: UInt(fillCoordinates.count), interiorPolygons: polygons) - Convert.interpretFillOptions(options: arguments["options"], delegate: fill) - fillAnnotationController.addStyleAnnotation(fill) - fillAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.fill") - identifier = fill.identifier - } - - result(identifier) - - case "fill#addAll": - guard let fillAnnotationController = fillAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - // Parse geometry - var identifier: String? = nil - if let allOptions = arguments["options"] as? [[String: Any]]{ - var fills: [MGLPolygonStyleAnnotation] = []; - - for options in allOptions{ - if let geometry = options["geometry"] as? [[[Double]]] { - guard geometry.count > 0 else { break } - // Convert geometry to coordinate and interior polygonc. - var fillCoordinates: [CLLocationCoordinate2D] = [] - for coordinate in geometry[0] { - fillCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) - } - let polygons = Convert.toPolygons(geometry: geometry.tail) - let fill = MGLPolygonStyleAnnotation(coordinates: fillCoordinates, count: UInt(fillCoordinates.count), interiorPolygons: polygons) - Convert.interpretFillOptions(options: options, delegate: fill) - fills.append(fill) - } - } - if !fills.isEmpty { - fillAnnotationController.addStyleAnnotations(fills) - } - result(fills.map { $0.identifier }) - } - else { - result(nil) - } + 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 - case "fill#update": - guard let fillAnnotationController = fillAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let fillId = arguments["fill"] as? String else { return } - - for fill in fillAnnotationController.styleAnnotations() { - if fill.identifier == fillId { - Convert.interpretFillOptions(options: arguments["options"], delegate: fill as! MGLPolygonStyleAnnotation) - fillAnnotationController.updateStyleAnnotation(fill) - break; - } - } - - result(nil) - case "fill#remove": - guard let fillAnnotationController = fillAnnotationController else { return } - guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let fillId = arguments["fill"] as? String else { return } - - for fill in fillAnnotationController.styleAnnotations() { - if fill.identifier == fillId { - fillAnnotationController.removeStyleAnnotation(fill) - break; - } - } + removeLayer(layerId: layerId) + addHeatmapLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + properties: properties + ) result(nil) - case "fill#removeAll": - guard let fillAnnotationController = fillAnnotationController else { return } + case "rasterLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let ids = arguments["ids"] as? [String] else { return } - - removeAllForController(controller:fillAnnotationController, ids:ids) + 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 + addRasterLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + properties: properties + ) result(nil) case "style#addImage": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let name = arguments["name"] as? String else { return } - //guard let length = arguments["length"] as? NSNumber else { return } + // guard let length = arguments["length"] as? NSNumber else { return } guard let bytes = arguments["bytes"] as? FlutterStandardTypedData else { return } guard let sdf = arguments["sdf"] as? Bool else { return } - guard let data = bytes.data as? Data else{ return } - guard let image = UIImage(data: data) else { return } - if (sdf) { - self.mapView.style?.setImage(image.withRenderingMode(.alwaysTemplate), forName: name) + guard let data = bytes.data as? Data else { return } + guard let image = UIImage(data: data, scale: UIScreen.main.scale) else { return } + if sdf { + mapView.style?.setImage(image.withRenderingMode(.alwaysTemplate), forName: name) } else { - self.mapView.style?.setImage(image, forName: name) + mapView.style?.setImage(image, forName: name) } result(nil) - case "style#addImageSource": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let imageSourceId = arguments["imageSourceId"] as? String else { return } guard let bytes = arguments["bytes"] as? FlutterStandardTypedData else { return } guard let data = bytes.data as? Data else { return } guard let image = UIImage(data: data) else { return } - - guard let coordinates = arguments["coordinates"] as? [[Double]] else { return }; + + guard let coordinates = arguments["coordinates"] as? [[Double]] else { return } 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]) + 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] + ) ) - - //Check for duplicateSource error - if (self.mapView.style?.source(withIdentifier: imageSourceId) != nil) { - result(FlutterError(code: "duplicateSource", message: "Source with imageSourceId \(imageSourceId) already exists", details: "Can't add duplicate source with imageSourceId: \(imageSourceId)" )) + + // Check for duplicateSource error + if mapView.style?.source(withIdentifier: imageSourceId) != nil { + result(FlutterError( + code: "duplicateSource", + message: "Source with imageSourceId \(imageSourceId) already exists", + details: "Can't add duplicate source with imageSourceId: \(imageSourceId)" + )) return } - - let source = MGLImageSource(identifier: imageSourceId, coordinateQuad: quad, image: image) - self.mapView.style?.addSource(source) - + + let source = MGLImageSource( + identifier: imageSourceId, + coordinateQuad: quad, + image: image + ) + mapView.style?.addSource(source) + result(nil) - case "style#removeImageSource": + case "style#updateImageSource": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let imageSourceId = arguments["imageSourceId"] as? String else { return } - guard let source = self.mapView.style?.source(withIdentifier: imageSourceId) else { return } - self.mapView.style?.removeSource(source) + 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 } + guard let source = mapView.style?.source(withIdentifier: sourceId) else { + result(nil) + return + } + mapView.style?.removeSource(source) result(nil) case "style#addLayer": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let imageLayerId = arguments["imageLayerId"] as? String else { return } guard let imageSourceId = arguments["imageSourceId"] as? String else { return } - - //Check for duplicateLayer error - if (self.mapView.style?.layer(withIdentifier: imageLayerId)) != nil { - result(FlutterError(code: "duplicateLayer", message: "Layer already exists", details: "Can't add duplicate layer with imageLayerId: \(imageLayerId)" )) + let minzoom = arguments["minzoom"] as? Double + let maxzoom = arguments["maxzoom"] as? Double + + // Check for duplicateLayer error + if (mapView.style?.layer(withIdentifier: imageLayerId)) != nil { + result(FlutterError( + code: "duplicateLayer", + message: "Layer already exists", + details: "Can't add duplicate layer with imageLayerId: \(imageLayerId)" + )) return } - //Check for noSuchSource error - guard let source = self.mapView.style?.source(withIdentifier: imageSourceId) else { - result(FlutterError(code: "noSuchSource", message: "No source found with imageSourceId \(imageSourceId)", details: "Can't add add layer for imageSourceId \(imageLayerId), as the source does not exist." )) + // Check for noSuchSource error + guard let source = mapView.style?.source(withIdentifier: imageSourceId) else { + result(FlutterError( + code: "noSuchSource", + message: "No source found with imageSourceId \(imageSourceId)", + details: "Can't add add layer for imageSourceId \(imageLayerId), as the source does not exist." + )) return } - + let layer = MGLRasterStyleLayer(identifier: imageLayerId, source: source) - self.mapView.style?.addLayer(layer) + + if let minzoom = minzoom { + layer.minimumZoomLevel = Float(minzoom) + } + + if let maxzoom = maxzoom { + layer.maximumZoomLevel = Float(maxzoom) + } + + mapView.style?.addLayer(layer) result(nil) case "style#addLayerBelow": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let imageLayerId = arguments["imageLayerId"] as? String else { return } guard let imageSourceId = arguments["imageSourceId"] as? String else { return } guard let belowLayerId = arguments["belowLayerId"] as? String else { return } - - //Check for duplicateLayer error - if (self.mapView.style?.layer(withIdentifier: imageLayerId)) != nil { - result(FlutterError(code: "duplicateLayer", message: "Layer already exists", details: "Can't add duplicate layer with imageLayerId: \(imageLayerId)" )) + let minzoom = arguments["minzoom"] as? Double + let maxzoom = arguments["maxzoom"] as? Double + + // Check for duplicateLayer error + if (mapView.style?.layer(withIdentifier: imageLayerId)) != nil { + result(FlutterError( + code: "duplicateLayer", + message: "Layer already exists", + details: "Can't add duplicate layer with imageLayerId: \(imageLayerId)" + )) return } - //Check for noSuchSource error - guard let source = self.mapView.style?.source(withIdentifier: imageSourceId) else { - result(FlutterError(code: "noSuchSource", message: "No source found with imageSourceId \(imageSourceId)", details: "Can't add add layer for imageSourceId \(imageLayerId), as the source does not exist." )) + // Check for noSuchSource error + guard let source = mapView.style?.source(withIdentifier: imageSourceId) else { + result(FlutterError( + code: "noSuchSource", + message: "No source found with imageSourceId \(imageSourceId)", + details: "Can't add add layer for imageSourceId \(imageLayerId), as the source does not exist." + )) return } - //Check for noSuchLayer error - guard let belowLayer = self.mapView.style?.layer(withIdentifier: belowLayerId) else { - result(FlutterError(code: "noSuchLayer", message: "No layer found with layerId \(belowLayerId)", details: "Can't insert layer below layer with id \(belowLayerId), as no such layer exists." )) + // Check for noSuchLayer error + guard let belowLayer = mapView.style?.layer(withIdentifier: belowLayerId) else { + result(FlutterError( + code: "noSuchLayer", + message: "No layer found with layerId \(belowLayerId)", + details: "Can't insert layer below layer with id \(belowLayerId), as no such layer exists." + )) return } + let layer = MGLRasterStyleLayer(identifier: imageLayerId, source: source) - self.mapView.style?.insertLayer(layer, below: belowLayer) + + if let minzoom = minzoom { + layer.minimumZoomLevel = Float(minzoom) + } + + if let maxzoom = maxzoom { + layer.maximumZoomLevel = Float(maxzoom) + } + + mapView.style?.insertLayer(layer, below: belowLayer) result(nil) + case "style#removeLayer": guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let imageLayerId = arguments["imageLayerId"] as? String else { return } - guard let layer = self.mapView.style?.layer(withIdentifier: imageLayerId) else { return } - self.mapView.style?.removeLayer(layer) + guard let layerId = arguments["layerId"] as? String else { return } + removeLayer(layerId: layerId) + result(nil) + + case "style#setFilter": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let layerId = arguments["layerId"] as? String else { return } + guard let filter = arguments["filter"] as? String else { return } + guard let layer = mapView.style?.layer(withIdentifier: layerId) else { + result(nil) + return + } + switch setFilter(layer, filter) { + case .success: result(nil) + 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 } + guard let geojson = arguments["geojson"] as? String else { return } + addSourceGeojson(sourceId: sourceId, geojson: geojson) + result(nil) + + case "style#addSource": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + guard let properties = arguments["properties"] as? [String: Any] else { return } + addSource(sourceId: sourceId, properties: properties) + result(nil) + + case "source#setGeoJson": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + guard let geojson = arguments["geojson"] as? String else { return } + setSource(sourceId: sourceId, geojson: geojson) + result(nil) + + case "source#setFeature": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + 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 getSymbolForOptions(options: [String: Any]) -> MGLSymbolStyleAnnotation? { - // Parse geometry - if let geometry = options["geometry"] as? [Double] { - // Convert geometry to coordinate and create symbol. - let coordinate = CLLocationCoordinate2DMake(geometry[0], geometry[1]) - let symbol = MGLSymbolStyleAnnotation(coordinate: coordinate) - Convert.interpretSymbolOptions(options: options, delegate: symbol) - // Load icon image from asset if an icon name is supplied. - if let iconImage = options["iconImage"] as? String { - addIconImageToMap(iconImageName: iconImage) - } - return symbol + + 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. + if let range = name.range(of: "/", options: [.backwards]) { + let directory = String(name[.. MGLMapCamera? { return trackCameraPosition ? mapView.camera : nil - } - + + /* + * Scan layers from top to bottom and return the first matching feature + */ + private func firstFeatureOnLayers(at: CGPoint) -> MGLFeature? { + guard let style = mapView.style else { return nil } + + // get layers in order (interactiveFeatureLayerIds is unordered) + let clickableLayers = style.layers.filter { layer in + interactiveFeatureLayerIds.contains(layer.identifier) + } + + for layer in clickableLayers.reversed() { + let features = mapView.visibleFeatures( + at: at, + styleLayerIdentifiers: [layer.identifier] + ) + if let feature = features.first { + return feature + } + } + return nil + } + /* - * UITapGestureRecognizer - * On tap invoke the map#onMapClick callback. - */ - @objc @IBAction func handleMapTap(sender: UITapGestureRecognizer) { + * UITapGestureRecognizer + * On tap invoke the map#onMapClick callback. + */ + @IBAction func handleMapTap(sender: UITapGestureRecognizer) { // Get the CGPoint where the user tapped. let point = sender.location(in: mapView) let coordinate = mapView.convert(point, toCoordinateFrom: mapView) - channel?.invokeMethod("map#onMapClick", arguments: [ - "x": point.x, - "y": point.y, - "lng": coordinate.longitude, - "lat": coordinate.latitude, - ]) - } - - /* - * UILongPressGestureRecognizer - * After a long press invoke the map#onMapLongClick callback. - */ - @objc @IBAction func handleMapLongPress(sender: UILongPressGestureRecognizer) { - //Fire when the long press starts - if (sender.state == .began) { - // Get the CGPoint where the user tapped. - let point = sender.location(in: mapView) - let coordinate = mapView.convert(point, toCoordinateFrom: mapView) - channel?.invokeMethod("map#onMapLongClick", arguments: [ - "x": point.x, - "y": point.y, - "lng": coordinate.longitude, - "lat": coordinate.latitude, - ]) + + if let feature = firstFeatureOnLayers(at: point), let id = feature.identifier { + channel?.invokeMethod("feature#onTap", arguments: [ + "id": id, + "x": point.x, + "y": point.y, + "lng": coordinate.longitude, + "lat": coordinate.latitude, + ]) + } else { + channel?.invokeMethod("map#onMapClick", arguments: [ + "x": point.x, + "y": point.y, + "lng": coordinate.longitude, + "lat": coordinate.latitude, + ]) } - } - - - - /* - * MGLAnnotationControllerDelegate - */ - func annotationController(_ annotationController: MGLAnnotationController, didSelect styleAnnotation: MGLStyleAnnotation) { - DispatchQueue.main.async { - // Remove tint color overlay from selected annotation by - // deselecting. This is not handled correctly if requested - // synchronously from the callback. - annotationController.deselectStyleAnnotation(styleAnnotation) + + fileprivate func invokeFeatureDrag( + _ point: CGPoint, + _ coordinate: CLLocationCoordinate2D, + _ eventType: String + ) { + if let feature = dragFeature, + let id = feature.identifier, + let previous = previousDragCoordinate, + let origin = originDragCoordinate + { + channel?.invokeMethod("feature#onDrag", arguments: [ + "id": id, + "x": point.x, + "y": point.y, + "originLng": origin.longitude, + "originLat": origin.latitude, + "currentLng": coordinate.longitude, + "currentLat": coordinate.latitude, + "eventType": eventType, + "deltaLng": coordinate.longitude - previous.longitude, + "deltaLat": coordinate.latitude - previous.latitude, + ]) } + } - guard let channel = channel else { - return + @IBAction func handleMapPan(sender: UIPanGestureRecognizer) { + let began = sender.state == UIGestureRecognizer.State.began + let end = sender.state == UIGestureRecognizer.State.ended + let point = sender.location(in: mapView) + let coordinate = mapView.convert(point, toCoordinateFrom: mapView) + + if dragFeature == nil, began, sender.numberOfTouches == 1, + let feature = firstFeatureOnLayers(at: point), + let draggable = feature.attribute(forKey: "draggable") as? Bool, + draggable + { + sender.state = UIGestureRecognizer.State.began + dragFeature = feature + originDragCoordinate = coordinate + previousDragCoordinate = coordinate + mapView.allowsScrolling = false + let eventType = "start" + invokeFeatureDrag(point, coordinate, eventType) + for gestureRecognizer in mapView.gestureRecognizers! { + if let _ = gestureRecognizer as? UIPanGestureRecognizer { + gestureRecognizer.addTarget(self, action: #selector(handleMapPan)) + break + } + } } - - if let symbol = styleAnnotation as? MGLSymbolStyleAnnotation { - channel.invokeMethod("symbol#onTap", arguments: ["symbol" : "\(symbol.identifier)"]) - } else if let circle = styleAnnotation as? MGLCircleStyleAnnotation { - channel.invokeMethod("circle#onTap", arguments: ["circle" : "\(circle.identifier)"]) - } else if let line = styleAnnotation as? MGLLineStyleAnnotation { - channel.invokeMethod("line#onTap", arguments: ["line" : "\(line.identifier)"]) - } else if let fill = styleAnnotation as? MGLPolygonStyleAnnotation { - channel.invokeMethod("fill#onTap", arguments: ["fill" : "\(fill.identifier)"]) + if end, dragFeature != nil { + mapView.allowsScrolling = true + let eventType = "end" + invokeFeatureDrag(point, coordinate, eventType) + dragFeature = nil + originDragCoordinate = nil + previousDragCoordinate = nil + } + + if !began, !end, dragFeature != nil { + let eventType = "drag" + invokeFeatureDrag(point, coordinate, eventType) + previousDragCoordinate = coordinate } } - - // This is required in order to hide the default Maps SDK pin - func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - if annotation is MGLUserLocation { - return nil + + /* + * UILongPressGestureRecognizer + * After a long press invoke the map#onMapLongClick callback. + */ + @IBAction func handleMapLongPress(sender: UILongPressGestureRecognizer) { + // Fire when the long press starts + if sender.state == .began { + // Get the CGPoint where the user tapped. + let point = sender.location(in: mapView) + let coordinate = mapView.convert(point, toCoordinateFrom: mapView) + channel?.invokeMethod("map#onMapLongClick", arguments: [ + "x": point.x, + "y": point.y, + "lng": coordinate.longitude, + "lat": coordinate.latitude, + ]) } - return MGLAnnotationView(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) } /* @@ -825,8 +1093,16 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma * Called if the application supplies an onAttributionClick handler. */ func setupAttribution(_ mapView: MGLMapView) { - mapView.attributionButton.removeTarget(mapView, action: #selector(mapView.showAttribution), for: .touchUpInside) - mapView.attributionButton.addTarget(self, action: #selector(showAttribution), for: UIControl.Event.touchUpInside) + mapView.attributionButton.removeTarget( + mapView, + action: #selector(mapView.showAttribution), + for: .touchUpInside + ) + mapView.attributionButton.addTarget( + self, + action: #selector(showAttribution), + for: UIControl.Event.touchUpInside + ) } /* @@ -840,214 +1116,618 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma /* * MGLMapViewDelegate */ - func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) { + func mapView(_ mapView: MGLMapView, didFinishLoading _: MGLStyle) { isMapReady = true updateMyLocationEnabled() - + if let initialTilt = initialTilt { let camera = mapView.camera camera.pitch = initialTilt mapView.setCamera(camera, animated: false) } - for annotationType in annotationOrder { - switch annotationType { - case "AnnotationType.fill": - fillAnnotationController = MGLPolygonAnnotationController(mapView: self.mapView) - fillAnnotationController!.annotationsInteractionEnabled = true - fillAnnotationController?.delegate = self - case "AnnotationType.line": - lineAnnotationController = MGLLineAnnotationController(mapView: self.mapView) - lineAnnotationController!.annotationsInteractionEnabled = true - lineAnnotationController?.delegate = self - case "AnnotationType.circle": - circleAnnotationController = MGLCircleAnnotationController(mapView: self.mapView) - circleAnnotationController!.annotationsInteractionEnabled = true - circleAnnotationController?.delegate = self - case "AnnotationType.symbol": - symbolAnnotationController = MGLSymbolAnnotationController(mapView: self.mapView) - symbolAnnotationController!.annotationsInteractionEnabled = true - symbolAnnotationController?.delegate = self - default: - print("Unknown annotation type: \(annotationType), must be either 'fill', 'line', 'circle' or 'symbol'") - } - } + addedShapesByLayer.removeAll() + interactiveFeatureLayerIds.removeAll() mapReadyResult?(nil) - if let channel = channel { - channel.invokeMethod("map#onStyleLoaded", arguments: nil) + + // On first launch we only call map#onStyleLoaded if map#waitForMap has already been called + if !isFirstStyleLoad || mapReadyResult != nil { + isFirstStyleLoad = false + + if let channel = channel { + channel.invokeMethod("map#onStyleLoaded", arguments: nil) + } } } - - func mapView(_ mapView: MGLMapView, shouldChangeFrom oldCamera: MGLMapCamera, to newCamera: MGLMapCamera) -> Bool { + + // handle missing images + func mapView(_: MGLMapView, didFailToLoadImage name: String) -> UIImage? { + return loadIconImage(name: name) + } + + func mapView(_ mapView: MGLMapView, shouldChangeFrom _: MGLMapCamera, + to newCamera: MGLMapCamera) -> Bool + { guard let bbox = cameraTargetBounds else { return true } - + // Get the current camera to restore it after. let currentCamera = mapView.camera - + // From the new camera obtain the center to test if it’s inside the boundaries. let newCameraCenter = newCamera.centerCoordinate - + // Set the map’s visible bounds to newCamera. mapView.camera = newCamera let newVisibleCoordinates = mapView.visibleCoordinateBounds - + // Revert the camera. mapView.camera = currentCamera - + // Test if the newCameraCenter and newVisibleCoordinates are inside bbox. let inside = MGLCoordinateInCoordinateBounds(newCameraCenter, bbox) - let intersects = MGLCoordinateInCoordinateBounds(newVisibleCoordinates.ne, bbox) && MGLCoordinateInCoordinateBounds(newVisibleCoordinates.sw, bbox) - + let intersects = MGLCoordinateInCoordinateBounds(newVisibleCoordinates.ne, bbox) && + MGLCoordinateInCoordinateBounds(newVisibleCoordinates.sw, bbox) + return inside && intersects } - - func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? { - // Only for Symbols images should loaded. - guard let symbol = annotation as? Symbol, - let iconImageFullPath = symbol.iconImage else { - return nil + + func mapView(_: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + if let channel = channel, let userLocation = userLocation, + let location = userLocation.location + { + channel.invokeMethod("map#onUserLocationUpdated", arguments: [ + "userLocation": location.toDict(), + "heading": userLocation.heading?.toDict(), + ]) } - // Reuse existing annotations for better performance. - var annotationImage = mapView.dequeueReusableAnnotationImage(withIdentifier: iconImageFullPath) - if annotationImage == nil { - // Initialize the annotation image (from predefined assets symbol folder). - if let range = iconImageFullPath.range(of: "/", options: [.backwards]) { - let directory = String(iconImageFullPath[.. Result { + if let style = mapView.style { + if let source = style.source(withIdentifier: sourceId) { + let layer = MGLSymbolStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addSymbolProperties( + symbolLayer: 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 annotationImage + return .success(()) } - - // On tap invoke the symbol#onTap callback. - func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) { - - if let symbol = annotation as? Symbol { - channel?.invokeMethod("symbol#onTap", arguments: ["symbol" : "\(symbol.id)"]) + + func addLineLayer( + 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 = MGLLineStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addLineProperties(lineLayer: 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(()) } - - // Allow callout view to appear when an annotation is tapped. - func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool { - return true + + func addFillLayer( + 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 = MGLFillStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addFillProperties(fillLayer: 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 mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { - if let channel = channel, let userLocation = userLocation, let location = userLocation.location { - channel.invokeMethod("map#onUserLocationUpdated", arguments: [ - "userLocation": location.toDict(), - "heading": userLocation.heading?.toDict() - ]); - } - } - - func mapView(_ mapView: MGLMapView, didChange mode: MGLUserTrackingMode, animated: Bool) { - if let channel = channel { - channel.invokeMethod("map#onCameraTrackingChanged", arguments: ["mode": mode.rawValue]) - if mode == .none { - channel.invokeMethod("map#onCameraTrackingDismissed", arguments: []) + 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, + 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 = MGLCircleStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addCircleProperties( + circleLayer: 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 setFilter(_ layer: MGLStyleLayer, _ filter: String) -> Result { + do { + let filter = try JSONSerialization.jsonObject( + with: filter.data(using: .utf8)!, + options: .fragmentsAllowed + ) + if filter is NSNull { + return .success(()) + } + let predicate = NSPredicate(mglJSONObject: filter) + if let layer = layer as? MGLVectorStyleLayer { + layer.predicate = predicate + } else { + return .failure(MethodCallError.invalidLayerType( + details: "Layer '\(layer.identifier)' does not support filtering." + )) + } + return .success(()) + } catch { + return .failure(MethodCallError.invalidExpression) + } + } + + func addHillshadeLayer( + 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 = MGLHillshadeStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addHillshadeProperties( + hillshadeLayer: 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 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, + belowLayerId: String?, + minimumZoomLevel: Double?, + maximumZoomLevel: Double?, + properties: [String: String] + ) { + if let style = mapView.style { + if let source = style.source(withIdentifier: sourceId) { + let layer = MGLRasterStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addRasterProperties( + rasterLayer: 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 mapViewDidBecomeIdle(_ mapView: MGLMapView) { + + func addSource(sourceId: String, properties: [String: Any]) { + if let style = mapView.style, let type = properties["type"] as? String { + var source: MGLSource? + + switch type { + case "vector": + source = SourcePropertyConverter.buildVectorTileSource( + identifier: sourceId, + properties: properties + ) + case "raster": + source = SourcePropertyConverter.buildRasterTileSource( + identifier: sourceId, + properties: properties + ) + case "raster-dem": + source = SourcePropertyConverter.buildRasterDemSource( + identifier: sourceId, + properties: properties + ) + case "image": + source = SourcePropertyConverter.buildImageSource( + identifier: sourceId, + properties: properties + ) + case "geojson": + source = SourcePropertyConverter.buildShapeSource( + identifier: sourceId, + properties: properties + ) + default: + // unsupported source type + source = nil + } + if let source = source { + style.addSource(source) + } + } + } + + func mapViewDidBecomeIdle(_: MGLMapView) { if let channel = channel { - channel.invokeMethod("map#onIdle", arguments: []); + channel.invokeMethod("map#onIdle", arguments: []) } } - - func mapView(_ mapView: MGLMapView, regionWillChangeAnimated animated: Bool) { + + func mapView(_: MGLMapView, regionWillChangeAnimated _: Bool) { if let channel = channel { - channel.invokeMethod("camera#onMoveStarted", arguments: []); + channel.invokeMethod("camera#onMoveStarted", arguments: []) } } - + func mapViewRegionIsChanging(_ mapView: MGLMapView) { - if !trackCameraPosition { return }; + if !trackCameraPosition { return } if let channel = channel { channel.invokeMethod("camera#onMove", arguments: [ - "position": getCamera()?.toDict(mapView: mapView) - ]); + "position": getCamera()?.toDict(mapView: mapView), + ]) } } - - func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) { + + func mapView(_ mapView: MGLMapView, regionDidChangeAnimated _: Bool) { let arguments = trackCameraPosition ? [ "position": getCamera()?.toDict(mapView: mapView) - ] : [:]; + ] : [:] if let channel = channel { - channel.invokeMethod("camera#onIdle", arguments: arguments); + channel.invokeMethod("camera#onIdle", arguments: arguments) } } - + + func addSourceGeojson(sourceId: String, geojson: String) { + do { + let parsed = try MGLShape( + data: geojson.data(using: .utf8)!, + encoding: String.Encoding.utf8.rawValue + ) + let source = MGLShapeSource(identifier: sourceId, shape: parsed, options: [:]) + addedShapesByLayer[sourceId] = parsed + mapView.style?.addSource(source) + print(source) + } catch {} + } + + func setSource(sourceId: String, geojson: String) { + do { + let parsed = try MGLShape( + data: geojson.data(using: .utf8)!, + encoding: String.Encoding.utf8.rawValue + ) + if let source = mapView.style?.source(withIdentifier: sourceId) as? MGLShapeSource { + addedShapesByLayer[sourceId] = parsed + source.shape = parsed + } + } catch {} + } + + func setFeature(sourceId: String, geojsonFeature: String) { + do { + let newShape = try MGLShape( + data: geojsonFeature.data(using: .utf8)!, + encoding: String.Encoding.utf8.rawValue + ) + if let source = mapView.style?.source(withIdentifier: sourceId) as? MGLShapeSource, + let shape = addedShapesByLayer[sourceId] as? MGLShapeCollectionFeature, + let feature = newShape as? MGLShape & MGLFeature + { + if let index = shape.shapes + .firstIndex(where: { + if let id = $0.identifier as? String, + let featureId = feature.identifier as? String + { return id == featureId } + + if let id = $0.identifier as? NSNumber, + let featureId = feature.identifier as? NSNumber + { return id == featureId } + return false + }) + { + var shapes = shape.shapes + shapes[index] = feature + + source.shape = MGLShapeCollectionFeature(shapes: shapes) + } + + addedShapesByLayer[sourceId] = source.shape + } + + } catch {} + } + /* * MapboxMapOptionsSink */ func setCameraTargetBounds(bounds: MGLCoordinateBounds?) { cameraTargetBounds = bounds } + func setCompassEnabled(compassEnabled: Bool) { mapView.compassView.isHidden = compassEnabled mapView.compassView.isHidden = !compassEnabled } + func setMinMaxZoomPreference(min: Double, max: Double) { mapView.minimumZoomLevel = min mapView.maximumZoomLevel = max } + func setStyleString(styleString: String) { // Check if json, url, absolute path or asset path: if styleString.isEmpty { NSLog("setStyleString - string empty") - } else if (styleString.hasPrefix("{") || styleString.hasPrefix("[")) { + } else if styleString.hasPrefix("{") || styleString.hasPrefix("[") { // Currently the iOS Mapbox SDK does not have a builder for json. NSLog("setStyleString - JSON style currently not supported") - } else if (styleString.hasPrefix("/")) { + } else if styleString.hasPrefix("/") { // Absolute path mapView.styleURL = URL(fileURLWithPath: styleString, isDirectory: false) - } else if ( - !styleString.hasPrefix("http://") && - !styleString.hasPrefix("https://") && - !styleString.hasPrefix("mapbox://")) { + } else if + !styleString.hasPrefix("http://"), + !styleString.hasPrefix("https://"), + !styleString.hasPrefix("mapbox://") + { // We are assuming that the style will be loaded from an asset here. let assetPath = registrar.lookupKey(forAsset: styleString) mapView.styleURL = URL(string: assetPath, relativeTo: Bundle.main.resourceURL) - - + } else { mapView.styleURL = URL(string: styleString) } } + func setRotateGesturesEnabled(rotateGesturesEnabled: Bool) { mapView.allowsRotating = rotateGesturesEnabled } + func setScrollGesturesEnabled(scrollGesturesEnabled: Bool) { mapView.allowsScrolling = scrollGesturesEnabled + scrollingEnabled = scrollGesturesEnabled } + func setTiltGesturesEnabled(tiltGesturesEnabled: Bool) { mapView.allowsTilting = tiltGesturesEnabled } + func setTrackCameraPosition(trackCameraPosition: Bool) { self.trackCameraPosition = trackCameraPosition } + func setZoomGesturesEnabled(zoomGesturesEnabled: Bool) { mapView.allowsZooming = zoomGesturesEnabled } + func setMyLocationEnabled(myLocationEnabled: Bool) { - if (self.myLocationEnabled == myLocationEnabled) { + if self.myLocationEnabled == myLocationEnabled { return } self.myLocationEnabled = myLocationEnabled updateMyLocationEnabled() } + func setMyLocationTrackingMode(myLocationTrackingMode: MGLUserTrackingMode) { mapView.userTrackingMode = myLocationTrackingMode } + func setMyLocationRenderMode(myLocationRenderMode: MyLocationRenderMode) { switch myLocationRenderMode { case .Normal: @@ -1058,16 +1738,24 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma NSLog("RenderMode.GPS currently not supported") } } + func setLogoViewMargins(x: Double, y: Double) { mapView.logoViewMargins = CGPoint(x: x, y: y) } + func setCompassViewPosition(position: MGLOrnamentPosition) { mapView.compassViewPosition = position } + func setCompassViewMargins(x: Double, y: Double) { mapView.compassViewMargins = CGPoint(x: x, y: y) } + func setAttributionButtonMargins(x: Double, y: Double) { mapView.attributionButtonMargins = CGPoint(x: x, y: y) } + + func setAttributionButtonPosition(position: MGLOrnamentPosition) { + mapView.attributionButtonPosition = position + } } diff --git a/ios/Classes/MapboxMapFactory.swift b/ios/Classes/MapboxMapFactory.swift index ba3bc4141..8f56d201e 100644 --- a/ios/Classes/MapboxMapFactory.swift +++ b/ios/Classes/MapboxMapFactory.swift @@ -1,19 +1,25 @@ import Flutter class MapboxMapFactory: NSObject, FlutterPlatformViewFactory { - var registrar: FlutterPluginRegistrar - + init(withRegistrar registrar: FlutterPluginRegistrar) { self.registrar = registrar super.init() } - + func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { return FlutterStandardMessageCodec.sharedInstance() } - - func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { - return MapboxMapController(withFrame: frame, viewIdentifier: viewId, arguments: args, registrar: registrar) + + func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, + arguments args: Any?) -> FlutterPlatformView + { + return MapboxMapController( + withFrame: frame, + viewIdentifier: viewId, + arguments: args, + registrar: registrar + ) } } diff --git a/ios/Classes/MapboxMapOptionsSink.swift b/ios/Classes/MapboxMapOptionsSink.swift index 2cd2cf746..c032baa15 100644 --- a/ios/Classes/MapboxMapOptionsSink.swift +++ b/ios/Classes/MapboxMapOptionsSink.swift @@ -18,4 +18,5 @@ protocol MapboxMapOptionsSink { func setCompassViewPosition(position: MGLOrnamentPosition) func setCompassViewMargins(x: Double, y: Double) func setAttributionButtonMargins(x: Double, y: Double) + func setAttributionButtonPosition(position: MGLOrnamentPosition) } diff --git a/ios/Classes/MethodCallError.swift b/ios/Classes/MethodCallError.swift new file mode 100644 index 000000000..8806bec33 --- /dev/null +++ b/ios/Classes/MethodCallError.swift @@ -0,0 +1,41 @@ +import Flutter + +enum MethodCallError: Error { + case invalidLayerType(details: String) + case invalidExpression + + var code: String { + switch self { + case .invalidLayerType: + return "invalidLayerType" + case .invalidExpression: + return "invalidExpression" + } + } + + var message: String { + switch self { + case .invalidLayerType: + return "Invalid layer type" + case .invalidExpression: + return "Invalid expression" + } + } + + var details: String { + switch self { + case let .invalidLayerType(details): + return details + case .invalidExpression: + return "Could not parse expression." + } + } + + var flutterError: FlutterError { + return FlutterError( + code: code, + message: message, + details: details + ) + } +} diff --git a/ios/Classes/OfflineChannelHandler.swift b/ios/Classes/OfflineChannelHandler.swift index 11b12b4ac..b5f474667 100644 --- a/ios/Classes/OfflineChannelHandler.swift +++ b/ios/Classes/OfflineChannelHandler.swift @@ -10,47 +10,59 @@ import Foundation class OfflineChannelHandler: NSObject, FlutterStreamHandler { private var sink: FlutterEventSink? - + init(messenger: FlutterBinaryMessenger, channelName: String) { super.init() let eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: messenger) eventChannel.setStreamHandler(self) } - + // MARK: FlutterStreamHandler protocol compliance - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + + func onListen(withArguments _: Any?, + eventSink events: @escaping FlutterEventSink) -> FlutterError? + { sink = events return nil } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { + + func onCancel(withArguments _: Any?) -> FlutterError? { sink = nil return nil } - + // MARK: Util methods + func onError(errorCode: String, errorMessage: String?, errorDetails: Any?) { sink?(FlutterError(code: errorCode, message: errorMessage, details: errorDetails)) } - + func onSuccess() { let body = ["status": "success"] - guard let jsonData = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted), + guard let jsonData = try? JSONSerialization.data( + withJSONObject: body, + options: .prettyPrinted + ), let jsonString = String(data: jsonData, encoding: .utf8) else { return } sink?(jsonString) - } - + func onStart() { let body = ["status": "start"] - guard let jsonData = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted), + guard let jsonData = try? JSONSerialization.data( + withJSONObject: body, + options: .prettyPrinted + ), let jsonString = String(data: jsonData, encoding: .utf8) else { return } sink?(jsonString) } - + func onProgress(progress: Double) { let body: [String: Any] = ["status": "progress", "progress": progress] - guard let jsonData = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted), + guard let jsonData = try? JSONSerialization.data( + withJSONObject: body, + options: .prettyPrinted + ), let jsonString = String(data: jsonData, encoding: .utf8) else { return } sink?(jsonString) } diff --git a/ios/Classes/OfflineManagerUtils.swift b/ios/Classes/OfflineManagerUtils.swift index 749baf0b3..a2d3bbe67 100644 --- a/ios/Classes/OfflineManagerUtils.swift +++ b/ios/Classes/OfflineManagerUtils.swift @@ -16,7 +16,7 @@ class OfflineManagerUtils { definition: OfflineRegionDefinition, metadata: [String: Any], result: @escaping FlutterResult, - registrar: FlutterPluginRegistrar, + registrar _: FlutterPluginRegistrar, channelHandler: OfflineChannelHandler ) { // Prepare downloader @@ -40,13 +40,13 @@ class OfflineManagerUtils { return } let regionsArgs = packs.compactMap { pack in - return OfflineRegion.fromOfflinePack(pack)?.toDictionary() + OfflineRegion.fromOfflinePack(pack)?.toDictionary() } guard let regionsArgsJsonData = try? JSONSerialization.data(withJSONObject: regionsArgs), - let regionsArgsJsonString = String(data: regionsArgsJsonData, encoding: .utf8) - else { - result(FlutterError(code: "RegionListError", message: nil, details: nil)) - return + let regionsArgsJsonString = String(data: regionsArgsJsonData, encoding: .utf8) + else { + result(FlutterError(code: "RegionListError", message: nil, details: nil)) + return } result(regionsArgsJsonString) } @@ -70,6 +70,10 @@ class OfflineManagerUtils { } }) if let packToRemoveUnwrapped = packToRemove { + // deletion is only safe if the download is suspended + packToRemoveUnwrapped.suspend() + OfflineManagerUtils.releaseDownloader(id: id) + offlineStorage.removePack(packToRemoveUnwrapped) { error in if let error = error { result(FlutterError( diff --git a/ios/Classes/OfflinePackDownloadManager.swift b/ios/Classes/OfflinePackDownloadManager.swift index e1802ff9a..783ba5815 100644 --- a/ios/Classes/OfflinePackDownloadManager.swift +++ b/ios/Classes/OfflinePackDownloadManager.swift @@ -11,6 +11,7 @@ import Mapbox class OfflinePackDownloader { // MARK: Properties + private let result: FlutterResult private let channelHandler: OfflineChannelHandler private let regionDefinition: OfflineRegionDefinition @@ -18,27 +19,34 @@ class OfflinePackDownloader { /// Currently managed pack private var pack: MGLOfflinePack? - + /// This variable is set to true when this downloader has finished downloading and called the result method. It is used to prevent /// the result method being called multiple times private var isCompleted = false - + // MARK: Initializers - init(result: @escaping FlutterResult, channelHandler: OfflineChannelHandler, regionDefintion: OfflineRegionDefinition, metadata: [String: Any]) { + + init( + result: @escaping FlutterResult, + channelHandler: OfflineChannelHandler, + regionDefintion: OfflineRegionDefinition, + metadata: [String: Any] + ) { self.result = result self.channelHandler = channelHandler - self.regionDefinition = regionDefintion + regionDefinition = regionDefintion self.metadata = metadata setupNotifications() } - + deinit { print("Removing offline pack notification observers") NotificationCenter.default.removeObserver(self) } - + // MARK: Public methods + func download() -> Int { let storage = MGLOfflineStorage.shared // While the Android SDK generates a region ID in createOfflineRegion, the iOS @@ -46,20 +54,24 @@ class OfflinePackDownloader { let id = UUID().hashValue let regionData = OfflineRegion(id: id, metadata: metadata, definition: regionDefinition) let tilePyramidRegion = regionDefinition.toMGLTilePyramidOfflineRegion() - storage.addPack(for: tilePyramidRegion, withContext: regionData.prepareContext()) { [weak self] (pack, error) in - if let pack = pack { - self?.onPackCreated(pack: pack) - } else { - self?.onPackCreationError(error: error) + storage + .addPack(for: tilePyramidRegion, + withContext: regionData.prepareContext()) { [weak self] pack, error in + if let pack = pack { + self?.onPackCreated(pack: pack) + } else { + self?.onPackCreationError(error: error) + } } - } return id } - + // MARK: Pack management + private func onPackCreated(pack: MGLOfflinePack) { if let region = OfflineRegion.fromOfflinePack(pack), - let regionData = try? JSONSerialization.data(withJSONObject: region.toDictionary()) { + let regionData = try? JSONSerialization.data(withJSONObject: region.toDictionary()) + { // Start downloading self.pack = pack pack.resume() @@ -70,7 +82,7 @@ class OfflinePackDownloader { onPackCreationError(error: OfflinePackError.InvalidPackData) } } - + private func onPackCreationError(error: Error?) { // Reset downloading state channelHandler.onError( @@ -84,12 +96,13 @@ class OfflinePackDownloader { details: nil )) } - + // MARK: Progress obseration + @objc private func onPackDownloadProgress(notification: NSNotification) { // Verify if correct pack is checked guard let pack = notification.object as? MGLOfflinePack, - verifyPack(pack: pack) else { return } + verifyPack(pack: pack) else { return } // Calculate progress of downloading let packProgress = pack.progress let downloadProgress = calculateDownloadingProgress( @@ -97,7 +110,7 @@ class OfflinePackDownloader { completedResourceCount: packProgress.countOfResourcesCompleted ) // Check if downloading is complete - if (pack.state == .complete) { + if pack.state == .complete { print("Region downloaded successfully") // set download state to inactive // This can be called multiple times but result can only be called once. We use this @@ -107,17 +120,17 @@ class OfflinePackDownloader { channelHandler.onSuccess() result(nil) if let region = OfflineRegion.fromOfflinePack(pack) { - OfflineManagerUtils.releaseDownloader(id:region.id) + OfflineManagerUtils.releaseDownloader(id: region.id) } } else { print("Region download progress \(downloadProgress)") channelHandler.onProgress(progress: downloadProgress) } } - + @objc private func onPackDownloadError(notification: NSNotification) { guard let pack = notification.object as? MGLOfflinePack, - verifyPack(pack: pack) else { return } + verifyPack(pack: pack) else { return } let error = notification.userInfo?[MGLOfflinePackUserInfoKey.error] as? NSError print("Pack download error: \(String(describing: error?.localizedDescription))") // set download state to inactive @@ -134,15 +147,14 @@ class OfflinePackDownloader { )) if let region = OfflineRegion.fromOfflinePack(pack) { OfflineManagerUtils.deleteRegion(result: result, id: region.id) - OfflineManagerUtils.releaseDownloader(id:region.id) } } - + @objc private func onMaximumAllowedMapboxTiles(notification: NSNotification) { guard let pack = notification.object as? MGLOfflinePack, - verifyPack(pack: pack) else { return } + verifyPack(pack: pack) else { return } let maximumCount = (notification.userInfo?[MGLOfflinePackUserInfoKey.maximumCount] - as AnyObject).uint64Value ?? 0 + as AnyObject).uint64Value ?? 0 print("Mapbox tile count limit exceeded: \(maximumCount)") // set download state to inactive isCompleted = true @@ -158,11 +170,11 @@ class OfflinePackDownloader { )) if let region = OfflineRegion.fromOfflinePack(pack) { OfflineManagerUtils.deleteRegion(result: result, id: region.id) - OfflineManagerUtils.releaseDownloader(id: region.id) } } - + // MARK: Util methods + private func setupNotifications() { NotificationCenter.default.addObserver( self, @@ -183,7 +195,7 @@ class OfflinePackDownloader { object: nil ) } - + /// Since NotificationCenter will send notifications about all packs downloads we need to make sure we only handle packs /// managed by this downloader. So this method checks if the pack we got from a notification is the same as the pack being /// managed by this downloader and if it is it returns true. Otherwise it returns false @@ -193,9 +205,9 @@ class OfflinePackDownloader { return false } // We can tell whether 2 packs are the same by comparing metadata we assigned earlier - return pack.context == currentlyManagedPack.context + return pack.state != .invalid && pack.context == currentlyManagedPack.context } - + private func calculateDownloadingProgress( requiredResourceCount: UInt64, completedResourceCount: UInt64 diff --git a/ios/Classes/OfflineRegion.swift b/ios/Classes/OfflineRegion.swift index bb47bd6ef..382316845 100644 --- a/ios/Classes/OfflineRegion.swift +++ b/ios/Classes/OfflineRegion.swift @@ -12,9 +12,9 @@ class OfflineRegion { let id: Int let metadata: [String: Any] let definition: OfflineRegionDefinition - + enum CodingKeys: CodingKey { - case id, metadata, definition + case id, metadata, definition } init(id: Int, metadata: [String: Any], definition: OfflineRegionDefinition) { @@ -33,16 +33,16 @@ class OfflineRegion { return [ "id": id, "metadata": metadata, - "definition": definition.toDictionary() + "definition": definition.toDictionary(), ] } static func fromOfflinePack(_ pack: MGLOfflinePack) -> OfflineRegion? { guard let region = pack.region as? MGLTilePyramidOfflineRegion, - let dataObject = try? JSONSerialization.jsonObject(with: pack.context, options: []), - let dict = dataObject as? [String: Any], - let id = dict["id"] as? Int, - let metadata = dict["metadata"] as? [String: Any] else { return nil } + let dataObject = try? JSONSerialization.jsonObject(with: pack.context, options: []), + let dict = dataObject as? [String: Any], + let id = dict["id"] as? Int, + let metadata = dict["metadata"] as? [String: Any] else { return nil } return OfflineRegion( id: id, metadata: metadata, diff --git a/ios/Classes/OfflineRegionDefinition.swift b/ios/Classes/OfflineRegionDefinition.swift index c198f0847..a4046b381 100644 --- a/ios/Classes/OfflineRegionDefinition.swift +++ b/ios/Classes/OfflineRegionDefinition.swift @@ -23,11 +23,11 @@ class OfflineRegionDefinition { static func fromDictionary(_ jsonDict: [String: Any]) -> OfflineRegionDefinition? { guard let bounds = jsonDict["bounds"] as? [[Double]], - let mapStyleUrlString = jsonDict["mapStyleUrl"] as? String, - let mapStyleUrl = URL(string: mapStyleUrlString), - let minZoom = jsonDict["minZoom"] as? Double, - let maxZoom = jsonDict["maxZoom"] as? Double - else { return nil } + let mapStyleUrlString = jsonDict["mapStyleUrl"] as? String, + let mapStyleUrl = URL(string: mapStyleUrlString), + let minZoom = jsonDict["minZoom"] as? Double, + let maxZoom = jsonDict["maxZoom"] as? Double + else { return nil } return OfflineRegionDefinition( bounds: bounds, mapStyleUrl: mapStyleUrl, @@ -38,11 +38,11 @@ class OfflineRegionDefinition { func toDictionary() -> [String: Any] { return [ - "bounds": self.bounds, - "mapStyleUrl": self.mapStyleUrl.absoluteString, - "minZoom": self.minZoom, - "maxZoom": self.maxZoom, - ]; + "bounds": bounds, + "mapStyleUrl": mapStyleUrl.absoluteString, + "minZoom": minZoom, + "maxZoom": maxZoom, + ] } func toMGLTilePyramidOfflineRegion() -> MGLTilePyramidOfflineRegion { 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/ios/Classes/SourcePropertyConverter.swift b/ios/Classes/SourcePropertyConverter.swift new file mode 100644 index 000000000..4831a4b4b --- /dev/null +++ b/ios/Classes/SourcePropertyConverter.swift @@ -0,0 +1,177 @@ +import Foundation +import Mapbox + +class SourcePropertyConverter { + class func interpretTileOptions(properties: [String: Any]) -> [MGLTileSourceOption: Any] { + var options = [MGLTileSourceOption: Any]() + + if let bounds = properties["bounds"] as? [Double] { + options[.coordinateBounds] = + NSValue(mglCoordinateBounds: boundsFromArray(coordinates: bounds)) + } + if let minzoom = properties["minzoom"] as? Double { + options[.minimumZoomLevel] = minzoom + } + if let maxzoom = properties["maxzoom"] as? Double { + options[.maximumZoomLevel] = maxzoom + } + if let tileSize = properties["tileSize"] as? Double { + options[.tileSize] = Int(tileSize) + } + if let scheme = properties["scheme"] as? String { + let system: MGLTileCoordinateSystem = (scheme == "tms" ? .TMS : .XYZ) + options[.tileCoordinateSystem] = system.rawValue + } + return options + // TODO: attribution not implemneted for IOS + } + + class func buildRasterTileSource(identifier: String, + properties: [String: Any]) -> MGLRasterTileSource? + { + if let rawUrl = properties["url"] as? String, let url = URL(string: rawUrl) { + return MGLRasterTileSource(identifier: identifier, configurationURL: url) + } + if let tiles = properties["tiles"] as? [String] { + let options = interpretTileOptions(properties: properties) + return MGLRasterTileSource( + identifier: identifier, + tileURLTemplates: tiles, + options: options + ) + } + return nil + } + + class func buildVectorTileSource(identifier: String, + properties: [String: Any]) -> MGLVectorTileSource? + { + if let rawUrl = properties["url"] as? String, let url = URL(string: rawUrl) { + return MGLVectorTileSource(identifier: identifier, configurationURL: url) + } + if let tiles = properties["tiles"] as? [String] { + return MGLVectorTileSource( + identifier: identifier, + tileURLTemplates: tiles, + options: interpretTileOptions(properties: properties) + ) + } + return nil + } + + class func buildRasterDemSource(identifier: String, + properties: [String: Any]) -> MGLRasterDEMSource? + { + if let rawUrl = properties["url"] as? String, let url = URL(string: rawUrl) { + return MGLRasterDEMSource(identifier: identifier, configurationURL: url) + } + if let tiles = properties["tiles"] as? [String] { + return MGLRasterDEMSource( + identifier: identifier, + tileURLTemplates: tiles, + options: interpretTileOptions(properties: properties) + ) + } + return nil + } + + class func interpretShapeOptions(properties: [String: Any]) -> [MGLShapeSourceOption: Any] { + var options = [MGLShapeSourceOption: Any]() + + if let maxzoom = properties["maxzoom"] as? Double { + options[.maximumZoomLevel] = maxzoom + } + + if let buffer = properties["buffer"] as? Double { + options[.buffer] = buffer + } + if let tolerance = properties["tolerance"] as? Double { + options[.simplificationTolerance] = tolerance + } + + if let cluster = properties["cluster"] as? Bool { + options[.clustered] = cluster + } + if let clusterRadius = properties["clusterRadius"] as? Double { + options[.clusterRadius] = clusterRadius + } + if let clusterMaxZoom = properties["clusterMaxZoom"] as? Double { + options[.maximumZoomLevelForClustering] = clusterMaxZoom + } + + // TODO: clusterProperties not implemneted for IOS + + if let lineMetrics = properties["lineMetrics"] as? Bool { + options[.lineDistanceMetrics] = lineMetrics + } + return options + } + + class func buildShapeSource(identifier: String, properties: [String: Any]) -> MGLShapeSource? { + let options = interpretShapeOptions(properties: properties) + if let data = properties["data"] as? String, let url = URL(string: data) { + return MGLShapeSource(identifier: identifier, url: url, options: options) + } + if let data = properties["data"] { + do { + let geoJsonData = try JSONSerialization.data(withJSONObject: data) + let shape = try MGLShape(data: geoJsonData, encoding: String.Encoding.utf8.rawValue) + return MGLShapeSource(identifier: identifier, shape: shape, options: options) + } catch {} + } + return nil + } + + class func buildImageSource(identifier: String, properties: [String: Any]) -> MGLImageSource? { + if let rawUrl = properties["url"] as? String, let url = URL(string: rawUrl), + let coordinates = properties["coordinates"] as? [[Double]] + { + return MGLImageSource( + identifier: identifier, + coordinateQuad: quadFromArray(coordinates: coordinates), + url: url + ) + } + return nil + } + + class func addShapeProperties(properties: [String: Any], source: MGLShapeSource) { + do { + if let data = properties["data"] as? String { + let parsed = try MGLShape( + data: data.data(using: .utf8)!, + encoding: String.Encoding.utf8.rawValue + ) + source.shape = parsed + } + } catch {} + } + + class func quadFromArray(coordinates: [[Double]]) -> MGLCoordinateQuad { + return MGLCoordinateQuad( + topLeft: CLLocationCoordinate2D( + latitude: coordinates[0][1], + longitude: coordinates[0][0] + ), + bottomLeft: CLLocationCoordinate2D( + latitude: coordinates[3][1], + longitude: coordinates[3][0] + ), + bottomRight: CLLocationCoordinate2D( + latitude: coordinates[2][1], + longitude: coordinates[2][0] + ), + topRight: CLLocationCoordinate2D( + latitude: coordinates[1][1], + longitude: coordinates[1][0] + ) + ) + } + + class func boundsFromArray(coordinates: [Double]) -> MGLCoordinateBounds { + return MGLCoordinateBounds( + sw: CLLocationCoordinate2D(latitude: coordinates[1], longitude: coordinates[0]), + ne: CLLocationCoordinate2D(latitude: coordinates[3], longitude: coordinates[2]) + ) + } +} diff --git a/ios/Classes/SwiftMapboxGlFlutterPlugin.swift b/ios/Classes/SwiftMapboxGlFlutterPlugin.swift index 8f00790a7..189bfc146 100644 --- a/ios/Classes/SwiftMapboxGlFlutterPlugin.swift +++ b/ios/Classes/SwiftMapboxGlFlutterPlugin.swift @@ -1,16 +1,36 @@ import Flutter +import Foundation +import Mapbox import UIKit public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let instance = MapboxMapFactory(withRegistrar: registrar) registrar.register(instance, withId: "plugins.flutter.io/mapbox_gl") - - let channel = FlutterMethodChannel(name: "plugins.flutter.io/mapbox_gl", binaryMessenger: registrar.messenger()) + let channel = FlutterMethodChannel( + name: "plugins.flutter.io/mapbox_gl", + binaryMessenger: registrar.messenger() + ) - channel.setMethodCallHandler { (methodCall, result) in - switch(methodCall.method) { + channel.setMethodCallHandler { methodCall, result in + switch methodCall.method { + case "setHttpHeaders": + guard let arguments = methodCall.arguments as? [String: Any], + let headers = arguments["headers"] as? [String: String] + else { + result(FlutterError( + code: "setHttpHeadersError", + message: "could not decode arguments", + details: nil + )) + result(nil) + return + } + let sessionConfig = URLSessionConfiguration.default + sessionConfig.httpAdditionalHeaders = headers // your headers here + MGLNetworkConfiguration.sharedManager.sessionConfiguration = sessionConfig + result(nil) case "installOfflineMapTiles": guard let arguments = methodCall.arguments as? [String: String] else { return } let tilesdb = arguments["tilesdb"] @@ -23,11 +43,13 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { let metadata = args["metadata"] as? [String: Any], let defintion = OfflineRegionDefinition.fromDictionary(definitionDictionary), let channelName = args["channelName"] as? String - else { - print("downloadOfflineRegion unexpected arguments: \(String(describing: methodCall.arguments))") - result(nil) - return - } + else { + print( + "downloadOfflineRegion unexpected arguments: \(String(describing: methodCall.arguments))" + ) + result(nil) + return + } // Prepare channel let channelHandler = OfflineChannelHandler( messenger: registrar.messenger(), @@ -42,13 +64,14 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { ) case "setOfflineTileCountLimit": guard let arguments = methodCall.arguments as? [String: Any], - let limit = arguments["limit"] as? UInt64 else { - result(FlutterError( - code: "SetOfflineTileCountLimitError", - message: "could not decode arguments", - details: nil - )) - return + let limit = arguments["limit"] as? UInt64 + else { + result(FlutterError( + code: "SetOfflineTileCountLimitError", + message: "could not decode arguments", + details: nil + )) + return } OfflineManagerUtils.setOfflineTileCountLimit(result: result, maximumCount: limit) case "getListOfRegions": @@ -56,9 +79,10 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { OfflineManagerUtils.regionsList(result: result) case "deleteOfflineRegion": guard let args = methodCall.arguments as? [String: Any], - let id = args["id"] as? Int else { - result(nil) - return + let id = args["id"] as? Int + else { + result(nil) + return } OfflineManagerUtils.deleteRegion(result: result, id: id) default: @@ -68,9 +92,14 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { } private static func getTilesUrl() -> URL { - guard var cachesUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first, - let bundleId = Bundle.main.object(forInfoDictionaryKey: kCFBundleIdentifierKey as String) as? String else { - fatalError("Could not get map tiles directory") + guard var cachesUrl = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first, + let bundleId = Bundle.main + .object(forInfoDictionaryKey: kCFBundleIdentifierKey as String) as? String + else { + fatalError("Could not get map tiles directory") } cachesUrl.appendPathComponent(bundleId) cachesUrl.appendPathComponent(".mapbox") @@ -82,10 +111,16 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { private static func installOfflineMapTiles(registrar: FlutterPluginRegistrar, tilesdb: String) { var tilesUrl = getTilesUrl() let bundlePath = getTilesDbPath(registrar: registrar, tilesdb: tilesdb) - NSLog("Cached tiles not found, copying from bundle... \(String(describing: bundlePath)) ==> \(tilesUrl)") + NSLog( + "Cached tiles not found, copying from bundle... \(String(describing: bundlePath)) ==> \(tilesUrl)" + ) do { let parentDir = tilesUrl.deletingLastPathComponent() - try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.createDirectory( + at: parentDir, + withIntermediateDirectories: true, + attributes: nil + ) if FileManager.default.fileExists(atPath: tilesUrl.path) { try FileManager.default.removeItem(atPath: tilesUrl.path) } @@ -93,14 +128,16 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try tilesUrl.setResourceValues(resourceValues) - } catch let error { + } catch { NSLog("Error copying bundled tiles: \(error)") } } - - private static func getTilesDbPath(registrar: FlutterPluginRegistrar, tilesdb: String) -> String? { - if (tilesdb.starts(with: "/")) { - return tilesdb; + + private static func getTilesDbPath(registrar: FlutterPluginRegistrar, + tilesdb: String) -> String? + { + if tilesdb.starts(with: "/") { + return tilesdb } else { let key = registrar.lookupKey(forAsset: tilesdb) return Bundle.main.path(forResource: key, ofType: nil) diff --git a/ios/Classes/Symbol.swift b/ios/Classes/Symbol.swift deleted file mode 100644 index 29506c70b..000000000 --- a/ios/Classes/Symbol.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Mapbox - -class Symbol : MGLPointAnnotation, SymbolOptionsSink { - - private var _id = UUID().uuidString - var id: String { - get { return _id } - } - - private var _iconImage: String? - var iconImage: String? { - get { return _iconImage } - } - - var textField: String? { - get { return title } - } - - var geometry: CLLocationCoordinate2D { - get{ return coordinate } - } - - // MARK: Setters - - func setIconSize(iconSize: Double) { - - } - - func setIconImage(iconImage: String) { - _iconImage = iconImage - } - - func setIconRotate(iconRotate: Double) { - - } - - func setIconAnchor(iconAnchor: String) { - - } - - func setTextField(textField: String) { - title = textField - } - - func setTextSize(textSize: Double) { - - } - - func setTextMaxWidth(textMaxWidth: Double) { - - } - - func setTextLetterSpacing(textLetterSpacing: Double) { - - } - - func setTextJustify(textJustify: String) { - - } - - func setTextAnchor(textAnchor: String) { - - } - - func setTextRotate(textRotate: Double) { - - } - - func setTextTransform(textTransform: String) { - - } - - func setIconOpacity(iconOpacity: Double) { - - } - - func setIconColor(iconColor: String) { - - } - - func setIconHaloColor(iconHaloColor: String) { - - } - - func setIconHaloWidth(iconHaloWidth: Double) { - - } - - func setIconHaloBlur(iconHaloBlur: Double) { - - } - - func setTextOpacity(textOpacity: Double) { - - } - - func setTextColor(textColor: String) { - - } - - func setTextHaloColor(textHaloColor: String) { - - } - - func setTextHaloWidth(textHaloWidth: Double) { - - } - - func setTextHaloBlur(textHaloBlur: Double) { - - } - - func setGeometry(geometry: [Double]) { - if geometry.count == 2, -90...90 ~= geometry[0], -180...180 ~= geometry[1] { - coordinate = CLLocationCoordinate2D(latitude: geometry[0], longitude: geometry[1]) - } else { - NSLog("Invalid geometry") - } - } - - func setZIndex(zIndex: Int) { - - } - - func setDraggable(draggable: Bool) { - - } -} diff --git a/ios/Classes/SymbolOptionsSink.swift b/ios/Classes/SymbolOptionsSink.swift deleted file mode 100644 index 2c07414a7..000000000 --- a/ios/Classes/SymbolOptionsSink.swift +++ /dev/null @@ -1,31 +0,0 @@ - -protocol SymbolOptionsSink { - func setIconSize(iconSize: Double) - func setIconImage(iconImage: String) - func setIconRotate(iconRotate: Double) -// final Offset iconOffset; - func setIconAnchor(iconAnchor: String) - func setTextField(textField: String) - func setTextSize(textSize: Double) - func setTextMaxWidth(textMaxWidth: Double) - func setTextLetterSpacing(textLetterSpacing: Double) - func setTextJustify(textJustify: String) - func setTextAnchor(textAnchor: String) - func setTextRotate(textRotate: Double) - func setTextTransform(textTransform: String) -// final Offset textOffset; - func setIconOpacity(iconOpacity: Double) - func setIconColor(iconColor: String) - func setIconHaloColor(iconHaloColor: String) - func setIconHaloWidth(iconHaloWidth: Double) - func setIconHaloBlur(iconHaloBlur: Double) - func setTextOpacity(textOpacity: Double) - func setTextColor(textColor: String) - func setTextHaloColor(textHaloColor: String) - func setTextHaloWidth(textHaloWidth: Double) - func setTextHaloBlur(textHaloBlur: Double) - - func setGeometry(geometry: [Double]) - func setZIndex(zIndex: Int) - func setDraggable(draggable: Bool) -} diff --git a/ios/mapbox_gl.podspec b/ios/mapbox_gl.podspec index c4f46d333..a43a7f041 100644 --- a/ios/mapbox_gl.podspec +++ b/ios/mapbox_gl.podspec @@ -16,7 +16,7 @@ A new Flutter plugin. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.dependency 'MapboxAnnotationExtension', '~> 0.0.1-beta.1' - s.dependency 'Mapbox-iOS-SDK', '~> 6.3.0' + s.dependency 'Mapbox-iOS-SDK', '~> 6.4.0' s.swift_version = '4.2' s.ios.deployment_target = '9.0' end diff --git a/lib/mapbox_gl.dart b/lib/mapbox_gl.dart index 121d6817b..ea45970d3 100644 --- a/lib/mapbox_gl.dart +++ b/lib/mapbox_gl.dart @@ -33,15 +33,30 @@ export 'package:mapbox_gl_platform_interface/mapbox_gl_platform_interface.dart' MyLocationTrackingMode, MyLocationRenderMode, CompassViewPosition, + AttributionButtonPosition, + Annotation, Circle, CircleOptions, Line, LineOptions, Fill, - FillOptions; + FillOptions, + SnapshotOptions, + SourceProperties, + RasterSourceProperties, + VectorSourceProperties, + RasterDemSourceProperties, + GeojsonSourceProperties, + VideoSourceProperties, + ImageSourceProperties; part 'src/controller.dart'; part 'src/mapbox_map.dart'; part 'src/global.dart'; part 'src/offline_region.dart'; part 'src/download_region_status.dart'; +part 'src/layer_expressions.dart'; +part 'src/layer_properties.dart'; +part 'src/color_tools.dart'; +part 'src/annotation_manager.dart'; +part 'src/util.dart'; diff --git a/lib/src/annotation_manager.dart b/lib/src/annotation_manager.dart new file mode 100644 index 000000000..65bb22426 --- /dev/null +++ b/lib/src/annotation_manager.dart @@ -0,0 +1,354 @@ +part of mapbox_gl; + +abstract class AnnotationManager { + final MapboxMapController controller; + final _idToAnnotation = {}; + final _idToLayerIndex = {}; + + /// Called if a annotation is tapped + final void Function(T)? onTap; + + /// base id of the manager. User [layerdIds] to get the actual ids. + String get id => "${managerType}_$randomPostFix"; + + final String managerType; + + final String randomPostFix; + + List get layerIds => + [for (int i = 0; i < allLayerProperties.length; i++) _makeLayerId(i)]; + + /// If disabled the manager offers no interaction for the created symbols + final bool enableInteraction; + + /// implemented to define the layer properties + List get allLayerProperties; + + /// used to spedicy the layer and annotation will life on + /// This can be replaced by layer filters a soon as they are implemented + final int Function(T)? selectLayer; + + /// get the an annotation by its id + T? byId(String id) => _idToAnnotation[id]; + + Set get annotations => _idToAnnotation.values.toSet(); + + 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([]), + promoteId: "id"); + controller.addLayer(layerId, layerId, allLayerProperties[i]); + } + + if (onTap != null) { + controller.onFeatureTapped.add(_onFeatureTapped); + } + controller.onFeatureDrag.add(_onDrag); + } + + /// This function can be used to rebuild all layers after their properties + /// changed + Future _rebuildLayers() async { + for (var i = 0; i < allLayerProperties.length; i++) { + final layerId = _makeLayerId(i); + await controller.addLayer(layerId, layerId, allLayerProperties[i]); + } + } + + _onFeatureTapped(dynamic id, Point point, LatLng coordinates) { + final annotation = _idToAnnotation[id]; + if (annotation != null) { + onTap!(annotation); + } + } + + String _makeLayerId(int layerIndex) => "${id}_$layerIndex"; + + Future _setAll() async { + if (selectLayer != null) { + final featureBuckets = [for (final _ in allLayerProperties) []]; + + for (final annotation in _idToAnnotation.values) { + final layerIndex = selectLayer!(annotation); + _idToLayerIndex[annotation.id] = layerIndex; + featureBuckets[layerIndex].add(annotation); + } + + for (var i = 0; i < featureBuckets.length; i++) { + await controller.setGeoJsonSource( + _makeLayerId(i), + buildFeatureCollection( + [for (final l in featureBuckets[i]) l.toGeoJson()])); + } + } else { + await controller.setGeoJsonSource( + _makeLayerId(0), + buildFeatureCollection( + [for (final l in _idToAnnotation.values) l.toGeoJson()])); + } + } + + /// Adds a multiple annotations to the map. This much faster than calling add + /// multiple times + Future addAll(Iterable annotations) async { + for (var a in annotations) { + _idToAnnotation[a.id] = a; + } + await _setAll(); + } + + /// add a single annotation to the map + Future add(T annotation) async { + _idToAnnotation[annotation.id] = annotation; + await _setAll(); + } + + /// Removes multiple annotations from the map + Future removeAll(Iterable annotations) async { + for (var a in annotations) { + _idToAnnotation.remove(a.id); + } + await _setAll(); + } + + /// Remove a single annotation form the map + Future remove(T annotation) async { + _idToAnnotation.remove(annotation.id); + await _setAll(); + } + + /// Removes all annotations from the map + Future clear() async { + _idToAnnotation.clear(); + + await _setAll(); + } + + /// Fully dipose of all the the resouces managed by the annotation manager. + /// The manager cannot be used after this has been called + Future dispose() async { + _idToAnnotation.clear(); + await _setAll(); + for (var i = 0; i < allLayerProperties.length; i++) { + await controller.removeLayer(_makeLayerId(i)); + await controller.removeSource(_makeLayerId(i)); + } + } + + _onDrag(dynamic id, + {required Point point, + required LatLng origin, + required LatLng current, + required LatLng delta, + required DragEventType eventType}) { + final annotation = byId(id); + if (annotation != null) { + annotation.translate(delta); + set(annotation); + } + } + + /// Set an existing anntotation to the map. Use this to do a fast update for a + /// single annotation + Future set(T anntotation) async { + assert(_idToAnnotation.containsKey(anntotation.id), + "you can only set existing annotations"); + _idToAnnotation[anntotation.id] = anntotation; + final oldLayerIndex = _idToLayerIndex[anntotation.id]; + final layerIndex = selectLayer != null ? selectLayer!(anntotation) : 0; + if (oldLayerIndex != layerIndex) { + // if the annotation has to be moved to another layer/source we have to + // set all + await _setAll(); + } else { + await controller.setGeoJsonFeature( + _makeLayerId(layerIndex), anntotation.toGeoJson()); + } + } +} + +class LineManager extends AnnotationManager { + LineManager(MapboxMapController controller, + {void Function(Line)? onTap, bool enableInteraction = true}) + : super( + controller, + managerType: "line", + onTap: onTap, + enableInteraction: enableInteraction, + selectLayer: (Line line) => line.options.linePattern == null ? 0 : 1, + ); + + static const _baseProperties = LineLayerProperties( + lineJoin: [Expressions.get, 'lineJoin'], + lineOpacity: [Expressions.get, 'lineOpacity'], + lineColor: [Expressions.get, 'lineColor'], + lineWidth: [Expressions.get, 'lineWidth'], + lineGapWidth: [Expressions.get, 'lineGapWidth'], + lineOffset: [Expressions.get, 'lineOffset'], + lineBlur: [Expressions.get, 'lineBlur'], + ); + @override + List get allLayerProperties => [ + _baseProperties, + _baseProperties.copyWith( + LineLayerProperties(linePattern: [Expressions.get, 'linePattern'])), + ]; +} + +class FillManager extends AnnotationManager { + FillManager( + MapboxMapController controller, { + void Function(Fill)? onTap, + bool enableInteraction = true, + }) : super( + controller, + managerType: "fill", + onTap: onTap, + enableInteraction: enableInteraction, + selectLayer: (Fill fill) => fill.options.fillPattern == null ? 0 : 1, + ); + @override + List get allLayerProperties => const [ + FillLayerProperties( + fillOpacity: [Expressions.get, 'fillOpacity'], + fillColor: [Expressions.get, 'fillColor'], + fillOutlineColor: [Expressions.get, 'fillOutlineColor'], + ), + FillLayerProperties( + fillOpacity: [Expressions.get, 'fillOpacity'], + fillColor: [Expressions.get, 'fillColor'], + fillOutlineColor: [Expressions.get, 'fillOutlineColor'], + fillPattern: [Expressions.get, 'fillPattern'], + ) + ]; +} + +class CircleManager extends AnnotationManager { + CircleManager( + MapboxMapController controller, { + void Function(Circle)? onTap, + bool enableInteraction = true, + }) : super( + controller, + managerType: "circle", + enableInteraction: enableInteraction, + onTap: onTap, + ); + @override + List get allLayerProperties => const [ + CircleLayerProperties( + circleRadius: [Expressions.get, 'circleRadius'], + circleColor: [Expressions.get, 'circleColor'], + circleBlur: [Expressions.get, 'circleBlur'], + circleOpacity: [Expressions.get, 'circleOpacity'], + circleStrokeWidth: [Expressions.get, 'circleStrokeWidth'], + circleStrokeColor: [Expressions.get, 'circleStrokeColor'], + circleStrokeOpacity: [Expressions.get, 'circleStrokeOpacity'], + ) + ]; +} + +class SymbolManager extends AnnotationManager { + SymbolManager( + MapboxMapController controller, { + void Function(Symbol)? onTap, + bool iconAllowOverlap = false, + bool textAllowOverlap = false, + bool iconIgnorePlacement = false, + bool textIgnorePlacement = false, + bool enableInteraction = true, + }) : _iconAllowOverlap = iconAllowOverlap, + _textAllowOverlap = textAllowOverlap, + _iconIgnorePlacement = iconIgnorePlacement, + _textIgnorePlacement = textIgnorePlacement, + super( + controller, + managerType: "symbol", + enableInteraction: enableInteraction, + onTap: onTap, + ); + + bool _iconAllowOverlap; + bool _textAllowOverlap; + bool _iconIgnorePlacement; + bool _textIgnorePlacement; + + /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision + Future setIconAllowOverlap(bool value) async { + _iconAllowOverlap = value; + await _rebuildLayers(); + } + + /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision + Future setTextAllowOverlap(bool value) async { + _textAllowOverlap = value; + await _rebuildLayers(); + } + + /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision + Future setIconIgnorePlacement(bool value) async { + _iconIgnorePlacement = value; + await _rebuildLayers(); + } + + /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision + Future setTextIgnorePlacement(bool value) async { + _textIgnorePlacement = value; + await _rebuildLayers(); + } + + @override + List get allLayerProperties => [ + SymbolLayerProperties( + iconSize: [Expressions.get, 'iconSize'], + iconImage: [Expressions.get, 'iconImage'], + iconRotate: [Expressions.get, 'iconRotate'], + iconOffset: [Expressions.get, 'iconOffset'], + iconAnchor: [Expressions.get, 'iconAnchor'], + iconOpacity: [Expressions.get, 'iconOpacity'], + iconColor: [Expressions.get, 'iconColor'], + iconHaloColor: [Expressions.get, 'iconHaloColor'], + iconHaloWidth: [Expressions.get, 'iconHaloWidth'], + iconHaloBlur: [Expressions.get, 'iconHaloBlur'], + // note that web does not support setting this in a fully data driven + // way this is a upstream issue + textFont: kIsWeb + ? null + : [ + Expressions.caseExpression, + [Expressions.has, 'fontNames'], + [Expressions.get, 'fontNames'], + [ + Expressions.literal, + ["Open Sans Regular", "Arial Unicode MS Regular"] + ], + ], + textField: [Expressions.get, 'textField'], + textSize: [Expressions.get, 'textSize'], + textMaxWidth: [Expressions.get, 'textMaxWidth'], + textLetterSpacing: [Expressions.get, 'textLetterSpacing'], + textJustify: [Expressions.get, 'textJustify'], + textAnchor: [Expressions.get, 'textAnchor'], + textRotate: [Expressions.get, 'textRotate'], + textTransform: [Expressions.get, 'textTransform'], + textOffset: [Expressions.get, 'textOffset'], + textOpacity: [Expressions.get, 'textOpacity'], + textColor: [Expressions.get, 'textColor'], + textHaloColor: [Expressions.get, 'textHaloColor'], + textHaloWidth: [Expressions.get, 'textHaloWidth'], + textHaloBlur: [Expressions.get, 'textHaloBlur'], + symbolSortKey: [Expressions.get, 'zIndex'], + iconAllowOverlap: _iconAllowOverlap, + iconIgnorePlacement: _iconIgnorePlacement, + textAllowOverlap: _textAllowOverlap, + textIgnorePlacement: _textIgnorePlacement, + ) + ]; +} diff --git a/lib/src/color_tools.dart b/lib/src/color_tools.dart new file mode 100644 index 000000000..f78a5a1c8 --- /dev/null +++ b/lib/src/color_tools.dart @@ -0,0 +1,10 @@ +part of mapbox_gl; + +extension MapBoxColorConversion on Color { + String toHexStringRGB() { + final r = red.toRadixString(16).padLeft(2, '0'); + final g = green.toRadixString(16).padLeft(2, '0'); + final b = blue.toRadixString(16).padLeft(2, '0'); + return '#$r$g$b'; + } +} diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 95e816809..e70e06a27 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -5,6 +5,17 @@ part of mapbox_gl; typedef void OnMapClickCallback(Point point, LatLng coordinates); + +typedef void OnFeatureInteractionCallback( + dynamic id, Point point, LatLng coordinates); + +typedef void OnFeatureDragnCallback(dynamic id, + {required Point point, + required LatLng origin, + required LatLng current, + required LatLng delta, + required DragEventType eventType}); + typedef void OnMapLongClickCallback(Point point, LatLng coordinates); typedef void OnAttributionClickCallback(); @@ -36,70 +47,54 @@ typedef void OnMapIdleCallback(); /// Line tap events can be received by adding callbacks to [onLineTapped]. /// Circle tap events can be received by adding callbacks to [onCircleTapped]. class MapboxMapController extends ChangeNotifier { - MapboxMapController._(this._id, CameraPosition initialCameraPosition, - {this.onStyleLoadedCallback, - this.onMapClick, - this.onMapLongClick, - this.onAttributionClick, - this.onCameraTrackingDismissed, - this.onCameraTrackingChanged, - this.onMapIdle, - this.onUserLocationUpdated, - this.onCameraIdle}) { + MapboxMapController({ + required MapboxGlPlatform mapboxGlPlatform, + required CameraPosition initialCameraPosition, + required Iterable annotationOrder, + required Iterable annotationConsumeTapEvents, + this.onStyleLoadedCallback, + this.onMapClick, + this.onMapLongClick, + this.onAttributionClick, + this.onCameraTrackingDismissed, + this.onCameraTrackingChanged, + this.onMapIdle, + this.onUserLocationUpdated, + this.onCameraIdle, + }) : _mapboxGlPlatform = mapboxGlPlatform { _cameraPosition = initialCameraPosition; - MapboxGlPlatform.getInstance(_id) - .onInfoWindowTappedPlatform - .add((symbolId) { - final symbol = _symbols[symbolId]; - if (symbol != null) { - onInfoWindowTapped(symbol); - } - }); - - MapboxGlPlatform.getInstance(_id).onSymbolTappedPlatform.add((symbolId) { - final symbol = _symbols[symbolId]; - if (symbol != null) { - onSymbolTapped(symbol); + _mapboxGlPlatform.onFeatureTappedPlatform.add((payload) { + for (final fun + in List.from(onFeatureTapped)) { + fun(payload["id"], payload["point"], payload["latLng"]); } }); - MapboxGlPlatform.getInstance(_id).onLineTappedPlatform.add((lineId) { - final line = _lines[lineId]; - if (line != null) { - onLineTapped(line); + _mapboxGlPlatform.onFeatureDraggedPlatform.add((payload) { + for (final fun in List.from(onFeatureDrag)) { + final DragEventType enmDragEventType = DragEventType.values + .firstWhere((element) => element.name == payload["eventType"]); + fun(payload["id"], + point: payload["point"], + origin: payload["origin"], + current: payload["current"], + delta: payload["delta"], + eventType: enmDragEventType); } }); - MapboxGlPlatform.getInstance(_id).onCircleTappedPlatform.add((circleId) { - final circle = _circles[circleId]; - if (circle != null) { - onCircleTapped(circle); - } - }); - - MapboxGlPlatform.getInstance(_id).onFillTappedPlatform.add((fillId) { - final fill = _fills[fillId]; - if (fill != null) { - onFillTapped(fill); - } - }); - - MapboxGlPlatform.getInstance(_id).onCameraMoveStartedPlatform.add((_) { + _mapboxGlPlatform.onCameraMoveStartedPlatform.add((_) { _isCameraMoving = true; notifyListeners(); }); - MapboxGlPlatform.getInstance(_id) - .onCameraMovePlatform - .add((cameraPosition) { + _mapboxGlPlatform.onCameraMovePlatform.add((cameraPosition) { _cameraPosition = cameraPosition; notifyListeners(); }); - MapboxGlPlatform.getInstance(_id) - .onCameraIdlePlatform - .add((cameraPosition) { + _mapboxGlPlatform.onCameraIdlePlatform.add((cameraPosition) { _isCameraMoving = false; if (cameraPosition != null) { _cameraPosition = cameraPosition; @@ -110,86 +105,82 @@ class MapboxMapController extends ChangeNotifier { notifyListeners(); }); - MapboxGlPlatform.getInstance(_id).onMapStyleLoadedPlatform.add((_) { + _mapboxGlPlatform.onMapStyleLoadedPlatform.add((_) { + final interactionEnabled = annotationConsumeTapEvents.toSet(); + for (var type in annotationOrder.toSet()) { + final enableInteraction = interactionEnabled.contains(type); + switch (type) { + case AnnotationType.fill: + fillManager = FillManager(this, + onTap: onFillTapped, enableInteraction: enableInteraction); + break; + case AnnotationType.line: + lineManager = LineManager(this, + onTap: onLineTapped, enableInteraction: enableInteraction); + break; + case AnnotationType.circle: + circleManager = CircleManager(this, + onTap: onCircleTapped, enableInteraction: enableInteraction); + break; + case AnnotationType.symbol: + symbolManager = SymbolManager(this, + onTap: onSymbolTapped, enableInteraction: enableInteraction); + break; + default: + } + } if (onStyleLoadedCallback != null) { onStyleLoadedCallback!(); } }); - MapboxGlPlatform.getInstance(_id).onMapClickPlatform.add((dict) { + _mapboxGlPlatform.onMapClickPlatform.add((dict) { if (onMapClick != null) { onMapClick!(dict['point'], dict['latLng']); } }); - MapboxGlPlatform.getInstance(_id).onMapLongClickPlatform.add((dict) { + _mapboxGlPlatform.onMapLongClickPlatform.add((dict) { if (onMapLongClick != null) { onMapLongClick!(dict['point'], dict['latLng']); } }); - MapboxGlPlatform.getInstance(_id).onAttributionClickPlatform.add((_) { + _mapboxGlPlatform.onAttributionClickPlatform.add((_) { if (onAttributionClick != null) { onAttributionClick!(); } }); - MapboxGlPlatform.getInstance(_id) - .onCameraTrackingChangedPlatform - .add((mode) { + _mapboxGlPlatform.onCameraTrackingChangedPlatform.add((mode) { if (onCameraTrackingChanged != null) { onCameraTrackingChanged!(mode); } }); - MapboxGlPlatform.getInstance(_id) - .onCameraTrackingDismissedPlatform - .add((_) { + _mapboxGlPlatform.onCameraTrackingDismissedPlatform.add((_) { if (onCameraTrackingDismissed != null) { onCameraTrackingDismissed!(); } }); - MapboxGlPlatform.getInstance(_id).onMapIdlePlatform.add((_) { + _mapboxGlPlatform.onMapIdlePlatform.add((_) { if (onMapIdle != null) { onMapIdle!(); } }); - MapboxGlPlatform.getInstance(_id) - .onUserLocationUpdatedPlatform - .add((location) { + _mapboxGlPlatform.onUserLocationUpdatedPlatform.add((location) { onUserLocationUpdated?.call(location); }); } + bool _disposed = false; - static MapboxMapController init(int id, CameraPosition initialCameraPosition, - {OnStyleLoadedCallback? onStyleLoadedCallback, - OnMapClickCallback? onMapClick, - OnUserLocationUpdated? onUserLocationUpdated, - OnMapLongClickCallback? onMapLongClick, - OnAttributionClickCallback? onAttributionClick, - OnCameraTrackingDismissedCallback? onCameraTrackingDismissed, - OnCameraTrackingChangedCallback? onCameraTrackingChanged, - OnCameraIdleCallback? onCameraIdle, - OnMapIdleCallback? onMapIdle}) { - return MapboxMapController._(id, initialCameraPosition, - onStyleLoadedCallback: onStyleLoadedCallback, - onMapClick: onMapClick, - onUserLocationUpdated: onUserLocationUpdated, - onMapLongClick: onMapLongClick, - onAttributionClick: onAttributionClick, - onCameraTrackingDismissed: onCameraTrackingDismissed, - onCameraTrackingChanged: onCameraTrackingChanged, - onCameraIdle: onCameraIdle, - onMapIdle: onMapIdle); - } - - static Future initPlatform(int id) async { - await MapboxGlPlatform.getInstance(id).initPlatform(id); - } + FillManager? fillManager; + LineManager? lineManager; + CircleManager? circleManager; + SymbolManager? symbolManager; final OnStyleLoadedCallback? onStyleLoadedCallback; - final OnMapClickCallback? onMapClick; final OnMapLongClickCallback? onMapLongClick; @@ -212,15 +203,20 @@ class MapboxMapController extends ChangeNotifier { /// Callbacks to receive tap events for fills placed on this map. final ArgumentCallbacks onFillTapped = ArgumentCallbacks(); + /// Callbacks to receive tap events for features (geojson layer) placed on this map. + final onFeatureTapped = []; + + final onFeatureDrag = []; + /// Callbacks to receive tap events for info windows on symbols + @Deprecated("InfoWindow tapped is no longer supported") final ArgumentCallbacks onInfoWindowTapped = ArgumentCallbacks(); /// The current set of symbols on this map. /// /// The returned set will be a detached snapshot of the symbols collection. - Set get symbols => Set.from(_symbols.values); - final Map _symbols = {}; + Set get symbols => symbolManager!.annotations; /// Callbacks to receive tap events for lines placed on this map. final ArgumentCallbacks onLineTapped = ArgumentCallbacks(); @@ -228,20 +224,17 @@ class MapboxMapController extends ChangeNotifier { /// The current set of lines on this map. /// /// The returned set will be a detached snapshot of the lines collection. - Set get lines => Set.from(_lines.values); - final Map _lines = {}; + Set get lines => lineManager!.annotations; /// The current set of circles on this map. /// /// The returned set will be a detached snapshot of the circles collection. - Set get circles => Set.from(_circles.values); - final Map _circles = {}; + Set get circles => circleManager!.annotations; /// The current set of fills on this map. /// /// The returned set will be a detached snapshot of the fills collection. - Set get fills => Set.from(_fills.values); - final Map _fills = {}; + Set get fills => fillManager!.annotations; /// True if the map camera is currently moving. bool get isCameraMoving => _isCameraMoving; @@ -252,15 +245,7 @@ class MapboxMapController extends ChangeNotifier { CameraPosition? get cameraPosition => _cameraPosition; CameraPosition? _cameraPosition; - final int _id; //ignore: unused_field - - Widget buildView( - Map creationParams, - OnPlatformViewCreatedCallback onPlatformViewCreated, - Set> gestureRecognizers) { - return MapboxGlPlatform.getInstance(_id) - .buildView(creationParams, onPlatformViewCreated, gestureRecognizers); - } + final MapboxGlPlatform _mapboxGlPlatform; //ignore: unused_field /// Updates configuration options of the map user interface. /// @@ -269,19 +254,48 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes after listeners have been notified. Future _updateMapOptions(Map optionsUpdate) async { - _cameraPosition = - await MapboxGlPlatform.getInstance(_id).updateMapOptions(optionsUpdate); + _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.getInstance(_id).animateCamera(cameraUpdate); + Future animateCamera(CameraUpdate cameraUpdate, + {Duration? duration}) async { + _disposeGuard(); + return _mapboxGlPlatform.animateCamera(cameraUpdate, duration: duration); } /// Instantaneously re-position the camera. @@ -292,7 +306,363 @@ 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 { - return MapboxGlPlatform.getInstance(_id).moveCamera(cameraUpdate); + _disposeGuard(); + return _mapboxGlPlatform.moveCamera(cameraUpdate); + } + + /// Adds a new geojson source + /// + /// The json in [geojson] has to comply with the schema for FeatureCollection + /// as specified in https://datatracker.ietf.org/doc/html/rfc7946#section-3.3 + /// + /// [promoteId] can be used on web to promote an id from properties to be the + /// id of the feature. This is useful because by default mapbox-gl-js does not + /// support string ids + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + /// + Future addGeoJsonSource(String sourceId, Map geojson, + {String? promoteId}) async { + _disposeGuard(); + await _mapboxGlPlatform.addGeoJsonSource(sourceId, geojson, + promoteId: promoteId); + } + + /// Sets new geojson data to and existing source + /// + /// This only works as exected if the source has been created with + /// [addGeoJsonSource] before. This is very useful if you want to update and + /// existing source with modified data. + /// + /// The json in [geojson] has to comply with the schema for FeatureCollection + /// as specified in https://datatracker.ietf.org/doc/html/rfc7946#section-3.3 + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + Future setGeoJsonSource( + String sourceId, Map geojson) async { + _disposeGuard(); + await _mapboxGlPlatform.setGeoJsonSource(sourceId, geojson); + } + + /// Sets new geojson data to and existing source + /// + /// This only works as exected if the source has been created with + /// [addGeoJsonSource] before. This is very useful if you want to update and + /// existing source with modified data. + /// + /// The json in [geojson] has to comply with the schema for FeatureCollection + /// as specified in https://datatracker.ietf.org/doc/html/rfc7946#section-3.3 + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + Future setGeoJsonFeature( + String sourceId, Map geojsonFeature) async { + _disposeGuard(); + await _mapboxGlPlatform.setFeatureForGeoJsonSource( + sourceId, geojsonFeature); + } + + /// Add a symbol 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 addSymbolLayer( + String sourceId, String layerId, SymbolLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + _disposeGuard(); + await _mapboxGlPlatform.addSymbolLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + + /// Add a line 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 addLineLayer( + String sourceId, String layerId, LineLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + _disposeGuard(); + await _mapboxGlPlatform.addLineLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + + /// Add a fill 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 addFillLayer( + String sourceId, String layerId, FillLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + _disposeGuard(); + await _mapboxGlPlatform.addFillLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + + /// 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. + /// + /// 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 addCircleLayer( + String sourceId, String layerId, CircleLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + _disposeGuard(); + await _mapboxGlPlatform.addCircleLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + + /// Add a raster 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 addRasterLayer( + String sourceId, String layerId, RasterLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + _disposeGuard(); + await _mapboxGlPlatform.addRasterLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + ); + } + + /// Add a hillshade 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 addHillshadeLayer( + String sourceId, String layerId, HillshadeLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + _disposeGuard(); + await _mapboxGlPlatform.addHillshadeLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + ); + } + + /// 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. @@ -301,7 +671,8 @@ class MapboxMapController extends ChangeNotifier { /// platform side. Future updateMyLocationTrackingMode( MyLocationTrackingMode myLocationTrackingMode) async { - return MapboxGlPlatform.getInstance(_id) + _disposeGuard(); + return _mapboxGlPlatform .updateMyLocationTrackingMode(myLocationTrackingMode); } @@ -310,8 +681,8 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the change has been made on the /// platform side. Future matchMapLanguageWithDeviceDefault() async { - return MapboxGlPlatform.getInstance(_id) - .matchMapLanguageWithDeviceDefault(); + _disposeGuard(); + return _mapboxGlPlatform.matchMapLanguageWithDeviceDefault(); } /// Updates the distance from the edges of the map view’s frame to the edges @@ -327,17 +698,19 @@ class MapboxMapController extends ChangeNotifier { /// platform side. Future updateContentInsets(EdgeInsets insets, [bool animated = false]) async { - return MapboxGlPlatform.getInstance(_id) - .updateContentInsets(insets, animated); + _disposeGuard(); + return _mapboxGlPlatform.updateContentInsets(insets, animated); } /// Updates the language of the map labels to match the specified language. /// Supported language strings are available here: https://github.com/mapbox/mapbox-plugins-android/blob/e29c18d25098eb023a831796ff807e30d8207c36/plugin-localization/src/main/java/com/mapbox/mapboxsdk/plugins/localization/MapLocale.java#L39-L87 + /// Attention: This may only be called after onStyleLoaded() has been invoked. /// /// The returned [Future] completes after the change has been made on the /// platform side. Future setMapLanguage(String language) async { - return MapboxGlPlatform.getInstance(_id).setMapLanguage(language); + _disposeGuard(); + return _mapboxGlPlatform.setMapLanguage(language); } /// Enables or disables the collection of anonymized telemetry data. @@ -345,7 +718,8 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the change has been made on the /// platform side. Future setTelemetryEnabled(bool enabled) async { - return MapboxGlPlatform.getInstance(_id).setTelemetryEnabled(enabled); + _disposeGuard(); + return _mapboxGlPlatform.setTelemetryEnabled(enabled); } /// Retrieves whether collection of anonymized telemetry data is enabled. @@ -353,7 +727,8 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes after the query has been made on the /// platform side. Future getTelemetryEnabled() async { - return MapboxGlPlatform.getInstance(_id).getTelemetryEnabled(); + _disposeGuard(); + return _mapboxGlPlatform.getTelemetryEnabled(); } /// Adds a symbol to the map, configured using the specified custom [options]. @@ -364,10 +739,11 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes with the added symbol once listeners have /// been notified. Future addSymbol(SymbolOptions options, [Map? data]) async { - List result = - await addSymbols([options], data != null ? [data] : []); - - return result.first; + final effectiveOptions = SymbolOptions.defaultOptions.copyWith(options); + final symbol = Symbol(getRandomString(), effectiveOptions, data); + await symbolManager!.add(symbol); + notifyListeners(); + return symbol; } /// Adds multiple symbols to the map, configured using the specified custom @@ -380,12 +756,13 @@ class MapboxMapController extends ChangeNotifier { /// been notified. Future> addSymbols(List options, [List? data]) async { - final List effectiveOptions = - options.map((o) => SymbolOptions.defaultOptions.copyWith(o)).toList(); + final symbols = [ + for (var i = 0; i < options.length; i++) + Symbol(getRandomString(), + SymbolOptions.defaultOptions.copyWith(options[i]), data?[i]) + ]; + await symbolManager!.addAll(symbols); - final symbols = await MapboxGlPlatform.getInstance(_id) - .addSymbols(effectiveOptions, data); - symbols.forEach((s) => _symbols[s.id] = s); notifyListeners(); return symbols; } @@ -398,10 +775,9 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future updateSymbol(Symbol symbol, SymbolOptions changes) async { - assert(_symbols[symbol.id] == symbol); + await symbolManager! + .set(symbol..options = symbol.options.copyWith(changes)); - await MapboxGlPlatform.getInstance(_id).updateSymbol(symbol, changes); - symbol.options = symbol.options.copyWith(changes); notifyListeners(); } @@ -409,11 +785,7 @@ class MapboxMapController extends ChangeNotifier { /// This may be different from the value of `symbol.options.geometry` if the symbol is draggable. /// In that case this method provides the symbol's actual position, and `symbol.options.geometry` the last programmatically set position. Future getSymbolLatLng(Symbol symbol) async { - assert(_symbols[symbol.id] == symbol); - final symbolLatLng = - await MapboxGlPlatform.getInstance(_id).getSymbolLatLng(symbol); - notifyListeners(); - return symbolLatLng; + return symbol.options.geometry!; } /// Removes the specified [symbol] from the map. The symbol must be a current @@ -424,8 +796,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeSymbol(Symbol symbol) async { - assert(_symbols[symbol.id] == symbol); - await _removeSymbols([symbol.id]); + await symbolManager!.remove(symbol); notifyListeners(); } @@ -437,10 +808,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeSymbols(Iterable symbols) async { - final ids = symbols.where((s) => _symbols[s.id] == s).map((s) => s.id); - assert(symbols.length == ids.length); - - await _removeSymbols(ids); + await symbolManager!.removeAll(symbols); notifyListeners(); } @@ -451,21 +819,10 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future clearSymbols() async { - await MapboxGlPlatform.getInstance(_id).removeSymbols(_symbols.keys); - _symbols.clear(); + symbolManager!.clear(); notifyListeners(); } - /// Helper method to remove a single symbol from the map. Consumed by - /// [removeSymbol] and [clearSymbols]. - /// - /// The returned [Future] completes once the symbol has been removed from - /// [_symbols]. - Future _removeSymbols(Iterable ids) async { - await MapboxGlPlatform.getInstance(_id).removeSymbols(ids); - _symbols.removeWhere((k, s) => ids.contains(k)); - } - /// Adds a line to the map, configured using the specified custom [options]. /// /// Change listeners are notified once the line has been added on the @@ -474,11 +831,9 @@ class MapboxMapController extends ChangeNotifier { /// The returned [Future] completes with the added line once listeners have /// been notified. Future addLine(LineOptions options, [Map? data]) async { - final LineOptions effectiveOptions = - LineOptions.defaultOptions.copyWith(options); - final line = - await MapboxGlPlatform.getInstance(_id).addLine(effectiveOptions, data); - _lines[line.id] = line; + final effectiveOptions = LineOptions.defaultOptions.copyWith(options); + final line = Line(getRandomString(), effectiveOptions, data); + await lineManager!.add(line); notifyListeners(); return line; } @@ -492,9 +847,13 @@ class MapboxMapController extends ChangeNotifier { /// been notified. Future> addLines(List options, [List? data]) async { - final lines = - await MapboxGlPlatform.getInstance(_id).addLines(options, data); - lines.forEach((l) => _lines[l.id] = l); + final lines = [ + for (var i = 0; i < options.length; i++) + Line(getRandomString(), LineOptions.defaultOptions.copyWith(options[i]), + data?[i]) + ]; + await lineManager!.addAll(lines); + notifyListeners(); return lines; } @@ -507,9 +866,8 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future updateLine(Line line, LineOptions changes) async { - assert(_lines[line.id] == line); - await MapboxGlPlatform.getInstance(_id).updateLine(line, changes); line.options = line.options.copyWith(changes); + await lineManager!.set(line); notifyListeners(); } @@ -517,11 +875,7 @@ class MapboxMapController extends ChangeNotifier { /// This may be different from the value of `line.options.geometry` if the line is draggable. /// In that case this method provides the line's actual position, and `line.options.geometry` the last programmatically set position. Future> getLineLatLngs(Line line) async { - assert(_lines[line.id] == line); - final lineLatLngs = - await MapboxGlPlatform.getInstance(_id).getLineLatLngs(line); - notifyListeners(); - return lineLatLngs; + return line.options.geometry!; } /// Removes the specified [line] from the map. The line must be a current @@ -532,10 +886,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeLine(Line line) async { - assert(_lines[line.id] == line); - - await MapboxGlPlatform.getInstance(_id).removeLine(line.id); - _lines.remove(line.id); + await lineManager!.remove(line); notifyListeners(); } @@ -547,11 +898,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeLines(Iterable lines) async { - final ids = lines.where((l) => _lines[l.id] == l).map((l) => l.id); - assert(lines.length == ids.length); - - await MapboxGlPlatform.getInstance(_id).removeLines(ids); - ids.forEach((id) => _lines.remove(id)); + await lineManager!.removeAll(lines); notifyListeners(); } @@ -562,9 +909,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future clearLines() async { - final List lineIds = List.from(_lines.keys); - await MapboxGlPlatform.getInstance(_id).removeLines(lineIds); - _lines.clear(); + await lineManager!.clear(); notifyListeners(); } @@ -578,9 +923,8 @@ class MapboxMapController extends ChangeNotifier { Future addCircle(CircleOptions options, [Map? data]) async { final CircleOptions effectiveOptions = CircleOptions.defaultOptions.copyWith(options); - final circle = await MapboxGlPlatform.getInstance(_id) - .addCircle(effectiveOptions, data); - _circles[circle.id] = circle; + final circle = Circle(getRandomString(), effectiveOptions, data); + await circleManager!.add(circle); notifyListeners(); return circle; } @@ -595,11 +939,15 @@ class MapboxMapController extends ChangeNotifier { /// been notified. Future> addCircles(List options, [List? data]) async { - final circles = - await MapboxGlPlatform.getInstance(_id).addCircles(options, data); - circles.forEach((c) => _circles[c.id] = c); + final cricles = [ + for (var i = 0; i < options.length; i++) + Circle(getRandomString(), + CircleOptions.defaultOptions.copyWith(options[i]), data?[i]) + ]; + await circleManager!.addAll(cricles); + notifyListeners(); - return circles; + return cricles; } /// Updates the specified [circle] with the given [changes]. The circle must @@ -610,9 +958,9 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future updateCircle(Circle circle, CircleOptions changes) async { - assert(_circles[circle.id] == circle); - await MapboxGlPlatform.getInstance(_id).updateCircle(circle, changes); circle.options = circle.options.copyWith(changes); + await circleManager!.set(circle); + notifyListeners(); } @@ -620,11 +968,7 @@ class MapboxMapController extends ChangeNotifier { /// This may be different from the value of `circle.options.geometry` if the circle is draggable. /// In that case this method provides the circle's actual position, and `circle.options.geometry` the last programmatically set position. Future getCircleLatLng(Circle circle) async { - assert(_circles[circle.id] == circle); - final circleLatLng = - await MapboxGlPlatform.getInstance(_id).getCircleLatLng(circle); - notifyListeners(); - return circleLatLng; + return circle.options.geometry!; } /// Removes the specified [circle] from the map. The circle must be a current @@ -635,10 +979,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeCircle(Circle circle) async { - assert(_circles[circle.id] == circle); - - await MapboxGlPlatform.getInstance(_id).removeCircle(circle.id); - _circles.remove(circle.id); + circleManager!.remove(circle); notifyListeners(); } @@ -651,11 +992,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeCircles(Iterable circles) async { - final ids = circles.where((c) => _circles[c.id] == c).map((c) => c.id); - assert(circles.length == ids.length); - - await MapboxGlPlatform.getInstance(_id).removeCircles(ids); - ids.forEach((id) => _circles.remove(id)); + await circleManager!.removeAll(circles); notifyListeners(); } @@ -666,8 +1003,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future clearCircles() async { - await MapboxGlPlatform.getInstance(_id).removeCircles(_circles.keys); - _circles.clear(); + circleManager!.clear(); notifyListeners(); } @@ -682,9 +1018,8 @@ class MapboxMapController extends ChangeNotifier { Future addFill(FillOptions options, [Map? data]) async { final FillOptions effectiveOptions = FillOptions.defaultOptions.copyWith(options); - final fill = - await MapboxGlPlatform.getInstance(_id).addFill(effectiveOptions, data); - _fills[fill.id] = fill; + final fill = Fill(getRandomString(), effectiveOptions, data); + await fillManager!.add(fill); notifyListeners(); return fill; } @@ -699,11 +1034,15 @@ class MapboxMapController extends ChangeNotifier { /// been notified. Future> addFills(List options, [List? data]) async { - final circles = - await MapboxGlPlatform.getInstance(_id).addFills(options, data); - circles.forEach((f) => _fills[f.id] = f); + final fills = [ + for (var i = 0; i < options.length; i++) + Fill(getRandomString(), FillOptions.defaultOptions.copyWith(options[i]), + data?[i]) + ]; + await fillManager!.addAll(fills); + notifyListeners(); - return circles; + return fills; } /// Updates the specified [fill] with the given [changes]. The fill must @@ -714,9 +1053,9 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future updateFill(Fill fill, FillOptions changes) async { - assert(_fills[fill.id] == fill); - await MapboxGlPlatform.getInstance(_id).updateFill(fill, changes); fill.options = fill.options.copyWith(changes); + await fillManager!.set(fill); + notifyListeners(); } @@ -727,8 +1066,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future clearFills() async { - await MapboxGlPlatform.getInstance(_id).removeFills(_fills.keys); - _fills.clear(); + await fillManager!.clear(); notifyListeners(); } @@ -741,10 +1079,7 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeFill(Fill fill) async { - assert(_fills[fill.id] == fill); - await MapboxGlPlatform.getInstance(_id).removeFill(fill.id); - _fills.remove(fill.id); - + await fillManager!.remove(fill); notifyListeners(); } @@ -756,40 +1091,42 @@ class MapboxMapController extends ChangeNotifier { /// /// The returned [Future] completes once listeners have been notified. Future removeFills(Iterable fills) async { - final ids = fills.where((f) => _fills[f.id] == f).map((f) => f.id); - assert(fills.length == ids.length); - - await MapboxGlPlatform.getInstance(_id).removeFills(ids); - ids.forEach((id) => _fills.remove(id)); + await fillManager!.removeAll(fills); notifyListeners(); } + /// Query rendered features at a point in screen cooridnates Future queryRenderedFeatures( Point point, List layerIds, List? filter) async { - return MapboxGlPlatform.getInstance(_id) - .queryRenderedFeatures(point, layerIds, filter); + _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 { - return MapboxGlPlatform.getInstance(_id) - .queryRenderedFeaturesInRect(rect, layerIds, filter); + _disposeGuard(); + return _mapboxGlPlatform.queryRenderedFeaturesInRect( + rect, layerIds, filter); } Future invalidateAmbientCache() async { - return MapboxGlPlatform.getInstance(_id).invalidateAmbientCache(); + _disposeGuard(); + return _mapboxGlPlatform.invalidateAmbientCache(); } /// Get last my location /// /// Return last latlng, nullable Future requestMyLocationLatLng() async { - return MapboxGlPlatform.getInstance(_id).requestMyLocationLatLng(); + _disposeGuard(); + return _mapboxGlPlatform.requestMyLocationLatLng(); } /// This method returns the boundaries of the region currently displayed in the map. Future getVisibleRegion() async { - return MapboxGlPlatform.getInstance(_id).getVisibleRegion(); + _disposeGuard(); + return _mapboxGlPlatform.getVisibleRegion(); } /// Adds an image to the style currently displayed in the map, so that it can later be referred to by the provided name. @@ -828,59 +1165,100 @@ class MapboxMapController extends ChangeNotifier { /// } /// ``` Future addImage(String name, Uint8List bytes, [bool sdf = false]) { - return MapboxGlPlatform.getInstance(_id).addImage(name, bytes, sdf); + _disposeGuard(); + return _mapboxGlPlatform.addImage(name, bytes, sdf); } /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision Future setSymbolIconAllowOverlap(bool enable) async { - await MapboxGlPlatform.getInstance(_id).setSymbolIconAllowOverlap(enable); + await symbolManager?.setIconAllowOverlap(enable); } /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision Future setSymbolIconIgnorePlacement(bool enable) async { - await MapboxGlPlatform.getInstance(_id) - .setSymbolIconIgnorePlacement(enable); + await symbolManager?.setIconIgnorePlacement(enable); } /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision Future setSymbolTextAllowOverlap(bool enable) async { - await MapboxGlPlatform.getInstance(_id).setSymbolTextAllowOverlap(enable); + await symbolManager?.setTextAllowOverlap(enable); } /// For more information on what this does, see https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-collision Future setSymbolTextIgnorePlacement(bool enable) async { - await MapboxGlPlatform.getInstance(_id) - .setSymbolTextIgnorePlacement(enable); + await symbolManager?.setTextIgnorePlacement(enable); } /// 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) { - return MapboxGlPlatform.getInstance(_id) - .addImageSource(imageSourceId, bytes, 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) { - return MapboxGlPlatform.getInstance(_id).removeImageSource(imageSourceId); + _disposeGuard(); + return _mapboxGlPlatform.removeSource(imageSourceId); + } + + /// Removes previously added source by id + Future removeSource(String sourceId) { + _disposeGuard(); + return _mapboxGlPlatform.removeSource(sourceId); } - /// Adds a Mapbox style layer to the map's style at render time. - Future addLayer(String imageLayerId, String imageSourceId) { - return MapboxGlPlatform.getInstance(_id) - .addLayer(imageLayerId, imageSourceId); + /// 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); } - /// Adds a Mapbox style layer below the layer provided with belowLayerId to the map's style at render time, + /// Adds a Mapbox image layer below the layer provided with belowLayerId to the map's style at render time. + Future addImageLayerBelow( + String layerId, String sourceId, String imageSourceId, + {double? minzoom, double? maxzoom}) { + _disposeGuard(); + return _mapboxGlPlatform.addLayerBelow( + layerId, sourceId, imageSourceId, minzoom, maxzoom); + } + + /// Adds a Mapbox image layer below the layer provided with belowLayerId to the map's style at render time. Only works for image sources! + @Deprecated("This method was renamed to addImageLayerBelow for clarity.") Future addLayerBelow( - String imageLayerId, String imageSourceId, String belowLayerId) { - return MapboxGlPlatform.getInstance(_id) - .addLayerBelow(imageLayerId, imageSourceId, belowLayerId); + 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 imageLayerId) { - return MapboxGlPlatform.getInstance(_id).removeLayer(imageLayerId); + 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) @@ -890,22 +1268,145 @@ class MapboxMapController extends ChangeNotifier { /// /// Returns null if [latLng] is not currently visible on the map. Future toScreenLocation(LatLng latLng) async { - return MapboxGlPlatform.getInstance(_id).toScreenLocation(latLng); + _disposeGuard(); + return _mapboxGlPlatform.toScreenLocation(latLng); } Future> toScreenLocationBatch(Iterable latLngs) async { - return MapboxGlPlatform.getInstance(_id).toScreenLocationBatch(latLngs); + _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 { - return MapboxGlPlatform.getInstance(_id).toLatLng(screenLocation); + _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 { - return MapboxGlPlatform.getInstance(_id) - .getMetersPerPixelAtLatitude(latitude); + _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. 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 + /// events this has no effect for [RasterLayerProperties] and + /// [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. + /// + /// [expressions]: https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions + Future addLayer( + String sourceId, String layerId, LayerProperties properties, + {String? belowLayerId, + bool enableInteraction = true, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter}) async { + if (properties is FillLayerProperties) { + addFillLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + enableInteraction: enableInteraction, + sourceLayer: sourceLayer, + 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, + enableInteraction: enableInteraction, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter); + } else if (properties is SymbolLayerProperties) { + addSymbolLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + enableInteraction: enableInteraction, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter); + } else if (properties is CircleLayerProperties) { + addCircleLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + enableInteraction: enableInteraction, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter); + } else if (properties is RasterLayerProperties) { + if (filter != null) { + throw UnimplementedError("RasterLayer does not support filter"); + } + addRasterLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom); + } else if (properties is HillshadeLayerProperties) { + if (filter != null) { + throw UnimplementedError("HillShadeLayer does not support filter"); + } + addHillshadeLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + 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/global.dart b/lib/src/global.dart index 43b6788b0..d53aaa6ce 100644 --- a/lib/src/global.dart +++ b/lib/src/global.dart @@ -18,6 +18,8 @@ Future installOfflineMapTiles(String tilesDb) async { ); } +enum DragEventType { start, drag, end } + Future setOffline( bool offline, { String? accessToken, @@ -30,6 +32,15 @@ Future setOffline( }, ); +Future setHttpHeaders(Map headers) { + return _globalChannel.invokeMethod( + 'setHttpHeaders', + { + 'headers': headers, + }, + ); +} + Future> mergeOfflineRegions( String path, { String? accessToken, @@ -100,8 +111,8 @@ Future downloadOfflineRegion( String channelName = 'downloadOfflineRegion_${DateTime.now().microsecondsSinceEpoch}'; - final result = - _globalChannel.invokeMethod('downloadOfflineRegion', { + final result = await _globalChannel + .invokeMethod('downloadOfflineRegion', { 'accessToken': accessToken, 'channelName': channelName, 'definition': definition.toMap(), @@ -153,5 +164,5 @@ Future downloadOfflineRegion( }); } - return OfflineRegion.fromMap(json.decode(await result)); + return OfflineRegion.fromMap(json.decode(result)); } diff --git a/lib/src/layer_expressions.dart b/lib/src/layer_expressions.dart new file mode 100644 index 000000000..2a6b00e1b --- /dev/null +++ b/lib/src/layer_expressions.dart @@ -0,0 +1,657 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl; + +class Expressions { + /// Binds expressions to named variables, which can then be referenced in + /// the result expression using ["var", "variable_name"]. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const let = "let"; + + /// References variable bound using "let". + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const varExpression = "var"; + + /// Provides a literal array or object value. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const literal = "literal"; + + /// Asserts that the input is an array (optionally with a specific item + /// type and length). If, when the input expression is evaluated, it is + /// not of the asserted type, then this assertion will cause the whole + /// expression to be aborted. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const array = "array"; + + /// Retrieves an item from an array. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const at = "at"; + + /// Determines whether an item exists in an array or a substring exists in + /// a string. + /// + /// Sdk Support: + /// basic functionality with js + static const inExpression = "in"; + + /// Selects the first output whose corresponding test condition evaluates + /// to true, or the fallback value otherwise. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const caseExpression = "case"; + + /// Selects the output whose label value matches the input value, or the + /// fallback value if no match is found. The input can be any expression + /// (e.g. `["get", "building_type"]`). Each label must be either: + /// * a single literal value; or + /// * an array of literal values, whose values must be all strings or all + /// numbers (e.g. `[100, 101]` or `["c", "b"]`). The input matches if any + /// of the values in the array matches, similar to the `"in"` + /// operator.Each label must be unique. If the input type does not match + /// the type of the labels, the result will be the fallback value. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const match = "match"; + + /// Evaluates each expression in turn until the first non-null value is + /// obtained, and returns that value. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const coalesce = "coalesce"; + + /// Produces discrete, stepped results by evaluating a piecewise-constant + /// function defined by pairs of input and output values ("stops"). The + /// `input` may be any numeric expression (e.g., `["get", "population"]`). + /// Stop inputs must be numeric literals in strictly ascending order. + /// Returns the output value of the stop just less than the input, or the + /// first output if the input is less than the first stop. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const step = "step"; + + /// Produces continuous, smooth results by interpolating between pairs of + /// input and output values ("stops"). The `input` may be any numeric + /// expression (e.g., `["get", "population"]`). Stop inputs must be + /// numeric literals in strictly ascending order. The output type must be + /// `number`, `array`, or `color`.Interpolation types:- + /// `["linear"]`: interpolates linearly between the pair of stops just + /// less than and just greater than the input.- `["exponential", base]`: + /// interpolates exponentially between the stops just less than and just + /// greater than the input. `base` controls the rate at which the output + /// increases: higher values make the output increase more towards the + /// high end of the range. With values close to 1 the output increases + /// linearly.- `["cubic-bezier", x1, y1, x2, y2]`: interpolates using the + /// cubic bezier curve defined by the given control points. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const interpolate = "interpolate"; + + /// Produces continuous, smooth results by interpolating between pairs of + /// input and output values ("stops"). Works like `interpolate`, but the + /// output type must be `color`, and the interpolation is performed in the + /// Hue-Chroma-Luminance color space. + /// + /// Sdk Support: + /// basic functionality with js + static const interpolateHcl = "interpolate-hcl"; + + /// Produces continuous, smooth results by interpolating between pairs of + /// input and output values ("stops"). Works like `interpolate`, but the + /// output type must be `color`, and the interpolation is performed in the + /// CIELAB color space. + /// + /// Sdk Support: + /// basic functionality with js + static const interpolateLab = "interpolate-lab"; + + /// Returns mathematical constant ln(2). + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const ln2 = "ln2"; + + /// Returns the mathematical constant pi. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const pi = "pi"; + + /// Returns the mathematical constant e. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const e = "e"; + + /// Returns a string describing the type of the given value. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const typeof = "typeof"; + + /// Asserts that the input value is a string. If multiple values are + /// provided, each one is evaluated in order until a string is obtained. + /// If none of the inputs are strings, the expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const string = "string"; + + /// Asserts that the input value is a number. If multiple values are + /// provided, each one is evaluated in order until a number is obtained. + /// If none of the inputs are numbers, the expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const number = "number"; + + /// Asserts that the input value is a boolean. If multiple values are + /// provided, each one is evaluated in order until a boolean is obtained. + /// If none of the inputs are booleans, the expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const boolean = "boolean"; + + /// Asserts that the input value is an object. If multiple values are + /// provided, each one is evaluated in order until an object is obtained. + /// If none of the inputs are objects, the expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const object = "object"; + + /// Returns a `collator` for use in locale-dependent comparison + /// operations. The `case-sensitive` and `diacritic-sensitive` options + /// default to `false`. The `locale` argument specifies the IETF language + /// tag of the locale to use. If none is provided, the default locale is + /// used. If the requested locale is not available, the `collator` will + /// use a system-defined fallback locale. Use `resolved-locale` to test + /// the results of locale fallback behavior. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const collator = "collator"; + + /// Returns `formatted` text containing annotations for use in + /// mixed-format `text-field` entries. For a `text-field` entries of a + /// string type, following option object's properties are supported: If + /// set, the `text-font` value overrides the font specified by the root + /// layout properties. If set, the `font-scale` value specifies a scaling + /// factor relative to the `text-size` specified in the root layout + /// properties. If set, the `text-color` value overrides the color + /// specified by the root paint properties for this layer. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const format = "format"; + + /// Returns an `image` type for use in `icon-image`, `*-pattern` entries + /// and as a section in the `format` expression. If set, the `image` + /// argument will check that the requested image exists in the style and + /// will return either the resolved image name or `null`, depending on + /// whether or not the image is currently in the style. This validation + /// process is synchronous and requires the image to have been added to + /// the style before requesting it in the `image` argument. + /// + /// Sdk Support: + /// basic functionality with js, android, ios + static const image = "image"; + + /// Converts the input number into a string representation using the + /// providing formatting rules. If set, the `locale` argument specifies + /// the locale to use, as a BCP 47 language tag. If set, the `currency` + /// argument specifies an ISO 4217 code to use for currency-style + /// formatting. If set, the `min-fraction-digits` and + /// `max-fraction-digits` arguments specify the minimum and maximum number + /// of fractional digits to include. + /// + /// Sdk Support: + /// basic functionality with js + static const numberFormat = "number-format"; + + /// Converts the input value to a string. If the input is `null`, the + /// result is `""`. If the input is a boolean, the result is `"true"` or + /// `"false"`. If the input is a number, it is converted to a string as + /// specified by the ["NumberToString" + /// algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) + /// of the ECMAScript Language Specification. If the input is a color, it + /// is converted to a string of the form `"rgba(r,g,b,a)"`, where `r`, + /// `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 + /// to 1. Otherwise, the input is converted to a string in the format + /// specified by the + /// [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) + /// function of the ECMAScript Language Specification. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const toStringExpression = "to-string"; + + /// Converts the input value to a number, if possible. If the input is + /// `null` or `false`, the result is 0. If the input is `true`, the result + /// is 1. If the input is a string, it is converted to a number as + /// specified by the ["ToNumber Applied to the String Type" + /// algorithm](https://tc39.github.io/ecma262/#sec-tonumber-applied-to-the-string-type) + /// of the ECMAScript Language Specification. If multiple values are + /// provided, each one is evaluated in order until the first successful + /// conversion is obtained. If none of the inputs can be converted, the + /// expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const toNumber = "to-number"; + + /// Converts the input value to a boolean. The result is `false` when then + /// input is an empty string, 0, `false`, `null`, or `NaN`; otherwise it + /// is `true`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const toBoolean = "to-boolean"; + + /// Returns a four-element array containing the input color's red, green, + /// blue, and alpha components, in that order. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const toRgba = "to-rgba"; + + /// Converts the input value to a color. If multiple values are provided, + /// each one is evaluated in order until the first successful conversion + /// is obtained. If none of the inputs can be converted, the expression is + /// an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const toColor = "to-color"; + + /// Creates a color value from red, green, and blue components, which must + /// range between 0 and 255, and an alpha component of 1. If any component + /// is out of range, the expression is an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const rgb = "rgb"; + + /// Creates a color value from red, green, blue components, which must + /// range between 0 and 255, and an alpha component which must range + /// between 0 and 1. If any component is out of range, the expression is + /// an error. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const rgba = "rgba"; + + /// Retrieves a property value from the current feature's properties, or + /// from another object if a second argument is provided. Returns null if + /// the requested property is missing. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const get = "get"; + + /// Tests for the presence of an property value in the current feature's + /// properties, or from another object if a second argument is provided. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const has = "has"; + + /// Gets the length of an array or string. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const length = "length"; + + /// Gets the feature properties object. Note that in some cases, it may + /// be more efficient to use ["get", "property_name"] directly. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const properties = "properties"; + + /// Retrieves a property value from the current feature's state. Returns + /// null if the requested property is not present on the feature's state. + /// A feature's state is not part of the GeoJSON or vector tile data, and + /// must be set programmatically on each feature. Features are identified + /// by their `id` attribute, which must be an integer or a string that can + /// be cast to an integer. Note that ["feature-state"] can only be used + /// with paint properties that support data-driven styling. + /// + /// Sdk Support: + /// basic functionality with js + static const featureState = "feature-state"; + + /// Gets the feature's geometry type: Point, MultiPoint, LineString, + /// MultiLineString, Polygon, MultiPolygon. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const geometryType = "geometry-type"; + + /// Gets the feature's id, if it has one. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const id = "id"; + + /// Gets the current zoom level. Note that in style layout and paint + /// properties, ["zoom"] may only appear as the input to a top-level + /// "step" or "interpolate" expression. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const zoom = "zoom"; + + /// Gets the kernel density estimation of a pixel in a heatmap layer, + /// which is a relative measure of how many data points are crowded around + /// a particular pixel. Can only be used in the `heatmap-color` property. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const heatmapDensity = "heatmap-density"; + + /// Gets the progress along a gradient line. Can only be used in the + /// `line-gradient` property. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const lineProgress = "line-progress"; + + /// Gets the value of a cluster property accumulated so far. Can only be + /// used in the `clusterProperties` option of a clustered GeoJSON source. + /// + /// Sdk Support: + /// basic functionality with js + static const accumulated = "accumulated"; + + /// Returns the sum of the inputs. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const plus = "+"; + + /// Returns the product of the inputs. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const multiply = "*"; + + /// For two inputs, returns the result of subtracting the second input + /// from the first. For a single input, returns the result of subtracting + /// it from 0. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const minus = "-"; + + /// Returns the result of floating point division of the first input by + /// the second. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const divide = "/"; + + /// Returns the remainder after integer division of the first input by the + /// second. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const precent = "%"; + + /// Returns the result of raising the first input to the power specified + /// by the second. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const xor = "^"; + + /// Returns the square root of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const sqrt = "sqrt"; + + /// Returns the base-ten logarithm of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const log10 = "log10"; + + /// Returns the natural logarithm of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const ln = "ln"; + + /// Returns the base-two logarithm of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const log2 = "log2"; + + /// Returns the sine of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const sin = "sin"; + + /// Returns the cosine of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const cos = "cos"; + + /// Returns the tangent of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const tan = "tan"; + + /// Returns the arcsine of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const asin = "asin"; + + /// Returns the arccosine of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const acos = "acos"; + + /// Returns the arctangent of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const atan = "atan"; + + /// Returns the minimum value of the inputs. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const min = "min"; + + /// Returns the maximum value of the inputs. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const max = "max"; + + /// Rounds the input to the nearest integer. Halfway values are rounded + /// away from zero. For example, `["round", -1.5]` evaluates to -2. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const round = "round"; + + /// Returns the absolute value of the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const abs = "abs"; + + /// Returns the smallest integer that is greater than or equal to the + /// input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const ceil = "ceil"; + + /// Returns the largest integer that is less than or equal to the input. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const floor = "floor"; + + /// Returns `true` if the input values are equal, `false` otherwise. The + /// comparison is strictly typed: values of different runtime types are + /// always considered unequal. Cases where the types are known to be + /// different at parse time are considered invalid and will produce a + /// parse error. Accepts an optional `collator` argument to control + /// locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const equal = "=="; + + /// Returns `true` if the input values are not equal, `false` otherwise. + /// The comparison is strictly typed: values of different runtime types + /// are always considered unequal. Cases where the types are known to be + /// different at parse time are considered invalid and will produce a + /// parse error. Accepts an optional `collator` argument to control + /// locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const notEqual = "!="; + + /// Returns `true` if the first input is strictly greater than the second, + /// `false` otherwise. The arguments are required to be either both + /// strings or both numbers; if during evaluation they are not, expression + /// evaluation produces an error. Cases where this constraint is known not + /// to hold at parse time are considered in valid and will produce a parse + /// error. Accepts an optional `collator` argument to control + /// locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const larger = ">"; + + /// Returns `true` if the first input is strictly less than the second, + /// `false` otherwise. The arguments are required to be either both + /// strings or both numbers; if during evaluation they are not, expression + /// evaluation produces an error. Cases where this constraint is known not + /// to hold at parse time are considered in valid and will produce a parse + /// error. Accepts an optional `collator` argument to control + /// locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const smaller = "<"; + + /// Returns `true` if the first input is greater than or equal to the + /// second, `false` otherwise. The arguments are required to be either + /// both strings or both numbers; if during evaluation they are not, + /// expression evaluation produces an error. Cases where this constraint + /// is known not to hold at parse time are considered in valid and will + /// produce a parse error. Accepts an optional `collator` argument to + /// control locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const largerOrEqual = ">="; + + /// Returns `true` if the first input is less than or equal to the second, + /// `false` otherwise. The arguments are required to be either both + /// strings or both numbers; if during evaluation they are not, expression + /// evaluation produces an error. Cases where this constraint is known not + /// to hold at parse time are considered in valid and will produce a parse + /// error. Accepts an optional `collator` argument to control + /// locale-dependent string comparisons. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const smallerOrEqual = "<="; + + /// Returns `true` if all the inputs are `true`, `false` otherwise. The + /// inputs are evaluated in order, and evaluation is short-circuiting: + /// once an input expression evaluates to `false`, the result is `false` + /// and no further input expressions are evaluated. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const all = "all"; + + /// Returns `true` if any of the inputs are `true`, `false` otherwise. The + /// inputs are evaluated in order, and evaluation is short-circuiting: + /// once an input expression evaluates to `true`, the result is `true` and + /// no further input expressions are evaluated. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const any = "any"; + + /// Logical negation. Returns `true` if the input is `false`, and `false` + /// if the input is `true`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const not = "!"; + + /// Returns `true` if the input string is expected to render legibly. + /// Returns `false` if the input string contains sections that cannot be + /// rendered without potential loss of meaning (e.g. Indic scripts that + /// require complex text shaping, or right-to-left scripts if the the + /// `mapbox-gl-rtl-text` plugin is not in use in Mapbox GL JS). + /// + /// Sdk Support: + /// basic functionality with js, android + static const isSupportedScript = "is-supported-script"; + + /// Returns the input string converted to uppercase. Follows the Unicode + /// Default Case Conversion algorithm and the locale-insensitive case + /// mappings in the Unicode Character Database. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const upcase = "upcase"; + + /// Returns the input string converted to lowercase. Follows the Unicode + /// Default Case Conversion algorithm and the locale-insensitive case + /// mappings in the Unicode Character Database. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const downcase = "downcase"; + + /// Returns a `string` consisting of the concatenation of the inputs. Each + /// input is converted to a string as if by `to-string`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const concat = "concat"; + + /// Returns the IETF language tag of the locale being used by the provided + /// `collator`. This can be used to determine the default system locale, + /// or to determine if a requested locale was successfully loaded. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + static const resolvedLocale = "resolved-locale"; +} diff --git a/lib/src/layer_properties.dart b/lib/src/layer_properties.dart new file mode 100644 index 000000000..8624c23d9 --- /dev/null +++ b/lib/src/layer_properties.dart @@ -0,0 +1,2380 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl; + +abstract class LayerProperties { + Map toJson(); +} + +class SymbolLayerProperties implements LayerProperties { + // Paint Properties + /// The opacity at which the icon will be drawn. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconOpacity; + + /// The color of the icon. This can only be used with sdf icons. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconColor; + + /// The color of the icon's halo. Icon halos can only be used with SDF + /// icons. + /// + /// Type: color + /// default: rgba(0, 0, 0, 0) + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconHaloColor; + + /// Distance of halo to the icon outline. + /// + /// 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 iconHaloWidth; + + /// Fade out the halo towards the outside. + /// + /// 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 iconHaloBlur; + + /// Distance that the icon's anchor is moved from its original placement. + /// Positive values indicate right and down, while negative values + /// indicate left and up. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconTranslate; + + /// Controls the frame of reference for `icon-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// Icons are translated relative to the map. + /// "viewport" + /// Icons are translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconTranslateAnchor; + + /// The opacity at which the text will be drawn. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textOpacity; + + /// The color with which the text will be drawn. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textColor; + + /// The color of the text's halo, which helps it stand out from + /// backgrounds. + /// + /// Type: color + /// default: rgba(0, 0, 0, 0) + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textHaloColor; + + /// Distance of halo to the font outline. Max text halo width is 1/4 of + /// the font-size. + /// + /// 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 textHaloWidth; + + /// The halo's fadeout distance towards the outside. + /// + /// 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 textHaloBlur; + + /// Distance that the text's anchor is moved from its original placement. + /// Positive values indicate right and down, while negative values + /// indicate left and up. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textTranslate; + + /// Controls the frame of reference for `text-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The text is translated relative to the map. + /// "viewport" + /// The text is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textTranslateAnchor; + + // Layout Properties + /// Label placement relative to its geometry. + /// + /// Type: enum + /// default: point + /// Options: + /// "point" + /// The label is placed at the point where the geometry is located. + /// "line" + /// The label is placed along the line of the geometry. Can only be + /// used on `LineString` and `Polygon` geometries. + /// "line-center" + /// The label is placed at the center of the line of the geometry. + /// Can only be used on `LineString` and `Polygon` geometries. Note + /// that a single feature in a vector tile may contain multiple line + /// geometries. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic symbolPlacement; + + /// Distance between two symbol anchors. + /// + /// Type: number + /// default: 250 + /// minimum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic symbolSpacing; + + /// If true, the symbols will not cross tile edges to avoid mutual + /// collisions. Recommended in layers that don't have enough padding in + /// the vector tile to prevent collisions, or if it is a point symbol + /// layer placed after a line symbol layer. When using a client that + /// supports global collision detection, like Mapbox GL JS version 0.42.0 + /// or greater, enabling this property is not needed to prevent clipped + /// labels at tile boundaries. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic symbolAvoidEdges; + + /// Sorts features in ascending order based on this value. Features with + /// lower sort keys are drawn and placed first. When `icon-allow-overlap` + /// or `text-allow-overlap` is `false`, features with a lower sort key + /// will have priority during placement. When `icon-allow-overlap` or + /// `text-allow-overlap` is set to `true`, features with a higher sort key + /// will overlap over features with a lower sort key. + /// + /// Type: number + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic symbolSortKey; + + /// Controls the order in which overlapping symbols in the same layer are + /// rendered + /// + /// Type: enum + /// default: auto + /// Options: + /// "auto" + /// If `symbol-sort-key` is set, sort based on that. Otherwise sort + /// symbols by their y-position relative to the viewport. + /// "viewport-y" + /// Symbols will be sorted by their y-position relative to the + /// viewport. + /// "source" + /// Symbols will be rendered in the same order as the source data + /// with no sorting applied. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic symbolZOrder; + + /// If true, the icon will be visible even if it collides with other + /// previously drawn symbols. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconAllowOverlap; + + /// If true, other symbols can be visible even if they collide with the + /// icon. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconIgnorePlacement; + + /// If true, text will display without their corresponding icons when the + /// icon collides with other symbols and the text does not. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconOptional; + + /// In combination with `symbol-placement`, determines the rotation + /// behavior of icons. + /// + /// Type: enum + /// default: auto + /// Options: + /// "map" + /// When `symbol-placement` is set to `point`, aligns icons + /// east-west. When `symbol-placement` is set to `line` or + /// `line-center`, aligns icon x-axes with the line. + /// "viewport" + /// Produces icons whose x-axes are aligned with the x-axis of the + /// viewport, regardless of the value of `symbol-placement`. + /// "auto" + /// When `symbol-placement` is set to `point`, this is equivalent to + /// `viewport`. When `symbol-placement` is set to `line` or + /// `line-center`, this is equivalent to `map`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconRotationAlignment; + + /// Scales the original size of the icon by the provided factor. The new + /// pixel size of the image will be the original pixel size multiplied by + /// `icon-size`. 1 is the original size; 3 triples the size of the image. + /// + /// 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 iconSize; + + /// Scales the icon to fit around the associated text. + /// + /// Type: enum + /// default: none + /// Options: + /// "none" + /// The icon is displayed at its intrinsic aspect ratio. + /// "width" + /// The icon is scaled in the x-dimension to fit the width of the + /// text. + /// "height" + /// The icon is scaled in the y-dimension to fit the height of the + /// text. + /// "both" + /// The icon is scaled in both x- and y-dimensions. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconTextFit; + + /// Size of the additional area added to dimensions determined by + /// `icon-text-fit`, in clockwise order: top, right, bottom, left. + /// + /// Type: array + /// default: [0, 0, 0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconTextFitPadding; + + /// Name of image in sprite to use for drawing an image background. + /// + /// Type: resolvedImage + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconImage; + + /// Rotates the icon clockwise. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconRotate; + + /// Size of the additional area around the icon bounding box used for + /// detecting symbol collisions. + /// + /// Type: number + /// default: 2 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconPadding; + + /// If true, the icon may be flipped to prevent it from being rendered + /// upside-down. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconKeepUpright; + + /// Offset distance of icon from its anchor. Positive values indicate + /// right and down, while negative values indicate left and up. Each + /// component is multiplied by the value of `icon-size` to obtain the + /// final offset in pixels. When combined with `icon-rotate` the offset + /// will be as if the rotated direction was up. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconOffset; + + /// Part of the icon placed closest to the anchor. + /// + /// Type: enum + /// default: center + /// Options: + /// "center" + /// The center of the icon is placed closest to the anchor. + /// "left" + /// The left side of the icon is placed closest to the anchor. + /// "right" + /// The right side of the icon is placed closest to the anchor. + /// "top" + /// The top of the icon is placed closest to the anchor. + /// "bottom" + /// The bottom of the icon is placed closest to the anchor. + /// "top-left" + /// The top left corner of the icon is placed closest to the anchor. + /// "top-right" + /// The top right corner of the icon is placed closest to the anchor. + /// "bottom-left" + /// The bottom left corner of the icon is placed closest to the + /// anchor. + /// "bottom-right" + /// The bottom right corner of the icon is placed closest to the + /// anchor. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic iconAnchor; + + /// Orientation of icon when map is pitched. + /// + /// Type: enum + /// default: auto + /// Options: + /// "map" + /// The icon is aligned to the plane of the map. + /// "viewport" + /// The icon is aligned to the plane of the viewport. + /// "auto" + /// Automatically matches the value of `icon-rotation-alignment`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic iconPitchAlignment; + + /// Orientation of text when map is pitched. + /// + /// Type: enum + /// default: auto + /// Options: + /// "map" + /// The text is aligned to the plane of the map. + /// "viewport" + /// The text is aligned to the plane of the viewport. + /// "auto" + /// Automatically matches the value of `text-rotation-alignment`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textPitchAlignment; + + /// In combination with `symbol-placement`, determines the rotation + /// behavior of the individual glyphs forming the text. + /// + /// Type: enum + /// default: auto + /// Options: + /// "map" + /// When `symbol-placement` is set to `point`, aligns text east-west. + /// When `symbol-placement` is set to `line` or `line-center`, aligns + /// text x-axes with the line. + /// "viewport" + /// Produces glyphs whose x-axes are aligned with the x-axis of the + /// viewport, regardless of the value of `symbol-placement`. + /// "auto" + /// When `symbol-placement` is set to `point`, this is equivalent to + /// `viewport`. When `symbol-placement` is set to `line` or + /// `line-center`, this is equivalent to `map`. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textRotationAlignment; + + /// Value to use for a text label. If a plain `string` is provided, it + /// will be treated as a `formatted` with default/inherited formatting + /// options. + /// + /// Type: formatted + /// default: + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textField; + + /// Font stack to use for displaying text. + /// + /// Type: array + /// default: [Open Sans Regular, Arial Unicode MS Regular] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textFont; + + /// Font size. + /// + /// Type: number + /// default: 16 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textSize; + + /// The maximum line width for text wrapping. + /// + /// Type: number + /// default: 10 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textMaxWidth; + + /// Text leading value for multi-line text. + /// + /// Type: number + /// default: 1.2 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textLineHeight; + + /// Text tracking amount. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textLetterSpacing; + + /// Text justification options. + /// + /// Type: enum + /// default: center + /// Options: + /// "auto" + /// The text is aligned towards the anchor position. + /// "left" + /// The text is aligned to the left. + /// "center" + /// The text is centered. + /// "right" + /// The text is aligned to the right. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textJustify; + + /// Radial offset of text, in the direction of the symbol's anchor. Useful + /// in combination with `text-variable-anchor`, which defaults to using + /// the two-dimensional `text-offset` if present. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textRadialOffset; + + /// To increase the chance of placing high-priority labels on the map, you + /// can provide an array of `text-anchor` locations: the renderer will + /// attempt to place the label at each location, in order, before moving + /// onto the next label. Use `text-justify: auto` to choose justification + /// based on anchor position. To apply an offset, use the + /// `text-radial-offset` or the two-dimensional `text-offset`. + /// + /// Type: array + /// Options: + /// "center" + /// The center of the text is placed closest to the anchor. + /// "left" + /// The left side of the text is placed closest to the anchor. + /// "right" + /// The right side of the text is placed closest to the anchor. + /// "top" + /// The top of the text is placed closest to the anchor. + /// "bottom" + /// The bottom of the text is placed closest to the anchor. + /// "top-left" + /// The top left corner of the text is placed closest to the anchor. + /// "top-right" + /// The top right corner of the text is placed closest to the anchor. + /// "bottom-left" + /// The bottom left corner of the text is placed closest to the + /// anchor. + /// "bottom-right" + /// The bottom right corner of the text is placed closest to the + /// anchor. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textVariableAnchor; + + /// Part of the text placed closest to the anchor. + /// + /// Type: enum + /// default: center + /// Options: + /// "center" + /// The center of the text is placed closest to the anchor. + /// "left" + /// The left side of the text is placed closest to the anchor. + /// "right" + /// The right side of the text is placed closest to the anchor. + /// "top" + /// The top of the text is placed closest to the anchor. + /// "bottom" + /// The bottom of the text is placed closest to the anchor. + /// "top-left" + /// The top left corner of the text is placed closest to the anchor. + /// "top-right" + /// The top right corner of the text is placed closest to the anchor. + /// "bottom-left" + /// The bottom left corner of the text is placed closest to the + /// anchor. + /// "bottom-right" + /// The bottom right corner of the text is placed closest to the + /// anchor. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textAnchor; + + /// Maximum angle change between adjacent characters. + /// + /// Type: number + /// default: 45 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textMaxAngle; + + /// The property allows control over a symbol's orientation. Note that the + /// property values act as a hint, so that a symbol whose language doesn’t + /// support the provided orientation will be laid out in its natural + /// orientation. Example: English point symbol will be rendered + /// horizontally even if array value contains single 'vertical' enum + /// value. The order of elements in an array define priority order for the + /// placement of an orientation variant. + /// + /// Type: array + /// Options: + /// "horizontal" + /// If a text's language supports horizontal writing mode, symbols + /// with point placement would be laid out horizontally. + /// "vertical" + /// If a text's language supports vertical writing mode, symbols with + /// point placement would be laid out vertically. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textWritingMode; + + /// Rotates the text clockwise. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textRotate; + + /// Size of the additional area around the text bounding box used for + /// detecting symbol collisions. + /// + /// Type: number + /// default: 2 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textPadding; + + /// If true, the text may be flipped vertically to prevent it from being + /// rendered upside-down. + /// + /// Type: boolean + /// default: true + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textKeepUpright; + + /// Specifies how to capitalize text, similar to the CSS `text-transform` + /// property. + /// + /// Type: enum + /// default: none + /// Options: + /// "none" + /// The text is not altered. + /// "uppercase" + /// Forces all letters to be displayed in uppercase. + /// "lowercase" + /// Forces all letters to be displayed in lowercase. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textTransform; + + /// Offset distance of text from its anchor. Positive values indicate + /// right and down, while negative values indicate left and up. If used + /// with text-variable-anchor, input values will be taken as absolute + /// values. Offsets along the x- and y-axis will be applied automatically + /// based on the anchor position. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic textOffset; + + /// If true, the text will be visible even if it collides with other + /// previously drawn symbols. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textAllowOverlap; + + /// If true, other symbols can be visible even if they collide with the + /// text. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textIgnorePlacement; + + /// If true, icons will display without their corresponding text when the + /// text collides with other symbols and the icon does not. + /// + /// Type: boolean + /// default: false + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic textOptional; + + /// 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 SymbolLayerProperties({ + this.iconOpacity, + this.iconColor, + this.iconHaloColor, + this.iconHaloWidth, + this.iconHaloBlur, + this.iconTranslate, + this.iconTranslateAnchor, + this.textOpacity, + this.textColor, + this.textHaloColor, + this.textHaloWidth, + this.textHaloBlur, + this.textTranslate, + this.textTranslateAnchor, + this.symbolPlacement, + this.symbolSpacing, + this.symbolAvoidEdges, + this.symbolSortKey, + this.symbolZOrder, + this.iconAllowOverlap, + this.iconIgnorePlacement, + this.iconOptional, + this.iconRotationAlignment, + this.iconSize, + this.iconTextFit, + this.iconTextFitPadding, + this.iconImage, + this.iconRotate, + this.iconPadding, + this.iconKeepUpright, + this.iconOffset, + this.iconAnchor, + this.iconPitchAlignment, + this.textPitchAlignment, + this.textRotationAlignment, + this.textField, + this.textFont, + this.textSize, + this.textMaxWidth, + this.textLineHeight, + this.textLetterSpacing, + this.textJustify, + this.textRadialOffset, + this.textVariableAnchor, + this.textAnchor, + this.textMaxAngle, + this.textWritingMode, + this.textRotate, + this.textPadding, + this.textKeepUpright, + this.textTransform, + this.textOffset, + this.textAllowOverlap, + this.textIgnorePlacement, + this.textOptional, + this.visibility, + }); + + SymbolLayerProperties copyWith(SymbolLayerProperties changes) { + return SymbolLayerProperties( + iconOpacity: changes.iconOpacity ?? iconOpacity, + iconColor: changes.iconColor ?? iconColor, + iconHaloColor: changes.iconHaloColor ?? iconHaloColor, + iconHaloWidth: changes.iconHaloWidth ?? iconHaloWidth, + iconHaloBlur: changes.iconHaloBlur ?? iconHaloBlur, + iconTranslate: changes.iconTranslate ?? iconTranslate, + iconTranslateAnchor: changes.iconTranslateAnchor ?? iconTranslateAnchor, + textOpacity: changes.textOpacity ?? textOpacity, + textColor: changes.textColor ?? textColor, + textHaloColor: changes.textHaloColor ?? textHaloColor, + textHaloWidth: changes.textHaloWidth ?? textHaloWidth, + textHaloBlur: changes.textHaloBlur ?? textHaloBlur, + textTranslate: changes.textTranslate ?? textTranslate, + textTranslateAnchor: changes.textTranslateAnchor ?? textTranslateAnchor, + symbolPlacement: changes.symbolPlacement ?? symbolPlacement, + symbolSpacing: changes.symbolSpacing ?? symbolSpacing, + symbolAvoidEdges: changes.symbolAvoidEdges ?? symbolAvoidEdges, + symbolSortKey: changes.symbolSortKey ?? symbolSortKey, + symbolZOrder: changes.symbolZOrder ?? symbolZOrder, + iconAllowOverlap: changes.iconAllowOverlap ?? iconAllowOverlap, + iconIgnorePlacement: changes.iconIgnorePlacement ?? iconIgnorePlacement, + iconOptional: changes.iconOptional ?? iconOptional, + iconRotationAlignment: + changes.iconRotationAlignment ?? iconRotationAlignment, + iconSize: changes.iconSize ?? iconSize, + iconTextFit: changes.iconTextFit ?? iconTextFit, + iconTextFitPadding: changes.iconTextFitPadding ?? iconTextFitPadding, + iconImage: changes.iconImage ?? iconImage, + iconRotate: changes.iconRotate ?? iconRotate, + iconPadding: changes.iconPadding ?? iconPadding, + iconKeepUpright: changes.iconKeepUpright ?? iconKeepUpright, + iconOffset: changes.iconOffset ?? iconOffset, + iconAnchor: changes.iconAnchor ?? iconAnchor, + iconPitchAlignment: changes.iconPitchAlignment ?? iconPitchAlignment, + textPitchAlignment: changes.textPitchAlignment ?? textPitchAlignment, + textRotationAlignment: + changes.textRotationAlignment ?? textRotationAlignment, + textField: changes.textField ?? textField, + textFont: changes.textFont ?? textFont, + textSize: changes.textSize ?? textSize, + textMaxWidth: changes.textMaxWidth ?? textMaxWidth, + textLineHeight: changes.textLineHeight ?? textLineHeight, + textLetterSpacing: changes.textLetterSpacing ?? textLetterSpacing, + textJustify: changes.textJustify ?? textJustify, + textRadialOffset: changes.textRadialOffset ?? textRadialOffset, + textVariableAnchor: changes.textVariableAnchor ?? textVariableAnchor, + textAnchor: changes.textAnchor ?? textAnchor, + textMaxAngle: changes.textMaxAngle ?? textMaxAngle, + textWritingMode: changes.textWritingMode ?? textWritingMode, + textRotate: changes.textRotate ?? textRotate, + textPadding: changes.textPadding ?? textPadding, + textKeepUpright: changes.textKeepUpright ?? textKeepUpright, + textTransform: changes.textTransform ?? textTransform, + textOffset: changes.textOffset ?? textOffset, + textAllowOverlap: changes.textAllowOverlap ?? textAllowOverlap, + textIgnorePlacement: changes.textIgnorePlacement ?? textIgnorePlacement, + textOptional: changes.textOptional ?? textOptional, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('icon-opacity', iconOpacity); + addIfPresent('icon-color', iconColor); + addIfPresent('icon-halo-color', iconHaloColor); + addIfPresent('icon-halo-width', iconHaloWidth); + addIfPresent('icon-halo-blur', iconHaloBlur); + addIfPresent('icon-translate', iconTranslate); + addIfPresent('icon-translate-anchor', iconTranslateAnchor); + addIfPresent('text-opacity', textOpacity); + addIfPresent('text-color', textColor); + addIfPresent('text-halo-color', textHaloColor); + addIfPresent('text-halo-width', textHaloWidth); + addIfPresent('text-halo-blur', textHaloBlur); + addIfPresent('text-translate', textTranslate); + addIfPresent('text-translate-anchor', textTranslateAnchor); + addIfPresent('symbol-placement', symbolPlacement); + addIfPresent('symbol-spacing', symbolSpacing); + addIfPresent('symbol-avoid-edges', symbolAvoidEdges); + addIfPresent('symbol-sort-key', symbolSortKey); + addIfPresent('symbol-z-order', symbolZOrder); + addIfPresent('icon-allow-overlap', iconAllowOverlap); + addIfPresent('icon-ignore-placement', iconIgnorePlacement); + addIfPresent('icon-optional', iconOptional); + addIfPresent('icon-rotation-alignment', iconRotationAlignment); + addIfPresent('icon-size', iconSize); + addIfPresent('icon-text-fit', iconTextFit); + addIfPresent('icon-text-fit-padding', iconTextFitPadding); + addIfPresent('icon-image', iconImage); + addIfPresent('icon-rotate', iconRotate); + addIfPresent('icon-padding', iconPadding); + addIfPresent('icon-keep-upright', iconKeepUpright); + addIfPresent('icon-offset', iconOffset); + addIfPresent('icon-anchor', iconAnchor); + addIfPresent('icon-pitch-alignment', iconPitchAlignment); + addIfPresent('text-pitch-alignment', textPitchAlignment); + addIfPresent('text-rotation-alignment', textRotationAlignment); + addIfPresent('text-field', textField); + addIfPresent('text-font', textFont); + addIfPresent('text-size', textSize); + addIfPresent('text-max-width', textMaxWidth); + addIfPresent('text-line-height', textLineHeight); + addIfPresent('text-letter-spacing', textLetterSpacing); + addIfPresent('text-justify', textJustify); + addIfPresent('text-radial-offset', textRadialOffset); + addIfPresent('text-variable-anchor', textVariableAnchor); + addIfPresent('text-anchor', textAnchor); + addIfPresent('text-max-angle', textMaxAngle); + addIfPresent('text-writing-mode', textWritingMode); + addIfPresent('text-rotate', textRotate); + addIfPresent('text-padding', textPadding); + addIfPresent('text-keep-upright', textKeepUpright); + addIfPresent('text-transform', textTransform); + addIfPresent('text-offset', textOffset); + addIfPresent('text-allow-overlap', textAllowOverlap); + addIfPresent('text-ignore-placement', textIgnorePlacement); + addIfPresent('text-optional', textOptional); + addIfPresent('visibility', visibility); + return json; + } + + factory SymbolLayerProperties.fromJson(Map json) { + return SymbolLayerProperties( + iconOpacity: json['icon-opacity'], + iconColor: json['icon-color'], + iconHaloColor: json['icon-halo-color'], + iconHaloWidth: json['icon-halo-width'], + iconHaloBlur: json['icon-halo-blur'], + iconTranslate: json['icon-translate'], + iconTranslateAnchor: json['icon-translate-anchor'], + textOpacity: json['text-opacity'], + textColor: json['text-color'], + textHaloColor: json['text-halo-color'], + textHaloWidth: json['text-halo-width'], + textHaloBlur: json['text-halo-blur'], + textTranslate: json['text-translate'], + textTranslateAnchor: json['text-translate-anchor'], + symbolPlacement: json['symbol-placement'], + symbolSpacing: json['symbol-spacing'], + symbolAvoidEdges: json['symbol-avoid-edges'], + symbolSortKey: json['symbol-sort-key'], + symbolZOrder: json['symbol-z-order'], + iconAllowOverlap: json['icon-allow-overlap'], + iconIgnorePlacement: json['icon-ignore-placement'], + iconOptional: json['icon-optional'], + iconRotationAlignment: json['icon-rotation-alignment'], + iconSize: json['icon-size'], + iconTextFit: json['icon-text-fit'], + iconTextFitPadding: json['icon-text-fit-padding'], + iconImage: json['icon-image'], + iconRotate: json['icon-rotate'], + iconPadding: json['icon-padding'], + iconKeepUpright: json['icon-keep-upright'], + iconOffset: json['icon-offset'], + iconAnchor: json['icon-anchor'], + iconPitchAlignment: json['icon-pitch-alignment'], + textPitchAlignment: json['text-pitch-alignment'], + textRotationAlignment: json['text-rotation-alignment'], + textField: json['text-field'], + textFont: json['text-font'], + textSize: json['text-size'], + textMaxWidth: json['text-max-width'], + textLineHeight: json['text-line-height'], + textLetterSpacing: json['text-letter-spacing'], + textJustify: json['text-justify'], + textRadialOffset: json['text-radial-offset'], + textVariableAnchor: json['text-variable-anchor'], + textAnchor: json['text-anchor'], + textMaxAngle: json['text-max-angle'], + textWritingMode: json['text-writing-mode'], + textRotate: json['text-rotate'], + textPadding: json['text-padding'], + textKeepUpright: json['text-keep-upright'], + textTransform: json['text-transform'], + textOffset: json['text-offset'], + textAllowOverlap: json['text-allow-overlap'], + textIgnorePlacement: json['text-ignore-placement'], + textOptional: json['text-optional'], + visibility: json['visibility'], + ); + } +} + +class CircleLayerProperties implements LayerProperties { + // Paint Properties + /// Circle radius. + /// + /// Type: number + /// default: 5 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleRadius; + + /// The fill color of the circle. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleColor; + + /// Amount to blur the circle. 1 blurs the circle such that only the + /// centerpoint is full opacity. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleBlur; + + /// The opacity at which the circle will be drawn. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleOpacity; + + /// The geometry's offset. Values are [x, y] where negatives indicate left + /// and up, respectively. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic circleTranslate; + + /// Controls the frame of reference for `circle-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The circle is translated relative to the map. + /// "viewport" + /// The circle is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic circleTranslateAnchor; + + /// Controls the scaling behavior of the circle when the map is pitched. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// Circles are scaled according to their apparent distance to the + /// camera. + /// "viewport" + /// Circles are not scaled. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic circlePitchScale; + + /// Orientation of circle when map is pitched. + /// + /// Type: enum + /// default: viewport + /// Options: + /// "map" + /// The circle is aligned to the plane of the map. + /// "viewport" + /// The circle is aligned to the plane of the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic circlePitchAlignment; + + /// The width of the circle's stroke. Strokes are placed outside of the + /// `circle-radius`. + /// + /// 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 circleStrokeWidth; + + /// The stroke color of the circle. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleStrokeColor; + + /// The opacity of the circle's stroke. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic circleStrokeOpacity; + + // Layout Properties + /// Sorts features in ascending order based on this value. Features with a + /// higher sort key will appear above features with a lower sort key. + /// + /// Type: number + /// + /// Sdk Support: + /// basic functionality with js + /// data-driven styling with js + final dynamic circleSortKey; + + /// 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 CircleLayerProperties({ + this.circleRadius, + this.circleColor, + this.circleBlur, + this.circleOpacity, + this.circleTranslate, + this.circleTranslateAnchor, + this.circlePitchScale, + this.circlePitchAlignment, + this.circleStrokeWidth, + this.circleStrokeColor, + this.circleStrokeOpacity, + this.circleSortKey, + this.visibility, + }); + + CircleLayerProperties copyWith(CircleLayerProperties changes) { + return CircleLayerProperties( + circleRadius: changes.circleRadius ?? circleRadius, + circleColor: changes.circleColor ?? circleColor, + circleBlur: changes.circleBlur ?? circleBlur, + circleOpacity: changes.circleOpacity ?? circleOpacity, + circleTranslate: changes.circleTranslate ?? circleTranslate, + circleTranslateAnchor: + changes.circleTranslateAnchor ?? circleTranslateAnchor, + circlePitchScale: changes.circlePitchScale ?? circlePitchScale, + circlePitchAlignment: + changes.circlePitchAlignment ?? circlePitchAlignment, + circleStrokeWidth: changes.circleStrokeWidth ?? circleStrokeWidth, + circleStrokeColor: changes.circleStrokeColor ?? circleStrokeColor, + circleStrokeOpacity: changes.circleStrokeOpacity ?? circleStrokeOpacity, + circleSortKey: changes.circleSortKey ?? circleSortKey, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('circle-radius', circleRadius); + addIfPresent('circle-color', circleColor); + addIfPresent('circle-blur', circleBlur); + addIfPresent('circle-opacity', circleOpacity); + addIfPresent('circle-translate', circleTranslate); + addIfPresent('circle-translate-anchor', circleTranslateAnchor); + addIfPresent('circle-pitch-scale', circlePitchScale); + addIfPresent('circle-pitch-alignment', circlePitchAlignment); + addIfPresent('circle-stroke-width', circleStrokeWidth); + addIfPresent('circle-stroke-color', circleStrokeColor); + addIfPresent('circle-stroke-opacity', circleStrokeOpacity); + addIfPresent('circle-sort-key', circleSortKey); + addIfPresent('visibility', visibility); + return json; + } + + factory CircleLayerProperties.fromJson(Map json) { + return CircleLayerProperties( + circleRadius: json['circle-radius'], + circleColor: json['circle-color'], + circleBlur: json['circle-blur'], + circleOpacity: json['circle-opacity'], + circleTranslate: json['circle-translate'], + circleTranslateAnchor: json['circle-translate-anchor'], + circlePitchScale: json['circle-pitch-scale'], + circlePitchAlignment: json['circle-pitch-alignment'], + circleStrokeWidth: json['circle-stroke-width'], + circleStrokeColor: json['circle-stroke-color'], + circleStrokeOpacity: json['circle-stroke-opacity'], + circleSortKey: json['circle-sort-key'], + visibility: json['visibility'], + ); + } +} + +class LineLayerProperties implements LayerProperties { + // Paint Properties + /// The opacity at which the line will be drawn. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic lineOpacity; + + /// The color with which the line will be drawn. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic lineColor; + + /// The geometry's offset. Values are [x, y] where negatives indicate left + /// and up, respectively. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineTranslate; + + /// Controls the frame of reference for `line-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The line is translated relative to the map. + /// "viewport" + /// The line is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineTranslateAnchor; + + /// Stroke thickness. + /// + /// 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 lineWidth; + + /// Draws a line casing outside of a line's actual path. Value indicates + /// the width of the inner gap. + /// + /// 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 lineGapWidth; + + /// The line's offset. For linear features, a positive value offsets the + /// line to the right, relative to the direction of the line, and a + /// negative value to the left. For polygon features, a positive value + /// results in an inset, and a negative value results in an outset. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic lineOffset; + + /// Blur applied to the line, in pixels. + /// + /// 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 lineBlur; + + /// Specifies the lengths of the alternating dashes and gaps that form the + /// dash pattern. The lengths are later scaled by the line width. To + /// convert a dash length to pixels, multiply the length by the current + /// line width. Note that GeoJSON sources with `lineMetrics: true` + /// specified won't render dashed lines to the expected scale. Also note + /// that zoom-dependent expressions will be evaluated only at integer zoom + /// levels. + /// + /// Type: array + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineDasharray; + + /// Name of image in sprite to use for drawing image lines. For seamless + /// patterns, image width 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 linePattern; + + /// Defines a gradient with which to color a line feature. Can only be + /// used with GeoJSON sources that specify `"lineMetrics": true`. + /// + /// Type: color + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineGradient; + + // Layout Properties + /// The display of line endings. + /// + /// Type: enum + /// default: butt + /// Options: + /// "butt" + /// A cap with a squared-off end which is drawn to the exact endpoint + /// of the line. + /// "round" + /// A cap with a rounded end which is drawn beyond the endpoint of + /// the line at a radius of one-half of the line's width and centered + /// on the endpoint of the line. + /// "square" + /// A cap with a squared-off end which is drawn beyond the endpoint + /// of the line at a distance of one-half of the line's width. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineCap; + + /// The display of lines when joining. + /// + /// Type: enum + /// default: miter + /// Options: + /// "bevel" + /// A join with a squared-off end which is drawn beyond the endpoint + /// of the line at a distance of one-half of the line's width. + /// "round" + /// A join with a rounded end which is drawn beyond the endpoint of + /// the line at a radius of one-half of the line's width and centered + /// on the endpoint of the line. + /// "miter" + /// A join with a sharp, angled corner which is drawn with the outer + /// sides beyond the endpoint of the path until they meet. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic lineJoin; + + /// Used to automatically convert miter joins to bevel joins for sharp + /// angles. + /// + /// Type: number + /// default: 2 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineMiterLimit; + + /// Used to automatically convert round joins to miter joins for shallow + /// angles. + /// + /// Type: number + /// default: 1.05 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic lineRoundLimit; + + /// Sorts features in ascending order based on this value. Features with a + /// higher sort key will appear above features with a lower sort key. + /// + /// Type: number + /// + /// Sdk Support: + /// basic functionality with js + /// data-driven styling with js + final dynamic lineSortKey; + + /// 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 LineLayerProperties({ + this.lineOpacity, + this.lineColor, + this.lineTranslate, + this.lineTranslateAnchor, + this.lineWidth, + this.lineGapWidth, + this.lineOffset, + this.lineBlur, + this.lineDasharray, + this.linePattern, + this.lineGradient, + this.lineCap, + this.lineJoin, + this.lineMiterLimit, + this.lineRoundLimit, + this.lineSortKey, + this.visibility, + }); + + LineLayerProperties copyWith(LineLayerProperties changes) { + return LineLayerProperties( + lineOpacity: changes.lineOpacity ?? lineOpacity, + lineColor: changes.lineColor ?? lineColor, + lineTranslate: changes.lineTranslate ?? lineTranslate, + lineTranslateAnchor: changes.lineTranslateAnchor ?? lineTranslateAnchor, + lineWidth: changes.lineWidth ?? lineWidth, + lineGapWidth: changes.lineGapWidth ?? lineGapWidth, + lineOffset: changes.lineOffset ?? lineOffset, + lineBlur: changes.lineBlur ?? lineBlur, + lineDasharray: changes.lineDasharray ?? lineDasharray, + linePattern: changes.linePattern ?? linePattern, + lineGradient: changes.lineGradient ?? lineGradient, + lineCap: changes.lineCap ?? lineCap, + lineJoin: changes.lineJoin ?? lineJoin, + lineMiterLimit: changes.lineMiterLimit ?? lineMiterLimit, + lineRoundLimit: changes.lineRoundLimit ?? lineRoundLimit, + lineSortKey: changes.lineSortKey ?? lineSortKey, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('line-opacity', lineOpacity); + addIfPresent('line-color', lineColor); + addIfPresent('line-translate', lineTranslate); + addIfPresent('line-translate-anchor', lineTranslateAnchor); + addIfPresent('line-width', lineWidth); + addIfPresent('line-gap-width', lineGapWidth); + addIfPresent('line-offset', lineOffset); + addIfPresent('line-blur', lineBlur); + addIfPresent('line-dasharray', lineDasharray); + addIfPresent('line-pattern', linePattern); + addIfPresent('line-gradient', lineGradient); + addIfPresent('line-cap', lineCap); + addIfPresent('line-join', lineJoin); + addIfPresent('line-miter-limit', lineMiterLimit); + addIfPresent('line-round-limit', lineRoundLimit); + addIfPresent('line-sort-key', lineSortKey); + addIfPresent('visibility', visibility); + return json; + } + + factory LineLayerProperties.fromJson(Map json) { + return LineLayerProperties( + lineOpacity: json['line-opacity'], + lineColor: json['line-color'], + lineTranslate: json['line-translate'], + lineTranslateAnchor: json['line-translate-anchor'], + lineWidth: json['line-width'], + lineGapWidth: json['line-gap-width'], + lineOffset: json['line-offset'], + lineBlur: json['line-blur'], + lineDasharray: json['line-dasharray'], + linePattern: json['line-pattern'], + lineGradient: json['line-gradient'], + lineCap: json['line-cap'], + lineJoin: json['line-join'], + lineMiterLimit: json['line-miter-limit'], + lineRoundLimit: json['line-round-limit'], + lineSortKey: json['line-sort-key'], + visibility: json['visibility'], + ); + } +} + +class FillLayerProperties implements LayerProperties { + // Paint Properties + /// Whether or not the fill should be antialiased. + /// + /// Type: boolean + /// default: true + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillAntialias; + + /// The opacity of the entire fill layer. In contrast to the `fill-color`, + /// this value will also affect the 1px stroke around the fill, if the + /// stroke is used. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillOpacity; + + /// The color of the filled part of this layer. This color can be + /// specified as `rgba` with an alpha component and the color's opacity + /// will not affect the opacity of the 1px stroke, if it is used. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillColor; + + /// The outline color of the fill. Matches the value of `fill-color` if + /// unspecified. + /// + /// Type: color + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillOutlineColor; + + /// The geometry's offset. Values are [x, y] where negatives indicate left + /// and up, respectively. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillTranslate; + + /// Controls the frame of reference for `fill-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The fill is translated relative to the map. + /// "viewport" + /// The fill is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillTranslateAnchor; + + /// Name of image in sprite to use for drawing image 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 fillPattern; + + // Layout Properties + /// Sorts features in ascending order based on this value. Features with a + /// higher sort key will appear above features with a lower sort key. + /// + /// Type: number + /// + /// Sdk Support: + /// basic functionality with js + /// data-driven styling with js + final dynamic fillSortKey; + + /// 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 FillLayerProperties({ + this.fillAntialias, + this.fillOpacity, + this.fillColor, + this.fillOutlineColor, + this.fillTranslate, + this.fillTranslateAnchor, + this.fillPattern, + this.fillSortKey, + this.visibility, + }); + + FillLayerProperties copyWith(FillLayerProperties changes) { + return FillLayerProperties( + fillAntialias: changes.fillAntialias ?? fillAntialias, + fillOpacity: changes.fillOpacity ?? fillOpacity, + fillColor: changes.fillColor ?? fillColor, + fillOutlineColor: changes.fillOutlineColor ?? fillOutlineColor, + fillTranslate: changes.fillTranslate ?? fillTranslate, + fillTranslateAnchor: changes.fillTranslateAnchor ?? fillTranslateAnchor, + fillPattern: changes.fillPattern ?? fillPattern, + fillSortKey: changes.fillSortKey ?? fillSortKey, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('fill-antialias', fillAntialias); + addIfPresent('fill-opacity', fillOpacity); + addIfPresent('fill-color', fillColor); + addIfPresent('fill-outline-color', fillOutlineColor); + addIfPresent('fill-translate', fillTranslate); + addIfPresent('fill-translate-anchor', fillTranslateAnchor); + addIfPresent('fill-pattern', fillPattern); + addIfPresent('fill-sort-key', fillSortKey); + addIfPresent('visibility', visibility); + return json; + } + + factory FillLayerProperties.fromJson(Map json) { + return FillLayerProperties( + fillAntialias: json['fill-antialias'], + fillOpacity: json['fill-opacity'], + fillColor: json['fill-color'], + fillOutlineColor: json['fill-outline-color'], + fillTranslate: json['fill-translate'], + fillTranslateAnchor: json['fill-translate-anchor'], + fillPattern: json['fill-pattern'], + fillSortKey: json['fill-sort-key'], + visibility: json['visibility'], + ); + } +} + +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. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterOpacity; + + /// Rotates hues around the color wheel. + /// + /// Type: number + /// default: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterHueRotate; + + /// Increase or reduce the brightness of the image. The value is the + /// minimum brightness. + /// + /// Type: number + /// default: 0 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterBrightnessMin; + + /// Increase or reduce the brightness of the image. The value is the + /// maximum brightness. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterBrightnessMax; + + /// Increase or reduce the saturation of the image. + /// + /// Type: number + /// default: 0 + /// minimum: -1 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterSaturation; + + /// Increase or reduce the contrast of the image. + /// + /// Type: number + /// default: 0 + /// minimum: -1 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterContrast; + + /// The resampling/interpolation method to use for overscaling, also known + /// as texture magnification filter + /// + /// Type: enum + /// default: linear + /// Options: + /// "linear" + /// (Bi)linear filtering interpolates pixel values using the weighted + /// average of the four closest original source pixels creating a + /// smooth but blurry look when overscaled + /// "nearest" + /// Nearest neighbor filtering interpolates pixel values using the + /// nearest original source pixel creating a sharp but pixelated look + /// when overscaled + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterResampling; + + /// Fade duration when a new tile is added. + /// + /// Type: number + /// default: 300 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic rasterFadeDuration; + + // 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 RasterLayerProperties({ + this.rasterOpacity, + this.rasterHueRotate, + this.rasterBrightnessMin, + this.rasterBrightnessMax, + this.rasterSaturation, + this.rasterContrast, + this.rasterResampling, + this.rasterFadeDuration, + this.visibility, + }); + + RasterLayerProperties copyWith(RasterLayerProperties changes) { + return RasterLayerProperties( + rasterOpacity: changes.rasterOpacity ?? rasterOpacity, + rasterHueRotate: changes.rasterHueRotate ?? rasterHueRotate, + rasterBrightnessMin: changes.rasterBrightnessMin ?? rasterBrightnessMin, + rasterBrightnessMax: changes.rasterBrightnessMax ?? rasterBrightnessMax, + rasterSaturation: changes.rasterSaturation ?? rasterSaturation, + rasterContrast: changes.rasterContrast ?? rasterContrast, + rasterResampling: changes.rasterResampling ?? rasterResampling, + rasterFadeDuration: changes.rasterFadeDuration ?? rasterFadeDuration, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('raster-opacity', rasterOpacity); + addIfPresent('raster-hue-rotate', rasterHueRotate); + addIfPresent('raster-brightness-min', rasterBrightnessMin); + addIfPresent('raster-brightness-max', rasterBrightnessMax); + addIfPresent('raster-saturation', rasterSaturation); + addIfPresent('raster-contrast', rasterContrast); + addIfPresent('raster-resampling', rasterResampling); + addIfPresent('raster-fade-duration', rasterFadeDuration); + addIfPresent('visibility', visibility); + return json; + } + + factory RasterLayerProperties.fromJson(Map json) { + return RasterLayerProperties( + rasterOpacity: json['raster-opacity'], + rasterHueRotate: json['raster-hue-rotate'], + rasterBrightnessMin: json['raster-brightness-min'], + rasterBrightnessMax: json['raster-brightness-max'], + rasterSaturation: json['raster-saturation'], + rasterContrast: json['raster-contrast'], + rasterResampling: json['raster-resampling'], + rasterFadeDuration: json['raster-fade-duration'], + visibility: json['visibility'], + ); + } +} + +class HillshadeLayerProperties implements LayerProperties { + // Paint Properties + /// The direction of the light source used to generate the hillshading + /// with 0 as the top of the viewport if `hillshade-illumination-anchor` + /// is set to `viewport` and due north if `hillshade-illumination-anchor` + /// is set to `map`. + /// + /// Type: number + /// default: 335 + /// minimum: 0 + /// maximum: 359 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeIlluminationDirection; + + /// Direction of light source when map is rotated. + /// + /// Type: enum + /// default: viewport + /// Options: + /// "map" + /// The hillshade illumination is relative to the north direction. + /// "viewport" + /// The hillshade illumination is relative to the top of the + /// viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeIlluminationAnchor; + + /// Intensity of the hillshade + /// + /// Type: number + /// default: 0.5 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeExaggeration; + + /// The shading color of areas that face away from the light source. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeShadowColor; + + /// The shading color of areas that faces towards the light source. + /// + /// Type: color + /// default: #FFFFFF + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeHighlightColor; + + /// The shading color used to accentuate rugged terrain like sharp cliffs + /// and gorges. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic hillshadeAccentColor; + + // 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 HillshadeLayerProperties({ + this.hillshadeIlluminationDirection, + this.hillshadeIlluminationAnchor, + this.hillshadeExaggeration, + this.hillshadeShadowColor, + this.hillshadeHighlightColor, + this.hillshadeAccentColor, + this.visibility, + }); + + HillshadeLayerProperties copyWith(HillshadeLayerProperties changes) { + return HillshadeLayerProperties( + hillshadeIlluminationDirection: changes.hillshadeIlluminationDirection ?? + hillshadeIlluminationDirection, + hillshadeIlluminationAnchor: + changes.hillshadeIlluminationAnchor ?? hillshadeIlluminationAnchor, + hillshadeExaggeration: + changes.hillshadeExaggeration ?? hillshadeExaggeration, + hillshadeShadowColor: + changes.hillshadeShadowColor ?? hillshadeShadowColor, + hillshadeHighlightColor: + changes.hillshadeHighlightColor ?? hillshadeHighlightColor, + hillshadeAccentColor: + changes.hillshadeAccentColor ?? hillshadeAccentColor, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent( + 'hillshade-illumination-direction', hillshadeIlluminationDirection); + addIfPresent('hillshade-illumination-anchor', hillshadeIlluminationAnchor); + addIfPresent('hillshade-exaggeration', hillshadeExaggeration); + addIfPresent('hillshade-shadow-color', hillshadeShadowColor); + addIfPresent('hillshade-highlight-color', hillshadeHighlightColor); + addIfPresent('hillshade-accent-color', hillshadeAccentColor); + addIfPresent('visibility', visibility); + return json; + } + + factory HillshadeLayerProperties.fromJson(Map json) { + return HillshadeLayerProperties( + hillshadeIlluminationDirection: json['hillshade-illumination-direction'], + hillshadeIlluminationAnchor: json['hillshade-illumination-anchor'], + hillshadeExaggeration: json['hillshade-exaggeration'], + hillshadeShadowColor: json['hillshade-shadow-color'], + hillshadeHighlightColor: json['hillshade-highlight-color'], + hillshadeAccentColor: json['hillshade-accent-color'], + visibility: json['visibility'], + ); + } +} + +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 d2d38a9a4..6649c8c24 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -24,6 +24,8 @@ class MapboxMap extends StatefulWidget { this.scrollGesturesEnabled = true, this.zoomGesturesEnabled = true, this.tiltGesturesEnabled = true, + this.doubleClickZoomEnabled, + this.dragEnabled = true, this.trackCameraPosition = false, this.myLocationEnabled = false, this.myLocationTrackingMode = MyLocationTrackingMode.None, @@ -31,6 +33,7 @@ class MapboxMap extends StatefulWidget { this.logoViewMargins, this.compassViewPosition, this.compassViewMargins, + this.attributionButtonPosition, this.attributionButtonMargins, this.onMapClick, this.onUserLocationUpdated, @@ -52,15 +55,22 @@ class MapboxMap extends StatefulWidget { AnnotationType.line, AnnotationType.circle, ], - }) : assert(annotationOrder.length == 4), + this.useDelayedDisposal, + this.useHybridCompositionOverride, + }) : assert(annotationOrder.length <= 4), assert(annotationConsumeTapEvents.length > 0), super(key: key); - /// Defined the layer order of annotations displayed on map - /// (must contain all annotation types, 4 items) + /// Defines the layer order of annotations displayed on map + /// + /// Any annotation type can only be contained once, so 0 to 4 types + /// + /// Note that setting this to be empty gives a big perfomance boost for + /// android. However if you do so annotations will not work. final List annotationOrder; - /// Defined the layer order of click annotations + /// Defines the layer order of click annotations + /// /// (must contain at least 1 annotation type, 4 items max) final List annotationConsumeTapEvents; @@ -84,6 +94,12 @@ class MapboxMap extends StatefulWidget { /// True if the map should show a compass when rotated. final bool compassEnabled; + /// True if drag functionality should be enabled. + /// + /// Disable to avoid performance issues that from the drag event listeners. + /// Biggest impact in android + final bool dragEnabled; + /// Geographical bounding box for the camera target. final CameraTargetBounds cameraTargetBounds; @@ -109,6 +125,12 @@ class MapboxMap extends StatefulWidget { /// True if the map view should respond to tilt gestures. final bool tiltGesturesEnabled; + /// Set to true to forcefully disable/enable if map should respond to double + /// click to zoom. + /// + /// This takes presedence over zoomGesturesEnabled. Only supported for web. + final bool? doubleClickZoomEnabled; + /// True if you want to be notified of map camera movements by the MapboxMapController. Default is false. /// /// If this is set to true and the user pans/zooms/rotates the map, MapboxMapController (which is a ChangeNotifier) @@ -156,7 +178,13 @@ class MapboxMap extends StatefulWidget { /// Set the layout margins for the Mapbox Compass final Point? compassViewMargins; - /// Set the layout margins for the Mapbox Attribution Buttons + /// Set the position for the Mapbox Attribution Button + final AttributionButtonPosition? attributionButtonPosition; + + /// Set the layout margins for the Mapbox Attribution Buttons. If you set this + /// value, you may also want to set [attributionButtonPosition] to harmonize + /// the layout between iOS and Android, since the underlying frameworks have + /// different defaults. final Point? attributionButtonMargins; /// Which gestures should be consumed by the map. @@ -196,6 +224,21 @@ 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). + static bool get useHybridComposition => + MethodChannelMapboxGl.useHybridComposition; + static set useHybridComposition(bool useHybridComposition) => + MethodChannelMapboxGl.useHybridComposition = useHybridComposition; + @override State createState() => _MapboxMapState(); } @@ -209,18 +252,16 @@ class _MapboxMapState extends State { @override Widget build(BuildContext context) { - final List annotationOrder = - widget.annotationOrder.map((e) => e.toString()).toList(); - final List annotationConsumeTapEvents = - widget.annotationConsumeTapEvents.map((e) => e.toString()).toList(); - + assert( + widget.annotationOrder.toSet().length == widget.annotationOrder.length, + "annotationOrder must not have duplicate types"); final Map creationParams = { 'initialCameraPosition': widget.initialCameraPosition.toMap(), 'options': _MapboxMapOptions.fromWidget(widget).toMap(), 'accessToken': widget.accessToken, - 'annotationOrder': annotationOrder, - 'annotationConsumeTapEvents': annotationConsumeTapEvents, 'onAttributionClickOverride': widget.onAttributionClick != null, + 'dragEnabled': widget.dragEnabled, + 'useHybridCompositionOverride': widget.useHybridCompositionOverride, }; return _mapboxGlPlatform.buildView( creationParams, onPlatformViewCreated, widget.gestureRecognizers); @@ -232,6 +273,15 @@ class _MapboxMapState extends State { _mapboxMapOptions = _MapboxMapOptions.fromWidget(widget); } + @override + void dispose() async { + super.dispose(); + if (_controller.isCompleted) { + final controller = await _controller.future; + controller.dispose(); + } + } + @override void didUpdateWidget(MapboxMap oldWidget) { super.didUpdateWidget(oldWidget); @@ -251,22 +301,15 @@ class _MapboxMapState extends State { } Future onPlatformViewCreated(int id) async { - MapboxGlPlatform.addInstance(id, _mapboxGlPlatform); - final MapboxMapController controller = MapboxMapController.init( - id, - widget.initialCameraPosition, + final MapboxMapController controller = MapboxMapController( + mapboxGlPlatform: _mapboxGlPlatform, + initialCameraPosition: widget.initialCameraPosition, onStyleLoadedCallback: () { - if (_controller.isCompleted) { + _controller.future.then((_) { if (widget.onStyleLoadedCallback != null) { widget.onStyleLoadedCallback!(); } - } else { - _controller.future.then((_) { - if (widget.onStyleLoadedCallback != null) { - widget.onStyleLoadedCallback!(); - } - }); - } + }); }, onMapClick: widget.onMapClick, onUserLocationUpdated: widget.onUserLocationUpdated, @@ -276,8 +319,10 @@ class _MapboxMapState extends State { onCameraTrackingChanged: widget.onCameraTrackingChanged, onCameraIdle: widget.onCameraIdle, onMapIdle: widget.onMapIdle, + annotationOrder: widget.annotationOrder, + annotationConsumeTapEvents: widget.annotationConsumeTapEvents, ); - await MapboxMapController.initPlatform(id); + await _mapboxGlPlatform.initPlatform(id); _controller.complete(controller); if (widget.onMapCreated != null) { widget.onMapCreated!(controller); @@ -295,17 +340,19 @@ class _MapboxMapOptions { this.cameraTargetBounds, this.styleString, this.minMaxZoomPreference, - this.rotateGesturesEnabled, - this.scrollGesturesEnabled, - this.tiltGesturesEnabled, + required this.rotateGesturesEnabled, + required this.scrollGesturesEnabled, + required this.tiltGesturesEnabled, + required this.zoomGesturesEnabled, + required this.doubleClickZoomEnabled, this.trackCameraPosition, - this.zoomGesturesEnabled, this.myLocationEnabled, this.myLocationTrackingMode, this.myLocationRenderMode, this.logoViewMargins, this.compassViewPosition, this.compassViewMargins, + this.attributionButtonPosition, this.attributionButtonMargins, }); @@ -320,12 +367,15 @@ class _MapboxMapOptions { tiltGesturesEnabled: map.tiltGesturesEnabled, trackCameraPosition: map.trackCameraPosition, zoomGesturesEnabled: map.zoomGesturesEnabled, + doubleClickZoomEnabled: + map.doubleClickZoomEnabled ?? map.zoomGesturesEnabled, myLocationEnabled: map.myLocationEnabled, myLocationTrackingMode: map.myLocationTrackingMode, myLocationRenderMode: map.myLocationRenderMode, logoViewMargins: map.logoViewMargins, compassViewPosition: map.compassViewPosition, compassViewMargins: map.compassViewMargins, + attributionButtonPosition: map.attributionButtonPosition, attributionButtonMargins: map.attributionButtonMargins, ); } @@ -338,16 +388,18 @@ class _MapboxMapOptions { final MinMaxZoomPreference? minMaxZoomPreference; - final bool? rotateGesturesEnabled; + final bool rotateGesturesEnabled; + + final bool scrollGesturesEnabled; + + final bool tiltGesturesEnabled; - final bool? scrollGesturesEnabled; + final bool zoomGesturesEnabled; - final bool? tiltGesturesEnabled; + final bool doubleClickZoomEnabled; final bool? trackCameraPosition; - final bool? zoomGesturesEnabled; - final bool? myLocationEnabled; final MyLocationTrackingMode? myLocationTrackingMode; @@ -360,8 +412,18 @@ class _MapboxMapOptions { final Point? compassViewMargins; + final AttributionButtonPosition? attributionButtonPosition; + final Point? attributionButtonMargins; + final _gestureGroup = { + 'rotateGesturesEnabled', + 'scrollGesturesEnabled', + 'tiltGesturesEnabled', + 'zoomGesturesEnabled', + 'doubleClickZoomEnabled' + }; + Map toMap() { final Map optionsMap = {}; @@ -383,10 +445,13 @@ class _MapboxMapOptions { addIfNonNull('cameraTargetBounds', cameraTargetBounds?.toJson()); addIfNonNull('styleString', styleString); addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?.toJson()); + addIfNonNull('rotateGesturesEnabled', rotateGesturesEnabled); addIfNonNull('scrollGesturesEnabled', scrollGesturesEnabled); addIfNonNull('tiltGesturesEnabled', tiltGesturesEnabled); addIfNonNull('zoomGesturesEnabled', zoomGesturesEnabled); + addIfNonNull('doubleClickZoomEnabled', doubleClickZoomEnabled); + addIfNonNull('trackCameraPosition', trackCameraPosition); addIfNonNull('myLocationEnabled', myLocationEnabled); addIfNonNull('myLocationTrackingMode', myLocationTrackingMode?.index); @@ -394,6 +459,7 @@ class _MapboxMapOptions { addIfNonNull('logoViewMargins', pointToArray(logoViewMargins)); addIfNonNull('compassViewPosition', compassViewPosition?.index); addIfNonNull('compassViewMargins', pointToArray(compassViewMargins)); + addIfNonNull('attributionButtonPosition', attributionButtonPosition?.index); addIfNonNull( 'attributionButtonMargins', pointToArray(attributionButtonMargins)); return optionsMap; @@ -401,8 +467,22 @@ class _MapboxMapOptions { Map updatesMap(_MapboxMapOptions newOptions) { final Map prevOptionsMap = toMap(); - return newOptions.toMap() - ..removeWhere( - (String key, dynamic value) => prevOptionsMap[key] == value); + final newOptionsMap = newOptions.toMap(); + + // if any gesture is updated also all other gestures have to the saved to + // the update + + final gesturesRequireUpdate = + _gestureGroup.any((key) => newOptionsMap[key] != prevOptionsMap[key]); + + return newOptionsMap + ..removeWhere((String key, dynamic value) { + if (_gestureGroup.contains(key)) return !gesturesRequireUpdate; + final oldValue = prevOptionsMap[key]; + if (oldValue is List && value is List) { + return listEquals(oldValue, value); + } + return oldValue == value; + }); } } diff --git a/lib/src/util.dart b/lib/src/util.dart new file mode 100644 index 000000000..5ba0b789e --- /dev/null +++ b/lib/src/util.dart @@ -0,0 +1,14 @@ +part of mapbox_gl; + +Map buildFeatureCollection( + List> features) { + return {"type": "FeatureCollection", "features": features}; +} + +final _random = Random(); +String getRandomString([int length = 10]) { + const charSet = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + return String.fromCharCodes(Iterable.generate( + length, (_) => charSet.codeUnitAt(_random.nextInt(charSet.length)))); +} diff --git a/mapbox_gl_platform_interface/CHANGELOG.md b/mapbox_gl_platform_interface/CHANGELOG.md index 635770164..c187c4f50 100644 --- a/mapbox_gl_platform_interface/CHANGELOG.md +++ b/mapbox_gl_platform_interface/CHANGELOG.md @@ -1,10 +1,35 @@ -## 0.12.0, April 12, 2020 +## 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) +* 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) +* 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) + +## 0.13.0, October 21, 2021 +* Migrate to null-safety [#607](https://github.com/tobrun/flutter-mapbox-gl/pull/607) +* Support override of attribution click action (iOS) [#605](https://github.com/tobrun/flutter-mapbox-gl/pull/605) +* Fix requestMyLocationLatLng in the platform interface [#697](https://github.com/tobrun/flutter-mapbox-gl/pull/697) + +## 0.12.0, April 12, 2021 * Batch creation/removal for circles, fills and lines [#576](https://github.com/tobrun/flutter-mapbox-gl/pull/576) -## 0.11.0, March 30, 2020 +## 0.11.0, March 30, 2021 * Add batch mode of screen locations [#554](https://github.com/tobrun/flutter-mapbox-gl/pull/554) -## 0.10.0, February 12, 2020 +## 0.10.0, February 12, 2021 * Added web support for fills [#501](https://github.com/tobrun/flutter-mapbox-gl/pull/501) * Add heading to UserLocation and expose UserLocation type [#522](https://github.com/tobrun/flutter-mapbox-gl/pull/522) * Update tracked camera position in camera#onIdle [#500](https://github.com/tobrun/flutter-mapbox-gl/pull/500) 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 d82515e1b..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,15 @@ 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'; part 'src/camera.dart'; part 'src/circle.dart'; @@ -19,4 +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/annotation.dart b/mapbox_gl_platform_interface/lib/src/annotation.dart new file mode 100644 index 000000000..8478d34c3 --- /dev/null +++ b/mapbox_gl_platform_interface/lib/src/annotation.dart @@ -0,0 +1,8 @@ +part of mapbox_gl_platform_interface; + +abstract class Annotation { + String get id; + Map toGeoJson(); + + void translate(LatLng delta); +} diff --git a/mapbox_gl_platform_interface/lib/src/callbacks.dart b/mapbox_gl_platform_interface/lib/src/callbacks.dart index f2d084a8e..4813bdfff 100644 --- a/mapbox_gl_platform_interface/lib/src/callbacks.dart +++ b/mapbox_gl_platform_interface/lib/src/callbacks.dart @@ -46,6 +46,11 @@ class ArgumentCallbacks { _callbacks.remove(callback); } + /// Removes all callbacks + void clear() { + _callbacks.clear(); + } + /// Whether this collection is empty. bool get isEmpty => _callbacks.isEmpty; 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/circle.dart b/mapbox_gl_platform_interface/lib/src/circle.dart index a75a0217e..5e0000805 100644 --- a/mapbox_gl_platform_interface/lib/src/circle.dart +++ b/mapbox_gl_platform_interface/lib/src/circle.dart @@ -6,7 +6,7 @@ part of mapbox_gl_platform_interface; -class Circle { +class Circle implements Annotation { Circle(this._id, this.options, [this._data]); /// A unique identifier for this circle. @@ -24,6 +24,21 @@ class Circle { /// The returned value does not reflect any changes made to the circle through /// touch events. Add listeners to the owning map controller to track those. CircleOptions options; + + @override + Map toGeoJson() { + final geojson = options.toGeoJson(); + geojson["id"] = id; + geojson["properties"]["id"] = id; + + return geojson; + } + + @override + void translate(LatLng delta) { + options = options + .copyWith(CircleOptions(geometry: this.options.geometry! + delta)); + } } /// Configuration options for [Circle] instances. @@ -73,7 +88,7 @@ class CircleOptions { ); } - dynamic toJson() { + dynamic toJson([bool addGeometry = true]) { final Map json = {}; void addIfPresent(String fieldName, dynamic value) { @@ -89,8 +104,21 @@ class CircleOptions { addIfPresent('circleStrokeWidth', circleStrokeWidth); addIfPresent('circleStrokeColor', circleStrokeColor); addIfPresent('circleStrokeOpacity', circleStrokeOpacity); - addIfPresent('geometry', geometry?.toJson()); + if (addGeometry) { + addIfPresent('geometry', geometry?.toJson()); + } addIfPresent('draggable', draggable); return json; } + + Map toGeoJson() { + return { + "type": "Feature", + "properties": toJson(false), + "geometry": { + "type": "Point", + "coordinates": geometry!.toGeoJsonCoordinates() + } + }; + } } diff --git a/mapbox_gl_platform_interface/lib/src/fill.dart b/mapbox_gl_platform_interface/lib/src/fill.dart index 15c966551..d645de9a7 100644 --- a/mapbox_gl_platform_interface/lib/src/fill.dart +++ b/mapbox_gl_platform_interface/lib/src/fill.dart @@ -16,12 +16,12 @@ FillOptions translateFillOptions(FillOptions options, LatLng delta) { } newGeometry.add(newRing); } - return FillOptions(geometry: newGeometry); + return options.copyWith(FillOptions(geometry: newGeometry)); } return options; } -class Fill { +class Fill implements Annotation { Fill(this._id, this.options, [this._data]); /// A unique identifier for this fill. @@ -39,6 +39,20 @@ class Fill { /// The returned value does not reflect any changes made to the fill through /// touch events. Add listeners to the owning map controller to track those. FillOptions options; + + @override + Map toGeoJson() { + final geojson = options.toGeoJson(); + geojson["id"] = id; + geojson["properties"]["id"] = id; + + return geojson; + } + + @override + void translate(LatLng delta) { + options = translateFillOptions(options, delta); + } } /// Configuration options for [Fill] instances. @@ -78,7 +92,7 @@ class FillOptions { ); } - dynamic toJson() { + dynamic toJson([bool addGeometry = true]) { final Map json = {}; void addIfPresent(String fieldName, dynamic value) { @@ -91,13 +105,30 @@ class FillOptions { addIfPresent('fillColor', fillColor); addIfPresent('fillOutlineColor', fillOutlineColor); addIfPresent('fillPattern', fillPattern); - addIfPresent( - 'geometry', - geometry - ?.map((List latLngList) => - latLngList.map((LatLng latLng) => latLng.toJson()).toList()) - .toList()); + if (addGeometry) { + addIfPresent( + 'geometry', + geometry + ?.map((List latLngList) => + latLngList.map((LatLng latLng) => latLng.toJson()).toList()) + .toList()); + } addIfPresent('draggable', draggable); return json; } + + Map toGeoJson() { + return { + "type": "Feature", + "properties": toJson(false), + "geometry": { + "type": "Polygon", + "coordinates": geometry! + .map((List latLngList) => latLngList + .map((LatLng latLng) => latLng.toGeoJsonCoordinates()) + .toList()) + .toList() + } + }; + } } diff --git a/mapbox_gl_platform_interface/lib/src/line.dart b/mapbox_gl_platform_interface/lib/src/line.dart index 0447bd26d..092a69ab8 100644 --- a/mapbox_gl_platform_interface/lib/src/line.dart +++ b/mapbox_gl_platform_interface/lib/src/line.dart @@ -6,7 +6,7 @@ part of mapbox_gl_platform_interface; -class Line { +class Line implements Annotation { Line(this._id, this.options, [this._data]); /// A unique identifier for this line. @@ -26,6 +26,20 @@ class Line { /// The returned value does not reflect any changes made to the line through /// touch events. Add listeners to the owning map controller to track those. LineOptions options; + + Map toGeoJson() { + final geojson = options.toGeoJson(); + geojson["id"] = id; + geojson["properties"]["id"] = id; + + return geojson; + } + + @override + void translate(LatLng delta) { + options = options.copyWith(LineOptions( + geometry: this.options.geometry?.map((e) => e + delta).toList())); + } } /// Configuration options for [Line] instances. @@ -78,7 +92,7 @@ class LineOptions { ); } - dynamic toJson() { + dynamic toJson([bool addGeometry = true]) { final Map json = {}; void addIfPresent(String fieldName, dynamic value) { @@ -95,9 +109,22 @@ class LineOptions { addIfPresent('lineOffset', lineOffset); addIfPresent('lineBlur', lineBlur); addIfPresent('linePattern', linePattern); - addIfPresent( - 'geometry', geometry?.map((LatLng latLng) => latLng.toJson()).toList()); + if (addGeometry) { + addIfPresent('geometry', + geometry?.map((LatLng latLng) => latLng.toJson()).toList()); + } addIfPresent('draggable', draggable); return json; } + + Map toGeoJson() { + return { + "type": "Feature", + "properties": toJson(false), + "geometry": { + "type": "LineString", + "coordinates": geometry!.map((c) => c.toGeoJsonCoordinates()).toList() + } + }; + } } diff --git a/mapbox_gl_platform_interface/lib/src/location.dart b/mapbox_gl_platform_interface/lib/src/location.dart index c64e05e2c..879cd74cf 100644 --- a/mapbox_gl_platform_interface/lib/src/location.dart +++ b/mapbox_gl_platform_interface/lib/src/location.dart @@ -36,6 +36,10 @@ class LatLng { return [latitude, longitude]; } + dynamic toGeoJsonCoordinates() { + return [longitude, latitude]; + } + static LatLng _fromJson(List json) { return LatLng(json[0], json[1]); } @@ -49,7 +53,7 @@ class LatLng { } @override - int get hashCode => hashValues(latitude, longitude); + int get hashCode => Object.hash(latitude, longitude); } /// A latitude/longitude aligned rectangle. @@ -102,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 @@ -160,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 f28e37531..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 @@ -14,25 +14,11 @@ abstract class MapboxGlPlatform { static MapboxGlPlatform Function() createInstance = () => MethodChannelMapboxGl(); - static Map _instances = {}; - - static void addInstance(int id, MapboxGlPlatform platform) { - _instances[id] = platform; - } - - static MapboxGlPlatform getInstance(int id) { - return _instances[id]!; - } - final onInfoWindowTappedPlatform = ArgumentCallbacks(); - final onSymbolTappedPlatform = ArgumentCallbacks(); - - final onLineTappedPlatform = ArgumentCallbacks(); - - final onCircleTappedPlatform = ArgumentCallbacks(); + final onFeatureTappedPlatform = ArgumentCallbacks>(); - final onFillTappedPlatform = ArgumentCallbacks(); + final onFeatureDraggedPlatform = ArgumentCallbacks>(); final onCameraMoveStartedPlatform = ArgumentCallbacks(); @@ -59,233 +45,162 @@ abstract class MapboxGlPlatform { final onUserLocationUpdatedPlatform = ArgumentCallbacks(); - Future initPlatform(int id) async { - throw UnimplementedError('initPlatform() has not been implemented.'); - } - + Future initPlatform(int id); Widget buildView( Map creationParams, OnPlatformViewCreatedCallback onPlatformViewCreated, - Set>? gestureRecognizers) { - throw UnimplementedError('buildView() has not been implemented.'); - } - - Future updateMapOptions( - Map optionsUpdate) async { - throw UnimplementedError('updateMapOptions() has not been implemented.'); - } - - Future animateCamera(CameraUpdate cameraUpdate) async { - throw UnimplementedError('animateCamera() has not been implemented.'); - } - - Future moveCamera(CameraUpdate cameraUpdate) async { - throw UnimplementedError('moveCamera() has not been implemented.'); - } - + Set>? gestureRecognizers); + Future updateMapOptions(Map optionsUpdate); + Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}); + Future moveCamera(CameraUpdate cameraUpdate); Future updateMyLocationTrackingMode( - MyLocationTrackingMode myLocationTrackingMode) async { - throw UnimplementedError( - 'updateMyLocationTrackingMode() has not been implemented.'); - } - - Future matchMapLanguageWithDeviceDefault() async { - throw UnimplementedError( - 'matchMapLanguageWithDeviceDefault() has not been implemented.'); - } - - Future updateContentInsets(EdgeInsets insets, bool animated) async { - throw UnimplementedError('updateContentInsets() has not been implemented.'); - } - - Future setMapLanguage(String language) async { - throw UnimplementedError('setMapLanguage() has not been implemented.'); - } - - Future setTelemetryEnabled(bool enabled) async { - throw UnimplementedError('setTelemetryEnabled() has not been implemented.'); - } + MyLocationTrackingMode myLocationTrackingMode); - Future getTelemetryEnabled() async { - throw UnimplementedError('getTelemetryEnabled() has not been implemented.'); - } + Future matchMapLanguageWithDeviceDefault(); - Future> addSymbols(List options, - [List? data]) async { - throw UnimplementedError('addSymbols() has not been implemented.'); - } + void resizeWebMap(); + void forceResizeWebMap(); - Future updateSymbol(Symbol symbol, SymbolOptions changes) async { - throw UnimplementedError('updateSymbol() has not been implemented.'); - } - - Future removeSymbols(Iterable symbolsIds) async { - throw UnimplementedError('removeSymbol() has not been implemented.'); - } - - Future addLine(LineOptions options, [Map? data]) async { - throw UnimplementedError('addLine() has not been implemented.'); - } - - Future> addLines(List options, - [List? data]) async { - throw UnimplementedError('addLines() has not been implemented.'); - } - - Future updateLine(Line line, LineOptions changes) async { - throw UnimplementedError('updateLine() has not been implemented.'); - } - - Future removeLine(String lineId) async { - throw UnimplementedError('removeLine() has not been implemented.'); - } - - Future removeLines(Iterable ids) async { - throw UnimplementedError('removeLines() has not been implemented.'); - } - - Future addCircle(CircleOptions options, [Map? data]) async { - throw UnimplementedError('addCircle() has not been implemented.'); - } - - Future> addCircles(List options, - [List? data]) async { - throw UnimplementedError('addCircles() has not been implemented.'); - } - - Future updateCircle(Circle circle, CircleOptions changes) async { - throw UnimplementedError('updateCircle() has not been implemented.'); - } - - Future getCircleLatLng(Circle circle) async { - throw UnimplementedError('getCircleLatLng() has not been implemented.'); - } - - Future getSymbolLatLng(Symbol symbol) async { - throw UnimplementedError('getSymbolLatLng() has not been implemented.'); - } - - Future> getLineLatLngs(Line line) async { - throw UnimplementedError('getLineLatLngs() has not been implemented.'); - } - - Future removeCircle(String circleId) async { - throw UnimplementedError('removeCircle() has not been implemented.'); - } - - Future removeCircles(Iterable ids) async { - throw UnimplementedError('removeCircles() has not been implemented.'); - } - - Future addFill(FillOptions options, [Map? data]) async { - throw UnimplementedError('addFill() has not been implemented.'); - } - - Future> addFills(List options, - [List? data]) async { - throw UnimplementedError('addFills() has not been implemented.'); - } - - Future updateFill(Fill fill, FillOptions changes) async { - throw UnimplementedError('updateFill() has not been implemented.'); - } - - Future removeFill(String fillId) async { - throw UnimplementedError('removeFill() has not been implemented.'); - } - - Future removeFills(Iterable fillIds) async { - throw UnimplementedError('removeFills() has not been implemented.'); - } + Future updateContentInsets(EdgeInsets insets, bool animated); + Future setMapLanguage(String language); + Future setTelemetryEnabled(bool enabled); + Future getTelemetryEnabled(); Future queryRenderedFeatures( - Point point, List layerIds, List? filter) async { - throw UnimplementedError( - 'queryRenderedFeatures() has not been implemented.'); - } + Point point, List layerIds, List? filter); Future queryRenderedFeaturesInRect( - Rect rect, List layerIds, String? filter) async { - throw UnimplementedError( - 'queryRenderedFeaturesInRect() has not been implemented.'); - } + Rect rect, List layerIds, String? filter); + Future invalidateAmbientCache(); + Future requestMyLocationLatLng(); - Future invalidateAmbientCache() async { - throw UnimplementedError( - 'invalidateAmbientCache() has not been implemented.'); - } - - Future requestMyLocationLatLng() async { - throw UnimplementedError( - 'requestMyLocationLatLng() has not been implemented.'); - } + Future getVisibleRegion(); - Future getVisibleRegion() async { - throw UnimplementedError('getVisibleRegion() has not been implemented.'); - } - - Future addImage(String name, Uint8List bytes, - [bool sdf = false]) async { - throw UnimplementedError('addImage() has not been implemented.'); - } - - Future setSymbolIconAllowOverlap(bool enable) async { - throw UnimplementedError( - 'setSymbolIconAllowOverlap() has not been implemented.'); - } - - Future setSymbolIconIgnorePlacement(bool enable) async { - throw UnimplementedError( - 'setSymbolIconIgnorePlacement() has not been implemented.'); - } - - Future setSymbolTextAllowOverlap(bool enable) async { - throw UnimplementedError( - 'setSymbolTextAllowOverlap() has not been implemented.'); - } - - Future setSymbolTextIgnorePlacement(bool enable) async { - throw UnimplementedError( - 'setSymbolTextIgnorePlacement() has not been implemented.'); - } + Future addImage(String name, Uint8List bytes, [bool sdf = false]); Future addImageSource( - String imageSourceId, Uint8List bytes, LatLngQuad coordinates) async { - throw UnimplementedError('addImageSource() has not been implemented.'); - } - - Future removeImageSource(String imageSourceId) async { - throw UnimplementedError('removeImageSource() has not been implemented.'); - } - - Future addLayer(String imageLayerId, String imageSourceId) async { - throw UnimplementedError('addLayer() has not been implemented.'); - } - - Future addLayerBelow( - String imageLayerId, String imageSourceId, String belowLayerId) async { - throw UnimplementedError('addLayerBelow() has not been implemented.'); - } - - Future removeLayer(String imageLayerId) async { - throw UnimplementedError('removeLayer() has not been implemented.'); - } - - Future toScreenLocation(LatLng latLng) async { - throw UnimplementedError('toScreenLocation() has not been implemented.'); - } - - Future> toScreenLocationBatch(Iterable latLngs) async { - throw UnimplementedError( - 'toScreenLocationList() has not been implemented.'); - } - - Future toLatLng(Point screenLocation) async { - throw UnimplementedError('toLatLng() has not been implemented.'); - } - - Future getMetersPerPixelAtLatitude(double latitude) async { - throw UnimplementedError( - 'getMetersPerPixelAtLatitude() has not been implemented.'); + String imageSourceId, Uint8List bytes, LatLngQuad coordinates); + + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates); + + Future addLayer(String imageLayerId, String imageSourceId, + double? minzoom, double? maxzoom); + + Future addLayerBelow(String imageLayerId, String imageSourceId, + String belowLayerId, double? minzoom, double? maxzoom); + + Future removeLayer(String imageLayerId); + + Future setFilter(String layerId, dynamic filter); + + Future setVisibility(String layerId, bool isVisible); + + Future toScreenLocation(LatLng latLng); + + Future> toScreenLocationBatch(Iterable latLngs); + + Future toLatLng(Point screenLocation); + + Future getMetersPerPixelAtLatitude(double latitude); + + Future addGeoJsonSource(String sourceId, Map geojson, + {String? promoteId}); + + Future setGeoJsonSource(String sourceId, Map geojson); + + Future setFeatureForGeoJsonSource( + String sourceId, Map geojsonFeature); + + Future removeSource(String sourceId); + + Future addSymbolLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}); + + Future addLineLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}); + + Future addCircleLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}); + + Future addFillLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + 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, + String? sourceLayer, + double? minzoom, + double? maxzoom}); + + Future addHillshadeLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + 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 + onInfoWindowTappedPlatform.clear(); + onFeatureTappedPlatform.clear(); + onFeatureDraggedPlatform.clear(); + onCameraMoveStartedPlatform.clear(); + onCameraMovePlatform.clear(); + onCameraIdlePlatform.clear(); + onMapStyleLoadedPlatform.clear(); + + onMapClickPlatform.clear(); + onMapLongClickPlatform.clear(); + onAttributionClickPlatform.clear(); + onCameraTrackingChangedPlatform.clear(); + onCameraTrackingDismissedPlatform.clear(); + onMapIdlePlatform.clear(); + onUserLocationUpdatedPlatform.clear(); } } 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 e471f7d40..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 @@ -2,6 +2,7 @@ part of mapbox_gl_platform_interface; class MethodChannelMapboxGl extends MapboxGlPlatform { late MethodChannel _channel; + static bool useHybridComposition = false; Future _handleMethodCall(MethodCall call) async { switch (call.method) { @@ -11,30 +12,43 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { onInfoWindowTappedPlatform(symbolId); } break; - case 'symbol#onTap': - final String? symbolId = call.arguments['symbol']; - if (symbolId != null) { - onSymbolTappedPlatform(symbolId); - } - break; - case 'line#onTap': - final String? lineId = call.arguments['line']; - if (lineId != null) { - onLineTappedPlatform(lineId); - } - break; - case 'circle#onTap': - final String? circleId = call.arguments['circle']; - if (circleId != null) { - onCircleTappedPlatform(circleId); - } + + case 'feature#onTap': + final id = call.arguments['id']; + final double x = call.arguments['x']; + final double y = call.arguments['y']; + final double lng = call.arguments['lng']; + final double lat = call.arguments['lat']; + onFeatureTappedPlatform({ + 'id': id, + 'point': Point(x, y), + 'latLng': LatLng(lat, lng) + }); break; - case 'fill#onTap': - final String? fillId = call.arguments['fill']; - if (fillId != null) { - onFillTappedPlatform(fillId); - } + case 'feature#onDrag': + final id = call.arguments['id']; + final double x = call.arguments['x']; + final double y = call.arguments['y']; + final double originLat = call.arguments['originLat']; + final double originLng = call.arguments['originLng']; + + final double currentLat = call.arguments['currentLat']; + final double currentLng = call.arguments['currentLng']; + + final double deltaLat = call.arguments['deltaLat']; + final double deltaLng = call.arguments['deltaLng']; + final String eventType = call.arguments['eventType']; + + onFeatureDraggedPlatform({ + 'id': id, + 'point': Point(x, y), + 'origin': LatLng(originLat, originLng), + 'current': LatLng(currentLat, currentLng), + 'delta': LatLng(deltaLat, deltaLng), + 'eventType': eventType, + }); break; + case 'camera#onMoveStarted': onCameraMoveStartedPlatform(null); break; @@ -117,8 +131,8 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { @override Future initPlatform(int id) async { _channel = MethodChannel('plugins.flutter.io/mapbox_maps_$id'); - await _channel.invokeMethod('map#waitForMap'); _channel.setMethodCallHandler(_handleMethodCall); + await _channel.invokeMethod('map#waitForMap'); } @override @@ -127,13 +141,53 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { OnPlatformViewCreatedCallback onPlatformViewCreated, Set>? gestureRecognizers) { if (defaultTargetPlatform == TargetPlatform.android) { - return AndroidView( - viewType: 'plugins.flutter.io/mapbox_gl', - onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - ); + final useHybridCompositionParam = + (creationParams['useHybridCompositionOverride'] ?? + useHybridComposition) as bool; + if (useHybridCompositionParam) { + return PlatformViewLink( + viewType: 'plugins.flutter.io/mapbox_gl', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + late AndroidViewController controller; + controller = PlatformViewsService.initAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/mapbox_gl', + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } else { + return AndroidView( + viewType: 'plugins.flutter.io/mapbox_gl', + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'plugins.flutter.io/mapbox_gl', @@ -160,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, }); } @@ -219,235 +274,6 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { return await _channel.invokeMethod('map#getTelemetryEnabled'); } - @override - Future> addSymbols(List options, - [List? data]) async { - final List symbolIds = await _channel.invokeMethod( - 'symbols#addAll', - { - 'options': options.map((o) => o.toJson()).toList(), - }, - ); - final List symbols = symbolIds - .asMap() - .map((i, id) => MapEntry( - i, - Symbol(id, options.elementAt(i), - data != null && data.length > i ? data.elementAt(i) : null))) - .values - .toList(); - - return symbols; - } - - @override - Future updateSymbol(Symbol symbol, SymbolOptions changes) async { - await _channel.invokeMethod('symbol#update', { - 'symbol': symbol.id, - 'options': changes.toJson(), - }); - } - - @override - Future getSymbolLatLng(Symbol symbol) async { - Map mapLatLng = - await _channel.invokeMethod('symbol#getGeometry', { - 'symbol': symbol._id, - }); - LatLng symbolLatLng = - new LatLng(mapLatLng['latitude'], mapLatLng['longitude']); - return symbolLatLng; - } - - @override - Future removeSymbols(Iterable ids) async { - await _channel.invokeMethod('symbols#removeAll', { - 'ids': ids.toList(), - }); - } - - @override - Future addLine(LineOptions options, [Map? data]) async { - final String lineId = await _channel.invokeMethod( - 'line#add', - { - 'options': options.toJson(), - }, - ); - return Line(lineId, options, data); - } - - @override - Future> addLines(List options, - [List? data]) async { - final List ids = await _channel.invokeMethod( - 'line#addAll', - { - 'options': options.map((o) => o.toJson()).toList(), - }, - ); - final List lines = ids - .asMap() - .map((i, id) => MapEntry( - i, - Line(id, options.elementAt(i), - data != null && data.length > i ? data.elementAt(i) : null))) - .values - .toList(); - - return lines; - } - - @override - Future updateLine(Line line, LineOptions changes) async { - await _channel.invokeMethod('line#update', { - 'line': line.id, - 'options': changes.toJson(), - }); - } - - @override - Future> getLineLatLngs(Line line) async { - List latLngList = - await _channel.invokeMethod('line#getGeometry', { - 'line': line._id, - }); - List resultList = []; - for (var latLng in latLngList) { - resultList.add(LatLng(latLng['latitude'], latLng['longitude'])); - } - return resultList; - } - - @override - Future removeLine(String lineId) async { - await _channel.invokeMethod('line#remove', { - 'line': lineId, - }); - } - - @override - Future removeLines(Iterable ids) async { - await _channel.invokeMethod('line#removeAll', { - 'ids': ids.toList(), - }); - } - - @override - Future addCircle(CircleOptions options, [Map? data]) async { - final String circleId = await _channel.invokeMethod( - 'circle#add', - { - 'options': options.toJson(), - }, - ); - return Circle(circleId, options, data); - } - - @override - Future> addCircles(List options, - [List? data]) async { - final List ids = await _channel.invokeMethod( - 'circle#addAll', - { - 'options': options.map((o) => o.toJson()).toList(), - }, - ); - return ids - .asMap() - .map((i, id) => MapEntry( - i, - Circle(id, options.elementAt(i), - data != null && data.length > i ? data.elementAt(i) : null))) - .values - .toList(); - } - - @override - Future updateCircle(Circle circle, CircleOptions changes) async { - await _channel.invokeMethod('circle#update', { - 'circle': circle.id, - 'options': changes.toJson(), - }); - } - - @override - Future getCircleLatLng(Circle circle) async { - Map mapLatLng = - await _channel.invokeMethod('circle#getGeometry', { - 'circle': circle.id, - }); - return LatLng(mapLatLng['latitude'], mapLatLng['longitude']); - } - - @override - Future removeCircle(String circleId) async { - await _channel.invokeMethod('circle#remove', { - 'circle': circleId, - }); - } - - @override - Future removeCircles(Iterable ids) async { - await _channel.invokeMethod('circle#removeAll', { - 'ids': ids.toList(), - }); - } - - @override - Future addFill(FillOptions options, [Map? data]) async { - final String fillId = await _channel.invokeMethod( - 'fill#add', - { - 'options': options.toJson(), - }, - ); - return Fill(fillId, options, data); - } - - @override - Future> addFills(List options, - [List? data]) async { - final List ids = await _channel.invokeMethod( - 'fill#addAll', - { - 'options': options.map((o) => o.toJson()).toList(), - }, - ); - final List fills = ids - .asMap() - .map((i, id) => MapEntry( - i, - Fill(id, options.elementAt(i), - data != null && data.length > i ? data.elementAt(i) : null))) - .values - .toList(); - - return fills; - } - - @override - Future updateFill(Fill fill, FillOptions changes) async { - await _channel.invokeMethod('fill#update', { - 'fill': fill.id, - 'options': changes.toJson(), - }); - } - - @override - Future removeFill(String fillId) async { - await _channel.invokeMethod('fill#remove', { - 'fill': fillId, - }); - } - - @override - Future removeFills(Iterable ids) async { - await _channel.invokeMethod('fill#removeAll', { - 'ids': ids.toList(), - }); - } - @override Future queryRenderedFeatures( Point point, List layerIds, List? filter) async { @@ -476,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, }, @@ -501,7 +332,7 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { @override Future requestMyLocationLatLng() async { try { - final Map reply = await _channel.invokeMethod( + final Map reply = await _channel.invokeMethod( 'locationComponent#getLastLocation', null); double latitude = 0.0, longitude = 0.0; if (reply.containsKey('latitude') && reply['latitude'] != null) { @@ -548,47 +379,15 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future setSymbolIconAllowOverlap(bool enable) async { - try { - await _channel - .invokeMethod('symbolManager#iconAllowOverlap', { - 'iconAllowOverlap': enable, - }); - } on PlatformException catch (e) { - return new Future.error(e); - } - } - - @override - Future setSymbolIconIgnorePlacement(bool enable) async { - try { - await _channel - .invokeMethod('symbolManager#iconIgnorePlacement', { - 'iconIgnorePlacement': enable, - }); - } on PlatformException catch (e) { - return new Future.error(e); - } - } - - @override - Future setSymbolTextAllowOverlap(bool enable) async { - try { - await _channel - .invokeMethod('symbolManager#textAllowOverlap', { - 'textAllowOverlap': enable, - }); - } on PlatformException catch (e) { - return new Future.error(e); - } - } - - @override - Future setSymbolTextIgnorePlacement(bool enable) async { + Future addImageSource( + String imageSourceId, Uint8List bytes, LatLngQuad coordinates) async { try { - await _channel - .invokeMethod('symbolManager#textIgnorePlacement', { - 'textIgnorePlacement': enable, + return await _channel + .invokeMethod('style#addImageSource', { + 'imageSourceId': imageSourceId, + 'bytes': bytes, + 'length': bytes.length, + 'coordinates': coordinates.toList() }); } on PlatformException catch (e) { return new Future.error(e); @@ -596,15 +395,15 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future addImageSource( - String imageSourceId, Uint8List bytes, LatLngQuad coordinates) async { + Future updateImageSource( + String imageSourceId, Uint8List? bytes, LatLngQuad? coordinates) async { try { return await _channel - .invokeMethod('style#addImageSource', { + .invokeMethod('style#updateImageSource', { 'imageSourceId': imageSourceId, 'bytes': bytes, - 'length': bytes.length, - 'coordinates': coordinates.toList() + 'length': bytes?.length, + 'coordinates': coordinates?.toList() }); } on PlatformException catch (e) { return new Future.error(e); @@ -647,21 +446,26 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future removeImageSource(String imageSourceId) async { + Future removeSource(String sourceId) async { try { - return await _channel.invokeMethod('style#removeImageSource', - {'imageSourceId': imageSourceId}); + return await _channel.invokeMethod( + 'style#removeSource', + {'sourceId': sourceId}, + ); } on PlatformException catch (e) { return new Future.error(e); } } @override - Future addLayer(String imageLayerId, String imageSourceId) async { + Future addLayer(String imageLayerId, String imageSourceId, + double? minzoom, double? maxzoom) async { try { - return await _channel.invokeMethod('style#addLayer', { + return await _channel.invokeMethod('style#addLayer', { 'imageLayerId': imageLayerId, - 'imageSourceId': imageSourceId + 'imageSourceId': imageSourceId, + 'minzoom': minzoom, + 'maxzoom': maxzoom }); } on PlatformException catch (e) { return new Future.error(e); @@ -669,14 +473,16 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future addLayerBelow( - String imageLayerId, String imageSourceId, String belowLayerId) async { + Future addLayerBelow(String imageLayerId, String imageSourceId, + String belowLayerId, double? minzoom, double? maxzoom) async { try { return await _channel - .invokeMethod('style#addLayerBelow', { + .invokeMethod('style#addLayerBelow', { 'imageLayerId': imageLayerId, 'imageSourceId': imageSourceId, - 'belowLayerId': belowLayerId + 'belowLayerId': belowLayerId, + 'minzoom': minzoom, + 'maxzoom': maxzoom }); } on PlatformException catch (e) { return new Future.error(e); @@ -684,10 +490,30 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future removeLayer(String imageLayerId) async { + Future removeLayer(String layerId) async { try { return await _channel.invokeMethod( - 'style#removeLayer', {'imageLayerId': imageLayerId}); + 'style#removeLayer', {'layerId': layerId}); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future setFilter(String layerId, dynamic filter) async { + try { + return await _channel.invokeMethod('style#setFilter', + {'layerId': layerId, 'filter': jsonEncode(filter)}); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @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); } @@ -719,4 +545,231 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { return new Future.error(e); } } + + @override + Future addGeoJsonSource(String sourceId, Map geojson, + {String? promoteId}) async { + await _channel.invokeMethod('source#addGeoJson', { + 'sourceId': sourceId, + 'geojson': jsonEncode(geojson), + }); + } + + @override + Future setGeoJsonSource( + String sourceId, Map geojson) async { + await _channel.invokeMethod('source#setGeoJson', { + 'sourceId': sourceId, + 'geojson': jsonEncode(geojson), + }); + } + + @override + Future addSymbolLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('symbolLayer#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 + Future addLineLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('lineLayer#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 + Future addCircleLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('circleLayer#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 + Future addFillLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('fillLayer#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 + 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(); + _channel.setMethodCallHandler(null); + } + + @override + Future addSource(String sourceId, SourceProperties properties) async { + await _channel.invokeMethod('style#addSource', { + 'sourceId': sourceId, + 'properties': properties.toJson(), + }); + } + + @override + Future addRasterLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + await _channel.invokeMethod('rasterLayer#add', { + 'sourceId': sourceId, + 'layerId': layerId, + 'belowLayerId': belowLayerId, + 'minzoom': minzoom, + 'maxzoom': maxzoom, + 'properties': properties + .map((key, value) => MapEntry(key, jsonEncode(value))) + }); + } + + @override + Future addHillshadeLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + await _channel.invokeMethod('hillshadeLayer#add', { + 'sourceId': sourceId, + 'layerId': layerId, + 'belowLayerId': belowLayerId, + 'minzoom': minzoom, + 'maxzoom': maxzoom, + 'properties': properties + .map((key, value) => MapEntry(key, jsonEncode(value))) + }); + } + + @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', { + 'sourceId': sourceId, + '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 new file mode 100644 index 000000000..c6b450c7c --- /dev/null +++ b/mapbox_gl_platform_interface/lib/src/source_properties.dart @@ -0,0 +1,691 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl_platform_interface; + +abstract class SourceProperties { + Map toJson(); +} + +class VectorSourceProperties implements SourceProperties { + /// A URL to a TileJSON resource. Supported protocols are `http:`, + /// `https:`, and `mapbox://`. + /// + /// Type: string + final String? url; + + /// An array of one or more tile source URLs, as in the TileJSON spec. + /// + /// Type: array + final List? tiles; + + /// An array containing the longitude and latitude of the southwest and + /// northeast corners of the source's bounding box in the following order: + /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in + /// a source, no tiles outside of the given bounds are requested by Mapbox + /// GL. + /// + /// Type: array + /// default: [-180, -85.051129, 180, 85.051129] + final List? bounds; + + /// Influences the y direction of the tile coordinates. The + /// global-mercator (aka Spherical Mercator) profile is assumed. + /// + /// Type: enum + /// default: xyz + /// Options: + /// "xyz" + /// Slippy map tilenames scheme. + /// "tms" + /// OSGeo spec scheme. + final String? scheme; + + /// Minimum zoom level for which tiles are available, as in the TileJSON + /// spec. + /// + /// Type: number + /// default: 0 + final double? minzoom; + + /// Maximum zoom level for which tiles are available, as in the TileJSON + /// spec. Data from tiles at the maxzoom are used when displaying the map + /// at higher zoom levels. + /// + /// Type: number + /// default: 22 + final double? maxzoom; + + /// Contains an attribution to be displayed when the map is shown to a + /// user. + /// + /// Type: string + final String? attribution; + + /// A property to use as a feature id (for feature state). Either a + /// property name, or an object of the form `{: + /// }`. If specified as a string for a vector tile source, + /// the same property is used across all its source layers. + /// + /// Type: promoteId + final String? promoteId; + + const VectorSourceProperties({ + this.url, + this.tiles, + this.bounds = const [-180, -85.051129, 180, 85.051129], + this.scheme = "xyz", + this.minzoom = 0, + this.maxzoom = 22, + this.attribution, + this.promoteId, + }); + + VectorSourceProperties copyWith( + String? url, + List? tiles, + List? bounds, + String? scheme, + double? minzoom, + double? maxzoom, + String? attribution, + String? promoteId, + ) { + return VectorSourceProperties( + url: url ?? this.url, + tiles: tiles ?? this.tiles, + bounds: bounds ?? this.bounds, + scheme: scheme ?? this.scheme, + minzoom: minzoom ?? this.minzoom, + maxzoom: maxzoom ?? this.maxzoom, + attribution: attribution ?? this.attribution, + promoteId: promoteId ?? this.promoteId, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "vector"; + addIfPresent('url', url); + addIfPresent('tiles', tiles); + addIfPresent('bounds', bounds); + addIfPresent('scheme', scheme); + addIfPresent('minzoom', minzoom); + addIfPresent('maxzoom', maxzoom); + addIfPresent('attribution', attribution); + addIfPresent('promoteId', promoteId); + return json; + } + + factory VectorSourceProperties.fromJson(Map json) { + return VectorSourceProperties( + url: json['url'], + tiles: json['tiles'], + bounds: json['bounds'], + scheme: json['scheme'], + minzoom: json['minzoom'], + maxzoom: json['maxzoom'], + attribution: json['attribution'], + promoteId: json['promoteId'], + ); + } +} + +class RasterSourceProperties implements SourceProperties { + /// A URL to a TileJSON resource. Supported protocols are `http:`, + /// `https:`, and `mapbox://`. + /// + /// Type: string + final String? url; + + /// An array of one or more tile source URLs, as in the TileJSON spec. + /// + /// Type: array + final List? tiles; + + /// An array containing the longitude and latitude of the southwest and + /// northeast corners of the source's bounding box in the following order: + /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in + /// a source, no tiles outside of the given bounds are requested by Mapbox + /// GL. + /// + /// Type: array + /// default: [-180, -85.051129, 180, 85.051129] + final List? bounds; + + /// Minimum zoom level for which tiles are available, as in the TileJSON + /// spec. + /// + /// Type: number + /// default: 0 + final double? minzoom; + + /// Maximum zoom level for which tiles are available, as in the TileJSON + /// spec. Data from tiles at the maxzoom are used when displaying the map + /// at higher zoom levels. + /// + /// Type: number + /// default: 22 + final double? maxzoom; + + /// The minimum visual size to display tiles for this layer. Only + /// configurable for raster layers. + /// + /// Type: number + /// default: 512 + final double? tileSize; + + /// Influences the y direction of the tile coordinates. The + /// global-mercator (aka Spherical Mercator) profile is assumed. + /// + /// Type: enum + /// default: xyz + /// Options: + /// "xyz" + /// Slippy map tilenames scheme. + /// "tms" + /// OSGeo spec scheme. + final String? scheme; + + /// Contains an attribution to be displayed when the map is shown to a + /// user. + /// + /// Type: string + final String? attribution; + + const RasterSourceProperties({ + this.url, + this.tiles, + this.bounds = const [-180, -85.051129, 180, 85.051129], + this.minzoom = 0, + this.maxzoom = 22, + this.tileSize = 512, + this.scheme = "xyz", + this.attribution, + }); + + RasterSourceProperties copyWith( + String? url, + List? tiles, + List? bounds, + double? minzoom, + double? maxzoom, + double? tileSize, + String? scheme, + String? attribution, + ) { + return RasterSourceProperties( + url: url ?? this.url, + tiles: tiles ?? this.tiles, + bounds: bounds ?? this.bounds, + minzoom: minzoom ?? this.minzoom, + maxzoom: maxzoom ?? this.maxzoom, + tileSize: tileSize ?? this.tileSize, + scheme: scheme ?? this.scheme, + attribution: attribution ?? this.attribution, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "raster"; + addIfPresent('url', url); + addIfPresent('tiles', tiles); + addIfPresent('bounds', bounds); + addIfPresent('minzoom', minzoom); + addIfPresent('maxzoom', maxzoom); + addIfPresent('tileSize', tileSize); + addIfPresent('scheme', scheme); + addIfPresent('attribution', attribution); + return json; + } + + factory RasterSourceProperties.fromJson(Map json) { + return RasterSourceProperties( + url: json['url'], + tiles: json['tiles'], + bounds: json['bounds'], + minzoom: json['minzoom'], + maxzoom: json['maxzoom'], + tileSize: json['tileSize'], + scheme: json['scheme'], + attribution: json['attribution'], + ); + } +} + +class RasterDemSourceProperties implements SourceProperties { + /// A URL to a TileJSON resource. Supported protocols are `http:`, + /// `https:`, and `mapbox://`. + /// + /// Type: string + final String? url; + + /// An array of one or more tile source URLs, as in the TileJSON spec. + /// + /// Type: array + final List? tiles; + + /// An array containing the longitude and latitude of the southwest and + /// northeast corners of the source's bounding box in the following order: + /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in + /// a source, no tiles outside of the given bounds are requested by Mapbox + /// GL. + /// + /// Type: array + /// default: [-180, -85.051129, 180, 85.051129] + final List? bounds; + + /// Minimum zoom level for which tiles are available, as in the TileJSON + /// spec. + /// + /// Type: number + /// default: 0 + final double? minzoom; + + /// Maximum zoom level for which tiles are available, as in the TileJSON + /// spec. Data from tiles at the maxzoom are used when displaying the map + /// at higher zoom levels. + /// + /// Type: number + /// default: 22 + final double? maxzoom; + + /// The minimum visual size to display tiles for this layer. Only + /// configurable for raster layers. + /// + /// Type: number + /// default: 512 + final double? tileSize; + + /// Contains an attribution to be displayed when the map is shown to a + /// user. + /// + /// Type: string + final String? attribution; + + /// The encoding used by this source. Mapbox Terrain RGB is used by + /// default + /// + /// Type: enum + /// default: mapbox + /// Options: + /// "terrarium" + /// Terrarium format PNG tiles. See + /// https://aws.amazon.com/es/public-datasets/terrain/ for more info. + /// "mapbox" + /// Mapbox Terrain RGB tiles. See + /// https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb + /// for more info. + final String? encoding; + + const RasterDemSourceProperties({ + this.url, + this.tiles, + this.bounds = const [-180, -85.051129, 180, 85.051129], + this.minzoom = 0, + this.maxzoom = 22, + this.tileSize = 512, + this.attribution, + this.encoding = "mapbox", + }); + + RasterDemSourceProperties copyWith( + String? url, + List? tiles, + List? bounds, + double? minzoom, + double? maxzoom, + double? tileSize, + String? attribution, + String? encoding, + ) { + return RasterDemSourceProperties( + url: url ?? this.url, + tiles: tiles ?? this.tiles, + bounds: bounds ?? this.bounds, + minzoom: minzoom ?? this.minzoom, + maxzoom: maxzoom ?? this.maxzoom, + tileSize: tileSize ?? this.tileSize, + attribution: attribution ?? this.attribution, + encoding: encoding ?? this.encoding, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "raster-dem"; + addIfPresent('url', url); + addIfPresent('tiles', tiles); + addIfPresent('bounds', bounds); + addIfPresent('minzoom', minzoom); + addIfPresent('maxzoom', maxzoom); + addIfPresent('tileSize', tileSize); + addIfPresent('attribution', attribution); + addIfPresent('encoding', encoding); + return json; + } + + factory RasterDemSourceProperties.fromJson(Map json) { + return RasterDemSourceProperties( + url: json['url'], + tiles: json['tiles'], + bounds: json['bounds'], + minzoom: json['minzoom'], + maxzoom: json['maxzoom'], + tileSize: json['tileSize'], + attribution: json['attribution'], + encoding: json['encoding'], + ); + } +} + +class GeojsonSourceProperties implements SourceProperties { + /// A URL to a GeoJSON file, or inline GeoJSON. + /// + /// Type: * + final Object? data; + + /// Maximum zoom level at which to create vector tiles (higher means + /// greater detail at high zoom levels). + /// + /// Type: number + /// default: 18 + final double? maxzoom; + + /// Contains an attribution to be displayed when the map is shown to a + /// user. + /// + /// Type: string + final String? attribution; + + /// Size of the tile buffer on each side. A value of 0 produces no buffer. + /// A value of 512 produces a buffer as wide as the tile itself. Larger + /// values produce fewer rendering artifacts near tile edges and slower + /// performance. + /// + /// Type: number + /// default: 128 + /// minimum: 0 + /// maximum: 512 + final double? buffer; + + /// Douglas-Peucker simplification tolerance (higher means simpler + /// geometries and faster performance). + /// + /// Type: number + /// default: 0.375 + final double? tolerance; + + /// If the data is a collection of point features, setting this to true + /// clusters the points by radius into groups. Cluster groups become new + /// `Point` features in the source with additional properties: + /// * `cluster` Is `true` if the point is a cluster + /// * `cluster_id` A unqiue id for the cluster to be used in conjunction + /// with the [cluster inspection + /// methods](https://www.mapbox.com/mapbox-gl-js/api/#geojsonsource#getclusterexpansionzoom) + /// * `point_count` Number of original points grouped into this cluster + /// * `point_count_abbreviated` An abbreviated point count + /// + /// Type: boolean + /// default: false + final bool? cluster; + + /// Radius of each cluster if clustering is enabled. A value of 512 + /// indicates a radius equal to the width of a tile. + /// + /// Type: number + /// default: 50 + /// minimum: 0 + final double? clusterRadius; + + /// Max zoom on which to cluster points if clustering is enabled. Defaults + /// to one zoom less than maxzoom (so that last zoom features are not + /// clustered). + /// + /// Type: number + final double? clusterMaxZoom; + + /// An object defining custom properties on the generated clusters if + /// clustering is enabled, aggregating values from clustered points. Has + /// the form `{"property_name": [operator, map_expression]}`. `operator` + /// is any expression function that accepts at least 2 operands (e.g. + /// `"+"` or `"max"`) — it accumulates the property value from + /// clusters/points the cluster contains; `map_expression` produces the + /// value of a single point.Example: `{"sum": ["+", ["get", + /// "scalerank"]]}`.For more advanced use cases, in place of `operator`, + /// you can use a custom reduce expression that references a special + /// `["accumulated"]` value, e.g.:`{"sum": [["+", ["accumulated"], + /// ["get", "sum"]], ["get", "scalerank"]]}` + /// + /// Type: * + final Object? clusterProperties; + + /// Whether to calculate line distance metrics. This is required for line + /// layers that specify `line-gradient` values. + /// + /// Type: boolean + /// default: false + final bool? lineMetrics; + + /// Whether to generate ids for the geojson features. When enabled, the + /// `feature.id` property will be auto assigned based on its index in the + /// `features` array, over-writing any previous values. + /// + /// Type: boolean + /// default: false + final bool? generateId; + + /// A property to use as a feature id (for feature state). Either a + /// property name, or an object of the form `{: + /// }`. + /// + /// Type: promoteId + final String? promoteId; + + const GeojsonSourceProperties({ + this.data, + this.maxzoom = 18, + this.attribution, + this.buffer = 128, + this.tolerance = 0.375, + this.cluster = false, + this.clusterRadius = 50, + this.clusterMaxZoom, + this.clusterProperties, + this.lineMetrics = false, + this.generateId = false, + this.promoteId, + }); + + GeojsonSourceProperties copyWith( + Object? data, + double? maxzoom, + String? attribution, + double? buffer, + double? tolerance, + bool? cluster, + double? clusterRadius, + double? clusterMaxZoom, + Object? clusterProperties, + bool? lineMetrics, + bool? generateId, + String? promoteId, + ) { + return GeojsonSourceProperties( + data: data ?? this.data, + maxzoom: maxzoom ?? this.maxzoom, + attribution: attribution ?? this.attribution, + buffer: buffer ?? this.buffer, + tolerance: tolerance ?? this.tolerance, + cluster: cluster ?? this.cluster, + clusterRadius: clusterRadius ?? this.clusterRadius, + clusterMaxZoom: clusterMaxZoom ?? this.clusterMaxZoom, + clusterProperties: clusterProperties ?? this.clusterProperties, + lineMetrics: lineMetrics ?? this.lineMetrics, + generateId: generateId ?? this.generateId, + promoteId: promoteId ?? this.promoteId, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "geojson"; + addIfPresent('data', data); + addIfPresent('maxzoom', maxzoom); + addIfPresent('attribution', attribution); + addIfPresent('buffer', buffer); + addIfPresent('tolerance', tolerance); + addIfPresent('cluster', cluster); + addIfPresent('clusterRadius', clusterRadius); + addIfPresent('clusterMaxZoom', clusterMaxZoom); + addIfPresent('clusterProperties', clusterProperties); + addIfPresent('lineMetrics', lineMetrics); + addIfPresent('generateId', generateId); + addIfPresent('promoteId', promoteId); + return json; + } + + factory GeojsonSourceProperties.fromJson(Map json) { + return GeojsonSourceProperties( + data: json['data'], + maxzoom: json['maxzoom'], + attribution: json['attribution'], + buffer: json['buffer'], + tolerance: json['tolerance'], + cluster: json['cluster'], + clusterRadius: json['clusterRadius'], + clusterMaxZoom: json['clusterMaxZoom'], + clusterProperties: json['clusterProperties'], + lineMetrics: json['lineMetrics'], + generateId: json['generateId'], + promoteId: json['promoteId'], + ); + } +} + +class VideoSourceProperties implements SourceProperties { + /// URLs to video content in order of preferred format. + /// + /// Type: array + final List? urls; + + /// Corners of video specified in longitude, latitude pairs. + /// + /// Type: array + final List? coordinates; + + const VideoSourceProperties({ + this.urls, + this.coordinates, + }); + + VideoSourceProperties copyWith( + List? urls, + List? coordinates, + ) { + return VideoSourceProperties( + urls: urls ?? this.urls, + coordinates: coordinates ?? this.coordinates, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "video"; + addIfPresent('urls', urls); + addIfPresent('coordinates', coordinates); + return json; + } + + factory VideoSourceProperties.fromJson(Map json) { + return VideoSourceProperties( + urls: json['urls'], + coordinates: json['coordinates'], + ); + } +} + +class ImageSourceProperties implements SourceProperties { + /// URL that points to an image. + /// + /// Type: string + final String? url; + + /// Corners of image specified in longitude, latitude pairs. + /// + /// Type: array + final List? coordinates; + + const ImageSourceProperties({ + this.url, + this.coordinates, + }); + + ImageSourceProperties copyWith( + String? url, + List? coordinates, + ) { + return ImageSourceProperties( + url: url ?? this.url, + coordinates: coordinates ?? this.coordinates, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + json["type"] = "image"; + addIfPresent('url', url); + addIfPresent('coordinates', coordinates); + return json; + } + + factory ImageSourceProperties.fromJson(Map json) { + return ImageSourceProperties( + url: json['url'], + coordinates: json['coordinates'], + ); + } +} diff --git a/mapbox_gl_platform_interface/lib/src/symbol.dart b/mapbox_gl_platform_interface/lib/src/symbol.dart index 87eaa79b5..5e088d538 100644 --- a/mapbox_gl_platform_interface/lib/src/symbol.dart +++ b/mapbox_gl_platform_interface/lib/src/symbol.dart @@ -6,7 +6,7 @@ part of mapbox_gl_platform_interface; -class Symbol { +class Symbol implements Annotation { Symbol(this._id, this.options, [this._data]); /// A unique identifier for this symbol. @@ -25,6 +25,21 @@ class Symbol { /// The returned value does not reflect any changes made to the symbol through /// touch events. Add listeners to the owning map controller to track those. SymbolOptions options; + + @override + Map toGeoJson() { + final geojson = options.toGeoJson(); + geojson["id"] = id; + geojson["properties"]["id"] = id; + + return geojson; + } + + @override + void translate(LatLng delta) { + options = options + .copyWith(SymbolOptions(geometry: this.options.geometry! + delta)); + } } dynamic _offsetToJson(Offset? offset) { @@ -79,6 +94,8 @@ class SymbolOptions { final double? iconRotate; final Offset? iconOffset; final String? iconAnchor; + + /// Not supported on web final List? fontNames; final String? textField; final double? textSize; @@ -138,7 +155,7 @@ class SymbolOptions { ); } - dynamic toJson() { + dynamic toJson([bool addGeometry = true]) { final Map json = {}; void addIfPresent(String fieldName, dynamic value) { @@ -172,9 +189,22 @@ class SymbolOptions { addIfPresent('textHaloColor', textHaloColor); addIfPresent('textHaloWidth', textHaloWidth); addIfPresent('textHaloBlur', textHaloBlur); - addIfPresent('geometry', geometry?.toJson()); + if (addGeometry) { + addIfPresent('geometry', geometry?.toJson()); + } addIfPresent('zIndex', zIndex); addIfPresent('draggable', draggable); return json; } + + Map toGeoJson() { + return { + "type": "Feature", + "properties": toJson(false), + "geometry": { + "type": "Point", + "coordinates": geometry!.toGeoJsonCoordinates() + } + }; + } } diff --git a/mapbox_gl_platform_interface/lib/src/ui.dart b/mapbox_gl_platform_interface/lib/src/ui.dart index e9476b2cb..6a766d8fa 100644 --- a/mapbox_gl_platform_interface/lib/src/ui.dart +++ b/mapbox_gl_platform_interface/lib/src/ui.dart @@ -15,6 +15,9 @@ class MapboxStyles { /// style will always use the latest version and may change as we improve the style. static const String LIGHT = "mapbox://styles/mapbox/light-v10"; + /// Empty: Basic empty style + static const String EMPTY = "mapbox://styles/mapbox/empty-v8"; + /// Dark: Subtle dark backdrop for data visualizations. Using this constant means your map style /// will always use the latest version and may change as we improve the style. static const String DARK = "mapbox://styles/mapbox/dark-v10"; @@ -67,6 +70,14 @@ enum CompassViewPosition { BottomRight, } +/// Attribution Button Position +enum AttributionButtonPosition { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + /// Bounds for the map camera target. // Used with [MapboxMapOptions] to wrap a [LatLngBounds] value. This allows // distinguishing between specifying an unbounded target (null `LatLngBounds`) @@ -132,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 b6b027a1b..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.12.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 4419cbfc5..854f294c6 100644 --- a/mapbox_gl_web/CHANGELOG.md +++ b/mapbox_gl_web/CHANGELOG.md @@ -1,10 +1,40 @@ -## 0.12.0, April 12, 2020 +## 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) +* 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) +* Full style source support [#797](https://github.com/flutter-mapbox-gl/maps/pull/797) +* Gesture fixes [#851](https://github.com/flutter-mapbox-gl/maps/pull/851) +* 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 +* 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) +* Add getSymbolLatLng and getLineLatLngs for web [#720](https://github.com/tobrun/flutter-mapbox-gl/pull/720) + +## 0.13.0, October 21, 2021 +* Migrate to null-safety [#607](https://github.com/tobrun/flutter-mapbox-gl/pull/607) +* Add missing removeLines removeCircles and removeFills [#622](https://github.com/tobrun/flutter-mapbox-gl/pull/622) +* Fix Warning: Operand of null-aware operation '!' has type 'Locale' which excludes null [#676](https://github.com/tobrun/flutter-mapbox-gl/pull/676) + +## 0.12.0, April 12, 2021 * Dependencies: updated image package [#598](https://github.com/tobrun/flutter-mapbox-gl/pull/598) * Fix feature manager on release build [#593](https://github.com/tobrun/flutter-mapbox-gl/pull/593) * Emit onTap only for the feature above the others [#589](https://github.com/tobrun/flutter-mapbox-gl/pull/589) * Add annotationOrder to web [#588](https://github.com/tobrun/flutter-mapbox-gl/pull/588) -## 0.11.0, March 30, 2020 +## 0.11.0, March 30, 2021 * Fix Mapbox GL JS CSS embedding on web [#551](https://github.com/tobrun/flutter-mapbox-gl/pull/551) * Add batch mode of screen locations [#554](https://github.com/tobrun/flutter-mapbox-gl/pull/554) diff --git a/mapbox_gl_web/lib/mapbox_gl_web.dart b/mapbox_gl_web/lib/mapbox_gl_web.dart index da703e15c..0c154a4e6 100644 --- a/mapbox_gl_web/lib/mapbox_gl_web.dart +++ b/mapbox_gl_web/lib/mapbox_gl_web.dart @@ -1,30 +1,31 @@ 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; +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; +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'; part 'src/mapbox_map_plugin.dart'; part 'src/options_sink.dart'; -part 'src/feature_manager/feature_manager.dart'; -part 'src/feature_manager/symbol_manager.dart'; -part 'src/feature_manager/line_manager.dart'; -part 'src/feature_manager/circle_manager.dart'; -part 'src/feature_manager/fill_manager.dart'; -part 'src/mapbox_map_controller.dart'; +part 'src/mapbox_web_gl_platform.dart'; diff --git a/mapbox_gl_web/lib/src/convert.dart b/mapbox_gl_web/lib/src/convert.dart index 3b533c8ce..f51d40779 100644 --- a/mapbox_gl_web/lib/src/convert.dart +++ b/mapbox_gl_web/lib/src/convert.dart @@ -26,21 +26,23 @@ class Convert { sink.setMinMaxZoomPreference(options['minMaxZoomPreference'][0], options['minMaxZoomPreference'][1]); } - if (options.containsKey('rotateGesturesEnabled')) { - sink.setRotateGesturesEnabled(options['rotateGesturesEnabled']); - } - if (options.containsKey('scrollGesturesEnabled')) { - sink.setScrollGesturesEnabled(options['scrollGesturesEnabled']); - } - if (options.containsKey('tiltGesturesEnabled')) { - sink.setTiltGesturesEnabled(options['tiltGesturesEnabled']); + if (options['rotateGesturesEnabled'] != null && + options['scrollGesturesEnabled'] != null && + options['tiltGesturesEnabled'] != null && + options['zoomGesturesEnabled'] != null && + options['doubleClickZoomEnabled'] != null) { + sink.setGestures( + rotateGesturesEnabled: options['rotateGesturesEnabled'], + scrollGesturesEnabled: options['scrollGesturesEnabled'], + tiltGesturesEnabled: options['tiltGesturesEnabled'], + zoomGesturesEnabled: options['zoomGesturesEnabled'], + doubleClickZoomEnabled: options['doubleClickZoomEnabled']); } + if (options.containsKey('trackCameraPosition')) { sink.setTrackCameraPosition(options['trackCameraPosition']); } - if (options.containsKey('zoomGesturesEnabled')) { - sink.setZoomGesturesEnabled(options['zoomGesturesEnabled']); - } + if (options.containsKey('myLocationEnabled')) { sink.setMyLocationEnabled(options['myLocationEnabled']); } @@ -56,12 +58,19 @@ class Convert { options['logoViewMargins'][0], options['logoViewMargins'][1]); } if (options.containsKey('compassViewPosition')) { - sink.setCompassGravity(options['compassViewPosition']); + final position = + CompassViewPosition.values[options['compassViewPosition']]; + sink.setCompassAlignment(position); } if (options.containsKey('compassViewMargins')) { sink.setCompassViewMargins( options['compassViewMargins'][0], options['compassViewMargins'][1]); } + if (options.containsKey('attributionButtonPosition')) { + final position = AttributionButtonPosition + .values[options['attributionButtonPosition']]; + sink.setAttributionButtonAlignment(position); + } if (options.containsKey('attributionButtonMargins')) { sink.setAttributionButtonMargins(options['attributionButtonMargins'][0], options['attributionButtonMargins'][1]); @@ -203,7 +212,10 @@ class Convert { properties['iconRotate'] = options.iconRotate; } if (options.iconOffset != null) { - properties['iconOffset'] = [options.iconOffset!.dx, options.iconOffset!.dy]; + properties['iconOffset'] = [ + options.iconOffset!.dx, + options.iconOffset!.dy + ]; } if (options.iconAnchor != null) { properties['iconAnchor'] = options.iconAnchor; @@ -233,7 +245,10 @@ class Convert { properties['textTransform'] = options.textTransform; } if (options.textOffset != null) { - properties['textOffset'] = [options.textOffset!.dx, options.textOffset!.dy]; + properties['textOffset'] = [ + options.textOffset!.dx, + options.textOffset!.dy + ]; } if (options.iconOpacity != null) { properties['iconOpacity'] = options.iconOpacity; diff --git a/mapbox_gl_web/lib/src/feature_manager/circle_manager.dart b/mapbox_gl_web/lib/src/feature_manager/circle_manager.dart deleted file mode 100644 index 94269242f..000000000 --- a/mapbox_gl_web/lib/src/feature_manager/circle_manager.dart +++ /dev/null @@ -1,43 +0,0 @@ -part of mapbox_gl_web; - -class CircleManager extends FeatureManager { - CircleManager({ - required MapboxMap map, - ArgumentCallbacks? onTap, - }) : super( - sourceId: 'circle_source', - layerId: 'circle_layer', - map: map, - onTap: onTap, - ); - - @override - void initLayer() { - map.addLayer({ - 'id': layerId, - 'type': 'circle', - 'source': sourceId, - 'paint': { - 'circle-radius': ['get', 'circleRadius'], - 'circle-color': ['get', 'circleColor'], - 'circle-blur': ['get', 'circleBlur'], - 'circle-opacity': ['get', 'circleOpacity'], - 'circle-stroke-width': ['get', 'circleStrokeWidth'], - 'circle-stroke-color': ['get', 'circleStrokeColor'], - 'circle-stroke-opacity': ['get', 'circleStrokeOpacity'], - } - }); - } - - @override - void onDrag(String featureId, LatLng latLng) { - update(featureId, CircleOptions(geometry: latLng)); - } - - @override - void update(String lineId, CircleOptions changes) { - Feature olfFeature = getFeature(lineId)!; - Feature newFeature = Convert.interpretCircleOptions(changes, olfFeature); - updateFeature(newFeature); - } -} diff --git a/mapbox_gl_web/lib/src/feature_manager/feature_manager.dart b/mapbox_gl_web/lib/src/feature_manager/feature_manager.dart deleted file mode 100644 index a4bcfbfb0..000000000 --- a/mapbox_gl_web/lib/src/feature_manager/feature_manager.dart +++ /dev/null @@ -1,115 +0,0 @@ -part of mapbox_gl_web; - -abstract class FeatureManager { - final String sourceId; - final String layerId; - final MapboxMap map; - final ArgumentCallbacks? onTap; - @protected - late LatLng dragOrigin; - - final Map _features = {}; - num featureCounter = 1; - String? _draggableFeatureId; - - FeatureManager({ - required this.sourceId, - required this.layerId, - required this.map, - this.onTap, - }) { - var featureSource = GeoJsonSource(data: FeatureCollection(features: [])); - map.addSource(sourceId, featureSource); - initLayer(); - _initClickHandler(); - _initDragHandler(); - } - - void initLayer(); - - void update(String featureId, T changes); - - void onDrag(String featureId, LatLng latLng); - - String add(Feature feature) { - feature.id = featureCounter++; - _features['${feature.id}'] = feature; - _updateSource(); - return '${feature.id}'; - } - - void updateFeature(Feature feature) { - updateFeatures([feature]); - } - - void updateFeatures(Iterable features) { - features.forEach((feature) => _features['${feature.id}'] = feature); - _updateSource(); - } - - void remove(String featureId) { - removeAll([featureId]); - } - - void removeAll(Iterable featuresIds) { - featuresIds.forEach((featureId) => _features.remove(featureId)); - _updateSource(); - } - - Feature? getFeature(String featureId) { - return _features[featureId]; - } - - void _initClickHandler() { - map.on('click', (e) { - if (e is Event) { - final features = map.queryRenderedFeatures([e.point.x, e.point.y]); - if (features.isNotEmpty && features[0].source == sourceId) { - if (onTap != null) { - onTap!('${features[0].id}'); - } - } - } - }); - - map.on('mouseenter', layerId, (_) { - map.getCanvas().style.cursor = 'pointer'; - }); - - map.on('mouseleave', layerId, (_) { - map.getCanvas().style.cursor = ''; - }); - } - - void _initDragHandler() { - map.on('mousedown', layerId, (e) { - var isDraggable = e.features[0].properties['draggable']; - if (isDraggable != null && isDraggable) { - // Prevent the default map drag behavior. - e.preventDefault(); - _draggableFeatureId = '${e.features[0].id}'; - map.getCanvas().style.cursor = 'grabbing'; - var coords = e.lngLat; - dragOrigin = LatLng(coords.lat as double, coords.lng as double); - } - }); - - map.on('mousemove', (e) { - if (_draggableFeatureId != null) { - var coords = e.lngLat; - onDrag(_draggableFeatureId!, LatLng(coords.lat, coords.lng)); - } - }); - - map.on('mouseup', (_) { - _draggableFeatureId = null; - map.getCanvas().style.cursor = ''; - }); - } - - void _updateSource() { - GeoJsonSource featureSource = map.getSource(sourceId); - featureSource - .setData(FeatureCollection(features: _features.values.toList())); - } -} diff --git a/mapbox_gl_web/lib/src/feature_manager/fill_manager.dart b/mapbox_gl_web/lib/src/feature_manager/fill_manager.dart deleted file mode 100644 index 14def9505..000000000 --- a/mapbox_gl_web/lib/src/feature_manager/fill_manager.dart +++ /dev/null @@ -1,46 +0,0 @@ -part of mapbox_gl_web; - -class FillManager extends FeatureManager { - FillManager({ - required MapboxMap map, - ArgumentCallbacks? onTap, - }) : super( - sourceId: 'fill_source', - layerId: 'fill_layer', - map: map, - onTap: onTap, - ); - - @override - void initLayer() { - map.addLayer({ - 'id': layerId, - 'type': 'fill', - 'source': sourceId, - 'paint': { - 'fill-color': ['get', 'fillColor'], - 'fill-opacity': ['get', 'fillOpacity'], - 'fill-outline-color': ['get', 'fillOutlineColor'], - } - }); - } - - @override - void onDrag(String featureId, LatLng latLng) { - Feature oldFeature = getFeature(featureId)!; - final geometry = - Convert.featureGeometryToFillGeometry(oldFeature.geometry.coordinates); - update( - featureId, - translateFillOptions( - FillOptions(geometry: geometry), latLng - dragOrigin)); - dragOrigin = latLng; - } - - @override - void update(String featureId, FillOptions changes) { - Feature oldFeature = getFeature(featureId)!; - Feature newFeature = Convert.intepretFillOptions(changes, oldFeature); - updateFeature(newFeature); - } -} diff --git a/mapbox_gl_web/lib/src/feature_manager/line_manager.dart b/mapbox_gl_web/lib/src/feature_manager/line_manager.dart deleted file mode 100644 index 59c3a71ed..000000000 --- a/mapbox_gl_web/lib/src/feature_manager/line_manager.dart +++ /dev/null @@ -1,47 +0,0 @@ -part of mapbox_gl_web; - -class LineManager extends FeatureManager { - LineManager({ - required MapboxMap map, - ArgumentCallbacks? onTap, - }) : super( - sourceId: 'line_source', - layerId: 'line_layer', - map: map, - onTap: onTap, - ); - - @override - void initLayer() { - // NOTE: line-pattern disable line-color - map.addLayer({ - 'id': layerId, - 'type': 'line', - 'source': sourceId, - 'layout': { - 'line-join': ['get', 'lineJoin'], - }, - 'paint': { - 'line-opacity': ['get', 'lineOpacity'], - 'line-color': ['get', 'lineColor'], - 'line-width': ['get', 'lineWidth'], - 'line-gap-width': ['get', 'lineGapWidth'], - 'line-offset': ['get', 'lineOffset'], - 'line-blur': ['get', 'lineBlur'], - //'line-pattern': ['get', 'linePattern'], - } - }); - } - - void update(String lineId, LineOptions changes) { - Feature olfFeature = getFeature(lineId)!; - Feature newFeature = Convert.interpretLineOptions(changes, olfFeature); - updateFeature(newFeature); - } - - @override - void onDrag(String featureId, LatLng latLng) { - // TODO: implement onDrag - print('onDrag is not already implemented'); - } -} diff --git a/mapbox_gl_web/lib/src/feature_manager/symbol_manager.dart b/mapbox_gl_web/lib/src/feature_manager/symbol_manager.dart deleted file mode 100644 index a3d2e7567..000000000 --- a/mapbox_gl_web/lib/src/feature_manager/symbol_manager.dart +++ /dev/null @@ -1,93 +0,0 @@ -part of mapbox_gl_web; - -class SymbolManager extends FeatureManager { - SymbolManager({ - required MapboxMap map, - ArgumentCallbacks? onTap, - }) : super( - sourceId: 'symbol_source', - layerId: 'symbol_layer', - map: map, - onTap: onTap, - ); - - @override - void initLayer() { - map.addLayer({ - 'id': layerId, - 'type': 'symbol', - 'source': sourceId, - 'layout': { - 'icon-image': '{iconImage}', - 'icon-size': ['get', 'iconSize'], - 'icon-rotate': ['get', 'iconRotate'], - 'icon-offset': ['get', 'iconOffset'], - 'icon-anchor': ['get', 'iconAnchor'], - 'text-field': ['get', 'textField'], - 'text-size': ['get', 'textSize'], - 'text-max-width': ['get', 'textMaxWidth'], - 'text-letter-spacing': ['get', 'textLetterSpacing'], - 'text-justify': ['get', 'textJustify'], - 'text-anchor': ['get', 'textAnchor'], - 'text-rotate': ['get', 'textRotate'], - 'text-transform': ['get', 'textTransform'], - 'text-offset': ['get', 'textOffset'], - 'symbol-sort-key': ['get', 'symbolSortKey'], - 'icon-allow-overlap': true, - 'icon-ignore-placement': true, - 'text-allow-overlap': true, - 'text-ignore-placement': true, - }, - 'paint': { - 'icon-opacity': ['get', 'iconOpacity'], - 'icon-color': ['get', 'iconColor'], - 'icon-halo-color': ['get', 'iconHaloColor'], - 'icon-halo-width': ['get', 'iconHaloWidth'], - 'icon-halo-blur': ['get', 'iconHaloBlur'], - 'text-opacity': ['get', 'textOpacity'], - 'text-color': ['get', 'textColor'], - 'text-halo-color': ['get', 'textHaloColor'], - 'text-halo-width': ['get', 'textHaloWidth'], - 'text-halo-blur': ['get', 'textHaloBlur'], - } - }); - - map.on('styleimagemissing', (event) { - if (event.id == '') { - return; - } - var density = context['window'].devicePixelRatio ?? 1; - var imagePath = density == 1 - ? '/assets/assets/symbols/custom-icon.png' - : '/assets/assets/symbols/$density.0x/custom-icon.png'; - map.loadImage(imagePath, (error, image) { - if (error != null) throw error; - if (!map.hasImage(event.id)) - map.addImage(event.id, image, {'pixelRatio': density}); - }); - }); - } - - @override - void update(String lineId, SymbolOptions changes) { - updateAll({lineId: changes}); - } - - void updateAll(Map changesById) { - List featuresWithUpdatedOptions = []; - changesById.forEach( - (id, options) => featuresWithUpdatedOptions.add( - Convert.interpretSymbolOptions( - options, - getFeature(id)!, - ), - ), - ); - updateFeatures(featuresWithUpdatedOptions); - } - - @override - void onDrag(String featureId, LatLng latLng) { - update(featureId, SymbolOptions(geometry: latLng)); - } -} diff --git a/mapbox_gl_web/lib/src/layer_tools.dart b/mapbox_gl_web/lib/src/layer_tools.dart new file mode 100644 index 000000000..b6564111b --- /dev/null +++ b/mapbox_gl_web/lib/src/layer_tools.dart @@ -0,0 +1,58 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +const _layoutProperties = { + "symbol-placement", + "symbol-spacing", + "symbol-avoid-edges", + "symbol-sort-key", + "symbol-z-order", + "icon-allow-overlap", + "icon-ignore-placement", + "icon-optional", + "icon-rotation-alignment", + "icon-size", + "icon-text-fit", + "icon-text-fit-padding", + "icon-image", + "icon-rotate", + "icon-padding", + "icon-keep-upright", + "icon-offset", + "icon-anchor", + "icon-pitch-alignment", + "text-pitch-alignment", + "text-rotation-alignment", + "text-field", + "text-font", + "text-size", + "text-max-width", + "text-line-height", + "text-letter-spacing", + "text-justify", + "text-radial-offset", + "text-variable-anchor", + "text-anchor", + "text-max-angle", + "text-writing-mode", + "text-rotate", + "text-padding", + "text-keep-upright", + "text-transform", + "text-offset", + "text-allow-overlap", + "text-ignore-placement", + "text-optional", + "visibility", + "circle-sort-key", + "line-cap", + "line-join", + "line-miter-limit", + "line-round-limit", + "line-sort-key", + "fill-sort-key", +}; + +bool isLayoutProperty(String property) { + return _layoutProperties.contains(property); +} diff --git a/mapbox_gl_web/lib/src/mapbox_map_controller.dart b/mapbox_gl_web/lib/src/mapbox_map_controller.dart deleted file mode 100644 index 3fdc4cdb5..000000000 --- a/mapbox_gl_web/lib/src/mapbox_map_controller.dart +++ /dev/null @@ -1,756 +0,0 @@ -part of mapbox_gl_web; - -const _mapboxGlCssUrl = - 'https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css'; - -class MapboxMapController extends MapboxGlPlatform - implements MapboxMapOptionsSink { - late DivElement _mapElement; - - late Map _creationParams; - late MapboxMap _map; - - List annotationOrder = []; - late SymbolManager symbolManager; - late LineManager lineManager; - late CircleManager circleManager; - late FillManager fillManager; - - bool _trackCameraPosition = false; - GeolocateControl? _geolocateControl; - LatLng? _myLastLocation; - - String? _navigationControlPosition; - NavigationControl? _navigationControl; - - @override - Widget buildView( - Map creationParams, - OnPlatformViewCreatedCallback onPlatformViewCreated, - Set>? gestureRecognizers) { - _creationParams = creationParams; - _registerViewFactory(onPlatformViewCreated, this.hashCode); - return HtmlElementView( - viewType: 'plugins.flutter.io/mapbox_gl_${this.hashCode}'); - } - - void _registerViewFactory(Function(int) callback, int identifier) { - // ignore: undefined_prefixed_name - ui.platformViewRegistry.registerViewFactory( - 'plugins.flutter.io/mapbox_gl_$identifier', (int viewId) { - _mapElement = DivElement(); - callback(viewId); - return _mapElement; - }); - } - - @override - Future initPlatform(int id) async { - await _addStylesheetToShadowRoot(_mapElement); - if (_creationParams.containsKey('initialCameraPosition')) { - var camera = _creationParams['initialCameraPosition']; - if (_creationParams.containsKey('accessToken')) { - Mapbox.accessToken = _creationParams['accessToken']; - } - _map = MapboxMap( - MapOptions( - container: _mapElement, - style: 'mapbox://styles/mapbox/streets-v11', - center: LngLat(camera['target'][1], camera['target'][0]), - zoom: camera['zoom'], - bearing: camera['bearing'], - pitch: camera['tilt'], - ), - ); - _map.on('load', _onStyleLoaded); - } - Convert.interpretMapboxMapOptions(_creationParams['options'], this); - - if (_creationParams.containsKey('annotationOrder')) { - annotationOrder = _creationParams['annotationOrder']; - } - } - - Future _addStylesheetToShadowRoot(HtmlElement e) async { - LinkElement link = LinkElement() - ..href = _mapboxGlCssUrl - ..rel = 'stylesheet'; - e.append(link); - - await link.onLoad.first; - } - - @override - Future updateMapOptions( - Map optionsUpdate) async { - // FIX: why is called indefinitely? (map_ui page) - Convert.interpretMapboxMapOptions(optionsUpdate, this); - return _getCameraPosition(); - } - - @override - Future animateCamera(CameraUpdate cameraUpdate) async { - final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); - _map.flyTo(cameraOptions); - return true; - } - - @override - Future moveCamera(CameraUpdate cameraUpdate) async { - final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); - _map.jumpTo(cameraOptions); - return true; - } - - @override - Future updateMyLocationTrackingMode( - MyLocationTrackingMode myLocationTrackingMode) async { - setMyLocationTrackingMode(myLocationTrackingMode.index); - } - - @override - Future matchMapLanguageWithDeviceDefault() async { - setMapLanguage(ui.window.locale!.languageCode); - } - - @override - Future setMapLanguage(String language) async { - _map.setLayoutProperty( - 'country-label', - 'text-field', - ['get', 'name_' + language], - ); - } - - @override - Future setTelemetryEnabled(bool enabled) async { - print('Telemetry not available in web'); - return; - } - - @override - Future getTelemetryEnabled() async { - print('Telemetry not available in web'); - return false; - } - - @override - Future> addSymbols(List options, - [List? data]) async { - Map optionsById = { - for (final o in options) - symbolManager.add(Feature( - geometry: Geometry( - type: 'Point', - coordinates: [o.geometry!.longitude, o.geometry!.latitude], - ), - )): o, - }; - symbolManager.updateAll(optionsById); - - return optionsById - .map((id, singleOptions) { - int dataIndex = options.indexOf(singleOptions); - Map? singleData = data != null && data.length >= dataIndex + 1 - ? data[dataIndex] - : null; - return MapEntry(id, Symbol(id, singleOptions, singleData)); - }) - .values - .toList(); - } - - @override - Future updateSymbol(Symbol symbol, SymbolOptions changes) async { - symbolManager.update(symbol.id, changes); - } - - @override - Future removeSymbols(Iterable symbolsIds) async { - symbolManager.removeAll(symbolsIds); - } - - @override - Future addLine(LineOptions options, [Map? data]) async { - String lineId = lineManager.add(Feature( - geometry: Geometry( - type: 'LineString', - coordinates: options.geometry! - .map((latLng) => [latLng.longitude, latLng.latitude]) - .toList(), - ), - )); - lineManager.update(lineId, options); - return Line(lineId, options, data); - } - - @override - Future updateLine(Line line, LineOptions changes) async { - lineManager.update(line.id, changes); - } - - @override - Future removeLine(String lineId) async { - lineManager.remove(lineId); - } - - @override - Future removeLines(Iterable ids) async { - lineManager.removeAll(ids); - } - - @override - Future addCircle(CircleOptions options, [Map? data]) async { - String circleId = circleManager.add(Feature( - geometry: Geometry( - type: 'Point', - coordinates: [options.geometry!.longitude, options.geometry!.latitude], - ), - )); - circleManager.update(circleId, options); - return Circle(circleId, options, data); - } - - @override - Future updateCircle(Circle circle, CircleOptions changes) async { - circleManager.update(circle.id, changes); - } - - @override - Future getCircleLatLng(Circle circle) async { - var coordinates = circleManager.getFeature(circle.id)!.geometry.coordinates; - return LatLng(coordinates[1], coordinates[0]); - } - - @override - Future removeCircle(String circleId) async { - circleManager.remove(circleId); - } - - @override - Future removeCircles(Iterable ids) async { - circleManager.removeAll(ids); - } - - Future addFill(FillOptions options, [Map? data]) async { - String fillId = fillManager.add(Feature( - geometry: Geometry( - type: 'Polygon', - coordinates: Convert.fillGeometryToFeatureGeometry(options.geometry!), - ), - )); - - fillManager.update(fillId, options); - return Fill(fillId, options, data); - } - - Future updateFill(Fill fill, FillOptions changes) async { - fillManager.update(fill.id, changes); - } - - Future removeFill(String fillId) async { - fillManager.remove(fillId); - } - - @override - Future removeFills(Iterable ids) async { - fillManager.removeAll(ids); - } - - @override - Future queryRenderedFeatures( - Point point, List layerIds, List? filter) async { - Map options = {}; - if (layerIds.length > 0) { - options['layers'] = layerIds; - } - if (filter != null) { - options['filter'] = filter; - } - return _map - .queryRenderedFeatures([point, point], options) - .map((feature) => { - 'type': 'Feature', - 'id': feature.id as int?, - 'geometry': { - 'type': feature.geometry.type, - 'coordinates': feature.geometry.coordinates, - }, - 'properties': feature.properties, - 'source': feature.source, - }) - .toList(); - } - - @override - Future queryRenderedFeaturesInRect( - Rect rect, List layerIds, String? filter) async { - Map options = {}; - if (layerIds.length > 0) { - options['layers'] = layerIds; - } - if (filter != null) { - options['filter'] = filter; - } - return _map - .queryRenderedFeatures([ - Point(rect.left, rect.bottom), - Point(rect.right, rect.top), - ], options) - .map((feature) => { - 'type': 'Feature', - 'id': feature.id as int?, - 'geometry': { - 'type': feature.geometry.type, - 'coordinates': feature.geometry.coordinates, - }, - 'properties': feature.properties, - 'source': feature.source, - }) - .toList(); - } - - @override - Future invalidateAmbientCache() async { - print('Offline storage not available in web'); - } - - @override - Future requestMyLocationLatLng() async { - return _myLastLocation; - } - - @override - Future getVisibleRegion() async { - final bounds = _map.getBounds(); - return LatLngBounds( - southwest: LatLng( - bounds.getSouthWest().lat as double, - bounds.getSouthWest().lng as double, - ), - northeast: LatLng( - bounds.getNorthEast().lat as double, - bounds.getNorthEast().lng as double, - ), - ); - } - - @override - Future addImage(String name, Uint8List bytes, - [bool sdf = false]) async { - final photo = decodeImage(bytes)!; - if (!_map.hasImage(name)) { - _map.addImage( - name, - { - 'width': photo.width, - 'height': photo.height, - 'data': photo.getBytes(), - }, - {'sdf': sdf}, - ); - } - } - - @override - Future setSymbolIconAllowOverlap(bool enable) async { - //TODO: to implement - print('setSymbolIconAllowOverlap not implemented yet'); - } - - @override - Future setSymbolIconIgnorePlacement(bool enable) async { - //TODO: to implement - print('setSymbolIconIgnorePlacement not implemented yet'); - } - - @override - Future setSymbolTextAllowOverlap(bool enable) async { - //TODO: to implement - print('setSymbolTextAllowOverlap not implemented yet'); - } - - @override - Future setSymbolTextIgnorePlacement(bool enable) async { - //TODO: to implement - print('setSymbolTextIgnorePlacement not implemented yet'); - } - - CameraPosition? _getCameraPosition() { - if (_trackCameraPosition) { - final center = _map.getCenter(); - return CameraPosition( - bearing: _map.getBearing() as double, - target: LatLng(center.lat as double, center.lng as double), - tilt: _map.getPitch() as double, - zoom: _map.getZoom() as double, - ); - } - return null; - } - - void _onStyleLoaded(_) { - for (final annotationType in annotationOrder) { - switch (annotationType) { - case 'AnnotationType.symbol': - symbolManager = - SymbolManager(map: _map, onTap: onSymbolTappedPlatform); - break; - case 'AnnotationType.line': - lineManager = LineManager(map: _map, onTap: onLineTappedPlatform); - break; - case 'AnnotationType.circle': - circleManager = - CircleManager(map: _map, onTap: onCircleTappedPlatform); - break; - case 'AnnotationType.fill': - fillManager = FillManager(map: _map, onTap: onFillTappedPlatform); - break; - default: - print( - "Unknown annotation type: \(annotationType), must be either 'fill', 'line', 'circle' or 'symbol'"); - } - } - - onMapStyleLoadedPlatform(null); - _map.on('click', _onMapClick); - // long click not available in web, so it is mapped to double click - _map.on('dblclick', _onMapLongClick); - _map.on('movestart', _onCameraMoveStarted); - _map.on('move', _onCameraMove); - _map.on('moveend', _onCameraIdle); - _map.on('resize', _onMapResize); - } - - void _onMapResize(Event e) { - Timer(Duration(microseconds: 10), () { - var container = _map.getContainer(); - var canvas = _map.getCanvas(); - var widthMismatch = canvas.clientWidth != container.clientWidth; - var heightMismatch = canvas.clientHeight != container.clientHeight; - if (widthMismatch || heightMismatch) { - _map.resize(); - } - }); - } - - void _onMapClick(e) { - onMapClickPlatform({ - 'point': Point(e.point.x, e.point.y), - 'latLng': LatLng(e.lngLat.lat, e.lngLat.lng), - }); - } - - void _onMapLongClick(e) { - onMapLongClickPlatform({ - 'point': Point(e.point.x, e.point.y), - 'latLng': LatLng(e.lngLat.lat, e.lngLat.lng), - }); - } - - void _onCameraMoveStarted(_) { - onCameraMoveStartedPlatform(null); - } - - void _onCameraMove(_) { - final center = _map.getCenter(); - var camera = CameraPosition( - bearing: _map.getBearing() as double, - target: LatLng(center.lat as double, center.lng as double), - tilt: _map.getPitch() as double, - zoom: _map.getZoom() as double, - ); - onCameraMovePlatform(camera); - } - - void _onCameraIdle(_) { - final center = _map.getCenter(); - var camera = CameraPosition( - bearing: _map.getBearing() as double, - target: LatLng(center.lat as double, center.lng as double), - tilt: _map.getPitch() as double, - zoom: _map.getZoom() as double, - ); - onCameraIdlePlatform(camera); - } - - void _onCameraTrackingChanged(bool isTracking) { - if (isTracking) { - onCameraTrackingChangedPlatform(MyLocationTrackingMode.Tracking); - } else { - onCameraTrackingChangedPlatform(MyLocationTrackingMode.None); - } - } - - void _onCameraTrackingDismissed() { - onCameraTrackingDismissedPlatform(null); - } - - void _addGeolocateControl({bool trackUserLocation = false}) { - _removeGeolocateControl(); - _geolocateControl = GeolocateControl( - GeolocateControlOptions( - positionOptions: PositionOptions(enableHighAccuracy: true), - trackUserLocation: trackUserLocation, - showAccuracyCircle: true, - showUserLocation: true, - ), - ); - _geolocateControl!.on('geolocate', (e) { - _myLastLocation = LatLng(e.coords.latitude, e.coords.longitude); - onUserLocationUpdatedPlatform(UserLocation( - position: LatLng(e.coords.latitude, e.coords.longitude), - altitude: e.coords.altitude, - bearing: e.coords.heading, - speed: e.coords.speed, - horizontalAccuracy: e.coords.accuracy, - verticalAccuracy: e.coords.altitudeAccuracy, - heading: null, - timestamp: DateTime.fromMillisecondsSinceEpoch(e.timestamp))); - }); - _geolocateControl!.on('trackuserlocationstart', (_) { - _onCameraTrackingChanged(true); - }); - _geolocateControl!.on('trackuserlocationend', (_) { - _onCameraTrackingChanged(false); - _onCameraTrackingDismissed(); - }); - _map.addControl(_geolocateControl, 'bottom-right'); - } - - void _removeGeolocateControl() { - if (_geolocateControl != null) { - _map.removeControl(_geolocateControl); - _geolocateControl = null; - } - } - - void _updateNavigationControl({ - bool? compassEnabled, - CompassViewPosition? position, - }) { - bool? prevShowCompass; - if (_navigationControl != null) { - prevShowCompass = _navigationControl!.options.showCompass; - } - String? prevPosition = _navigationControlPosition; - - String? positionString; - switch (position) { - case CompassViewPosition.TopRight: - positionString = 'top-right'; - break; - case CompassViewPosition.TopLeft: - positionString = 'top-left'; - break; - case CompassViewPosition.BottomRight: - positionString = 'bottom-right'; - break; - case CompassViewPosition.BottomLeft: - positionString = 'bottom-left'; - break; - default: - positionString = null; - } - - bool newShowComapss = compassEnabled ?? prevShowCompass ?? false; - String? newPosition = positionString ?? prevPosition ?? null; - - _removeNavigationControl(); - _navigationControl = NavigationControl(NavigationControlOptions( - showCompass: newShowComapss, - showZoom: false, - visualizePitch: false, - )); - - if (newPosition == null) { - _map.addControl(_navigationControl); - } else { - _map.addControl(_navigationControl, newPosition); - _navigationControlPosition = newPosition; - } - } - - void _removeNavigationControl() { - if (_navigationControl != null) { - _map.removeControl(_navigationControl); - _navigationControl = null; - } - } - - /* - * MapboxMapOptionsSink - */ - @override - void setAttributionButtonMargins(int x, int y) { - print('setAttributionButtonMargins not available in web'); - } - - @override - void setCameraTargetBounds(LatLngBounds? bounds) { - if (bounds == null) { - _map.setMaxBounds(null); - } else { - _map.setMaxBounds( - LngLatBounds( - LngLat( - bounds.southwest.longitude, - bounds.southwest.latitude, - ), - LngLat( - bounds.northeast.longitude, - bounds.northeast.latitude, - ), - ), - ); - } - } - - @override - void setCompassEnabled(bool compassEnabled) { - _updateNavigationControl(compassEnabled: compassEnabled); - } - - @override - void setCompassGravity(int gravity) { - _updateNavigationControl(position: CompassViewPosition.values[gravity]); - } - - @override - void setCompassViewMargins(int x, int y) { - print('setCompassViewMargins not available in web'); - } - - @override - void setLogoViewMargins(int x, int y) { - print('setLogoViewMargins not available in web'); - } - - @override - void setMinMaxZoomPreference(num? min, num? max) { - // FIX: why is called indefinitely? (map_ui page) - _map.setMinZoom(min); - _map.setMaxZoom(max); - } - - @override - void setMyLocationEnabled(bool myLocationEnabled) { - if (myLocationEnabled) { - _addGeolocateControl(trackUserLocation: false); - } else { - _removeGeolocateControl(); - } - } - - @override - void setMyLocationRenderMode(int myLocationRenderMode) { - print('myLocationRenderMode not available in web'); - } - - @override - void setMyLocationTrackingMode(int myLocationTrackingMode) { - if (_geolocateControl == null) { - //myLocationEnabled is false, ignore myLocationTrackingMode - return; - } - if (myLocationTrackingMode == 0) { - _addGeolocateControl(trackUserLocation: false); - } else { - print('Only one tracking mode available in web'); - _addGeolocateControl(trackUserLocation: true); - } - } - - @override - void setRotateGesturesEnabled(bool rotateGesturesEnabled) { - if (rotateGesturesEnabled) { - _map.dragRotate.enable(); - _map.touchZoomRotate.enableRotation(); - _map.keyboard.enable(); - } else { - _map.dragRotate.disable(); - _map.touchZoomRotate.disableRotation(); - _map.keyboard.disable(); - } - } - - @override - void setScrollGesturesEnabled(bool scrollGesturesEnabled) { - if (scrollGesturesEnabled) { - _map.dragPan.enable(); - _map.keyboard.enable(); - } else { - _map.dragPan.disable(); - _map.keyboard.disable(); - } - } - - @override - void setStyleString(String? styleString) { - _map.setStyle(styleString); - } - - @override - void setTiltGesturesEnabled(bool tiltGesturesEnabled) { - if (tiltGesturesEnabled) { - _map.dragRotate.enable(); - _map.keyboard.enable(); - } else { - _map.dragRotate.disable(); - _map.keyboard.disable(); - } - } - - @override - void setTrackCameraPosition(bool trackCameraPosition) { - _trackCameraPosition = trackCameraPosition; - } - - @override - void setZoomGesturesEnabled(bool zoomGesturesEnabled) { - if (zoomGesturesEnabled) { - _map.doubleClickZoom.enable(); - _map.boxZoom.enable(); - _map.scrollZoom.enable(); - _map.touchZoomRotate.enable(); - _map.keyboard.enable(); - } else { - _map.doubleClickZoom.disable(); - _map.boxZoom.disable(); - _map.scrollZoom.disable(); - _map.touchZoomRotate.disable(); - _map.keyboard.disable(); - } - } - - @override - Future toScreenLocation(LatLng latLng) async { - var screenPosition = - _map.project(LngLat(latLng.longitude, latLng.latitude)); - return Point(screenPosition.x.round(), screenPosition.y.round()); - } - - @override - Future> toScreenLocationBatch(Iterable latLngs) async { - return latLngs.map((latLng) { - var screenPosition = - _map.project(LngLat(latLng.longitude, latLng.latitude)); - return Point(screenPosition.x.round(), screenPosition.y.round()); - }).toList(growable: false); - } - - @override - Future toLatLng(Point screenLocation) async { - var lngLat = - _map.unproject(mapbox.Point(screenLocation.x, screenLocation.y)); - return LatLng(lngLat.lat as double, lngLat.lng as double); - } - - @override - Future getMetersPerPixelAtLatitude(double latitude) async { - //https://wiki.openstreetmap.org/wiki/Zoom_levels - var circumference = 40075017.686; - var zoom = _map.getZoom(); - return circumference * cos(latitude * (pi / 180)) / pow(2, zoom + 9); - } -} diff --git a/mapbox_gl_web/lib/src/mapbox_map_plugin.dart b/mapbox_gl_web/lib/src/mapbox_map_plugin.dart index dcc779d70..68f7975f5 100644 --- a/mapbox_gl_web/lib/src/mapbox_map_plugin.dart +++ b/mapbox_gl_web/lib/src/mapbox_map_plugin.dart @@ -3,6 +3,6 @@ part of mapbox_gl_web; class MapboxMapPlugin { /// Registers this class as the default instance of [MapboxGlPlatform]. static void registerWith(Registrar registrar) { - MapboxGlPlatform.createInstance = () => MapboxMapController(); + MapboxGlPlatform.createInstance = () => MapboxWebGlPlatform(); } } diff --git a/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart b/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart new file mode 100644 index 000000000..c3872cf50 --- /dev/null +++ b/mapbox_gl_web/lib/src/mapbox_web_gl_platform.dart @@ -0,0 +1,1072 @@ +part of mapbox_gl_web; + +const _mapboxGlCssUrl = + 'https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css'; + +class MapboxWebGlPlatform extends MapboxGlPlatform + implements MapboxMapOptionsSink { + late DivElement _mapElement; + + late Map _creationParams; + late MapboxMap _map; + bool _mapReady = false; + dynamic _draggedFeatureId; + LatLng? _dragOrigin; + LatLng? _dragPrevious; + bool _dragEnabled = true; + final _addedFeaturesByLayer = {}; + + final _interactiveFeatureLayerIds = Set(); + + bool _trackCameraPosition = false; + GeolocateControl? _geolocateControl; + LatLng? _myLastLocation; + + String? _navigationControlPosition; + NavigationControl? _navigationControl; + Timer? lastResizeObserverTimer; + + @override + Widget buildView( + Map creationParams, + OnPlatformViewCreatedCallback onPlatformViewCreated, + Set>? gestureRecognizers) { + _creationParams = creationParams; + _registerViewFactory(onPlatformViewCreated, this.hashCode); + return HtmlElementView( + viewType: 'plugins.flutter.io/mapbox_gl_${this.hashCode}'); + } + + @override + void dispose() { + super.dispose(); + _map.remove(); + } + + void _registerViewFactory(Function(int) callback, int identifier) { + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + 'plugins.flutter.io/mapbox_gl_$identifier', (int viewId) { + _mapElement = DivElement() + ..style.position = 'absolute' + ..style.top = '0' + ..style.bottom = '0' + ..style.width = '100%'; + callback(viewId); + return _mapElement; + }); + } + + @override + Future initPlatform(int id) async { + await _addStylesheetToShadowRoot(_mapElement); + if (_creationParams.containsKey('initialCameraPosition')) { + var camera = _creationParams['initialCameraPosition']; + _dragEnabled = _creationParams['dragEnabled'] ?? true; + + if (_creationParams.containsKey('accessToken')) { + Mapbox.accessToken = _creationParams['accessToken']; + } + _map = MapboxMap( + MapOptions( + container: _mapElement, + style: 'mapbox://styles/mapbox/streets-v11', + center: LngLat(camera['target'][1], camera['target'][0]), + zoom: camera['zoom'], + bearing: camera['bearing'], + pitch: camera['tilt'], + preserveDrawingBuffer: true, + ), + ); + _map.on('load', _onStyleLoaded); + _map.on('click', _onMapClick); + // long click not available in web, so it is mapped to double click + _map.on('dblclick', _onMapLongClick); + _map.on('movestart', _onCameraMoveStarted); + _map.on('move', _onCameraMove); + _map.on('moveend', _onCameraIdle); + _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); + await addImage(imagePath, bytes.buffer.asUint8List()); + } + + _onMouseDown(Event e) { + var isDraggable = e.features[0].properties['draggable']; + if (isDraggable != null && isDraggable) { + // Prevent the default map drag behavior. + e.preventDefault(); + _draggedFeatureId = e.features[0].id; + _map.getCanvas().style.cursor = 'grabbing'; + var coords = e.lngLat; + _dragOrigin = LatLng(coords.lat as double, coords.lng as double); + + if (_draggedFeatureId != null) { + final current = + LatLng(e.lngLat.lat.toDouble(), e.lngLat.lng.toDouble()); + final payload = { + 'id': _draggedFeatureId, + 'point': Point(e.point.x.toDouble(), e.point.y.toDouble()), + 'origin': _dragOrigin, + 'current': current, + 'delta': LatLng(0, 0), + 'eventType': 'start' + }; + onFeatureDraggedPlatform(payload); + } + } + } + + _onMouseUp(Event e) { + if (_draggedFeatureId != null) { + final current = LatLng(e.lngLat.lat.toDouble(), e.lngLat.lng.toDouble()); + final payload = { + 'id': _draggedFeatureId, + 'point': Point(e.point.x.toDouble(), e.point.y.toDouble()), + 'origin': _dragOrigin, + 'current': current, + 'delta': current - (_dragPrevious ?? _dragOrigin!), + 'eventType': 'end' + }; + onFeatureDraggedPlatform(payload); + } + _draggedFeatureId = null; + _dragPrevious = null; + _dragOrigin = null; + _map.getCanvas().style.cursor = ''; + } + + _onMouseMove(Event e) { + if (_draggedFeatureId != null) { + final current = LatLng(e.lngLat.lat.toDouble(), e.lngLat.lng.toDouble()); + final payload = { + 'id': _draggedFeatureId, + 'point': Point(e.point.x.toDouble(), e.point.y.toDouble()), + 'origin': _dragOrigin, + 'current': current, + 'delta': current - (_dragPrevious ?? _dragOrigin!), + 'eventType': 'drag' + }; + _dragPrevious = current; + onFeatureDraggedPlatform(payload); + } + } + + Future _addStylesheetToShadowRoot(HtmlElement e) async { + LinkElement link = LinkElement() + ..href = _mapboxGlCssUrl + ..rel = 'stylesheet'; + e.append(link); + + await link.onLoad.first; + } + + @override + Future updateMapOptions( + Map optionsUpdate) async { + // FIX: why is called indefinitely? (map_ui page) + Convert.interpretMapboxMapOptions(optionsUpdate, this); + return _getCameraPosition(); + } + + @override + Future animateCamera(CameraUpdate cameraUpdate, + {Duration? duration}) async { + final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); + + 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; + } + + @override + Future moveCamera(CameraUpdate cameraUpdate) async { + final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); + _map.jumpTo(cameraOptions); + return true; + } + + @override + Future updateMyLocationTrackingMode( + MyLocationTrackingMode myLocationTrackingMode) async { + setMyLocationTrackingMode(myLocationTrackingMode.index); + } + + @override + Future matchMapLanguageWithDeviceDefault() async { + setMapLanguage(ui.window.locale.languageCode); + } + + @override + Future setMapLanguage(String language) async { + _map.setLayoutProperty( + 'country-label', + 'text-field', + ['get', 'name_' + language], + ); + } + + @override + Future setTelemetryEnabled(bool enabled) async { + print('Telemetry not available in web'); + return; + } + + @override + Future getTelemetryEnabled() async { + print('Telemetry not available in web'); + return false; + } + + @override + Future queryRenderedFeatures( + Point point, List layerIds, List? filter) async { + Map options = {}; + if (layerIds.length > 0) { + options['layers'] = layerIds; + } + if (filter != null) { + options['filter'] = filter; + } + + // avoid issues with the js point type + final pointAsList = [point.x, point.y]; + return _map + .queryRenderedFeatures([pointAsList, pointAsList], options) + .map((feature) => { + 'type': 'Feature', + 'id': feature.id, + 'geometry': { + 'type': feature.geometry.type, + 'coordinates': feature.geometry.coordinates, + }, + 'properties': feature.properties, + 'source': feature.source, + }) + .toList(); + } + + @override + Future queryRenderedFeaturesInRect( + Rect rect, List layerIds, String? filter) async { + Map options = {}; + if (layerIds.length > 0) { + options['layers'] = layerIds; + } + if (filter != null) { + options['filter'] = filter; + } + return _map + .queryRenderedFeatures([ + [rect.left, rect.bottom], + [rect.right, rect.top], + ], options) + .map((feature) => { + 'type': 'Feature', + 'id': feature.id, + 'geometry': { + 'type': feature.geometry.type, + 'coordinates': feature.geometry.coordinates, + }, + 'properties': feature.properties, + 'source': feature.source, + }) + .toList(); + } + + @override + Future invalidateAmbientCache() async { + print('Offline storage not available in web'); + } + + @override + Future requestMyLocationLatLng() async { + return _myLastLocation; + } + + @override + Future getVisibleRegion() async { + final bounds = _map.getBounds(); + return LatLngBounds( + southwest: LatLng( + bounds.getSouthWest().lat as double, + bounds.getSouthWest().lng as double, + ), + northeast: LatLng( + bounds.getNorthEast().lat as double, + bounds.getNorthEast().lng as double, + ), + ); + } + + @override + Future addImage(String name, Uint8List bytes, + [bool sdf = false]) async { + final photo = decodeImage(bytes)!; + if (!_map.hasImage(name)) { + _map.addImage( + name, + { + 'width': photo.width, + 'height': photo.height, + 'data': photo.getBytes(), + }, + {'sdf': sdf}, + ); + } + } + + @override + Future removeSource(String sourceId) async { + _map.removeSource(sourceId); + } + + CameraPosition? _getCameraPosition() { + if (_trackCameraPosition) { + final center = _map.getCenter(); + return CameraPosition( + bearing: _map.getBearing() as double, + target: LatLng(center.lat as double, center.lng as double), + tilt: _map.getPitch() as double, + zoom: _map.getZoom() as double, + ); + } + return null; + } + + void _onStyleLoaded(_) { + _mapReady = true; + _onMapResize(); + onMapStyleLoadedPlatform(null); + } + + void _onMapResize() { + Timer(Duration(), () { + var container = _map.getContainer(); + var canvas = _map.getCanvas(); + var widthMismatch = canvas.clientWidth != container.clientWidth; + var heightMismatch = canvas.clientHeight != container.clientHeight; + if (widthMismatch || heightMismatch) { + _map.resize(); + } + }); + } + + void _onMapClick(Event e) { + final features = _map.queryRenderedFeatures([e.point.x, e.point.y], + {"layers": _interactiveFeatureLayerIds.toList()}); + final payload = { + 'point': Point(e.point.x.toDouble(), e.point.y.toDouble()), + 'latLng': LatLng(e.lngLat.lat.toDouble(), e.lngLat.lng.toDouble()), + if (features.isNotEmpty) "id": features.first.id, + }; + if (features.isNotEmpty) { + onFeatureTappedPlatform(payload); + } else { + onMapClickPlatform(payload); + } + } + + void _onMapLongClick(e) { + onMapLongClickPlatform({ + 'point': Point(e.point.x, e.point.y), + 'latLng': LatLng(e.lngLat.lat, e.lngLat.lng), + }); + } + + void _onCameraMoveStarted(_) { + onCameraMoveStartedPlatform(null); + } + + void _onCameraMove(_) { + final center = _map.getCenter(); + var camera = CameraPosition( + bearing: _map.getBearing() as double, + target: LatLng(center.lat as double, center.lng as double), + tilt: _map.getPitch() as double, + zoom: _map.getZoom() as double, + ); + onCameraMovePlatform(camera); + } + + void _onCameraIdle(_) { + final center = _map.getCenter(); + var camera = CameraPosition( + bearing: _map.getBearing() as double, + target: LatLng(center.lat as double, center.lng as double), + tilt: _map.getPitch() as double, + zoom: _map.getZoom() as double, + ); + onCameraIdlePlatform(camera); + } + + void _onCameraTrackingChanged(bool isTracking) { + if (isTracking) { + onCameraTrackingChangedPlatform(MyLocationTrackingMode.Tracking); + } else { + onCameraTrackingChangedPlatform(MyLocationTrackingMode.None); + } + } + + void _onCameraTrackingDismissed() { + onCameraTrackingDismissedPlatform(null); + } + + void _addGeolocateControl({bool trackUserLocation = false}) { + _removeGeolocateControl(); + _geolocateControl = GeolocateControl( + GeolocateControlOptions( + positionOptions: PositionOptions(enableHighAccuracy: true), + trackUserLocation: trackUserLocation, + showAccuracyCircle: true, + showUserLocation: true, + ), + ); + _geolocateControl!.on('geolocate', (e) { + _myLastLocation = LatLng(e.coords.latitude, e.coords.longitude); + onUserLocationUpdatedPlatform(UserLocation( + position: LatLng(e.coords.latitude, e.coords.longitude), + altitude: e.coords.altitude, + bearing: e.coords.heading, + speed: e.coords.speed, + horizontalAccuracy: e.coords.accuracy, + verticalAccuracy: e.coords.altitudeAccuracy, + heading: null, + timestamp: DateTime.fromMillisecondsSinceEpoch(e.timestamp))); + }); + _geolocateControl!.on('trackuserlocationstart', (_) { + _onCameraTrackingChanged(true); + }); + _geolocateControl!.on('trackuserlocationend', (_) { + _onCameraTrackingChanged(false); + _onCameraTrackingDismissed(); + }); + _map.addControl(_geolocateControl, 'bottom-right'); + } + + void _removeGeolocateControl() { + if (_geolocateControl != null) { + _map.removeControl(_geolocateControl); + _geolocateControl = null; + } + } + + void _updateNavigationControl({ + bool? compassEnabled, + CompassViewPosition? position, + }) { + bool? prevShowCompass; + if (_navigationControl != null) { + prevShowCompass = _navigationControl!.options.showCompass; + } + String? prevPosition = _navigationControlPosition; + + String? positionString; + switch (position) { + case CompassViewPosition.TopRight: + positionString = 'top-right'; + break; + case CompassViewPosition.TopLeft: + positionString = 'top-left'; + break; + case CompassViewPosition.BottomRight: + positionString = 'bottom-right'; + break; + case CompassViewPosition.BottomLeft: + positionString = 'bottom-left'; + break; + default: + positionString = null; + } + + bool newShowComapss = compassEnabled ?? prevShowCompass ?? false; + String? newPosition = positionString ?? prevPosition ?? null; + + _removeNavigationControl(); + _navigationControl = NavigationControl(NavigationControlOptions( + showCompass: newShowComapss, + showZoom: false, + visualizePitch: false, + )); + + if (newPosition == null) { + _map.addControl(_navigationControl); + } else { + _map.addControl(_navigationControl, newPosition); + _navigationControlPosition = newPosition; + } + } + + void _removeNavigationControl() { + if (_navigationControl != null) { + _map.removeControl(_navigationControl); + _navigationControl = null; + } + } + + /* + * MapboxMapOptionsSink + */ + @override + void setAttributionButtonMargins(int x, int y) { + print('setAttributionButtonMargins not available in web'); + } + + @override + void setCameraTargetBounds(LatLngBounds? bounds) { + if (bounds == null) { + _map.setMaxBounds(null); + } else { + _map.setMaxBounds( + LngLatBounds( + LngLat( + bounds.southwest.longitude, + bounds.southwest.latitude, + ), + LngLat( + bounds.northeast.longitude, + bounds.northeast.latitude, + ), + ), + ); + } + } + + @override + void setCompassEnabled(bool compassEnabled) { + _updateNavigationControl(compassEnabled: compassEnabled); + } + + @override + void setCompassAlignment(CompassViewPosition position) { + _updateNavigationControl(position: position); + } + + @override + void setAttributionButtonAlignment(AttributionButtonPosition position) { + print('setAttributionButtonAlignment not available in web'); + } + + @override + void setCompassViewMargins(int x, int y) { + print('setCompassViewMargins not available in web'); + } + + @override + void setLogoViewMargins(int x, int y) { + print('setLogoViewMargins not available in web'); + } + + @override + void setMinMaxZoomPreference(num? min, num? max) { + // FIX: why is called indefinitely? (map_ui page) + _map.setMinZoom(min); + _map.setMaxZoom(max); + } + + @override + void setMyLocationEnabled(bool myLocationEnabled) { + if (myLocationEnabled) { + _addGeolocateControl(trackUserLocation: false); + } else { + _removeGeolocateControl(); + } + } + + @override + void setMyLocationRenderMode(int myLocationRenderMode) { + print('myLocationRenderMode not available in web'); + } + + @override + void setMyLocationTrackingMode(int myLocationTrackingMode) { + if (_geolocateControl == null) { + //myLocationEnabled is false, ignore myLocationTrackingMode + return; + } + if (myLocationTrackingMode == 0) { + _addGeolocateControl(trackUserLocation: false); + } else { + print('Only one tracking mode available in web'); + _addGeolocateControl(trackUserLocation: true); + } + } + + @override + void setStyleString(String? styleString) { + //remove old mouseenter callbacks to avoid multicalling + for (var layerId in _interactiveFeatureLayerIds) { + _map.off('mouseenter', layerId, _onMouseEnterFeature); + _map.off('mousemouve', layerId, _onMouseEnterFeature); + _map.off('mouseleave', layerId, _onMouseLeaveFeature); + if (_dragEnabled) _map.off('mousedown', layerId, _onMouseDown); + } + _interactiveFeatureLayerIds.clear(); + + 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); + } + } + + @override + void setTrackCameraPosition(bool trackCameraPosition) { + _trackCameraPosition = trackCameraPosition; + } + + @override + Future toScreenLocation(LatLng latLng) async { + var screenPosition = + _map.project(LngLat(latLng.longitude, latLng.latitude)); + return Point(screenPosition.x.round(), screenPosition.y.round()); + } + + @override + Future> toScreenLocationBatch(Iterable latLngs) async { + return latLngs.map((latLng) { + var screenPosition = + _map.project(LngLat(latLng.longitude, latLng.latitude)); + return Point(screenPosition.x.round(), screenPosition.y.round()); + }).toList(growable: false); + } + + @override + Future toLatLng(Point screenLocation) async { + var lngLat = + _map.unproject(mapbox.Point(screenLocation.x, screenLocation.y)); + return LatLng(lngLat.lat as double, lngLat.lng as double); + } + + @override + Future getMetersPerPixelAtLatitude(double latitude) async { + //https://wiki.openstreetmap.org/wiki/Zoom_levels + var circumference = 40075017.686; + var zoom = _map.getZoom(); + return circumference * cos(latitude * (pi / 180)) / pow(2, zoom + 9); + } + + @override + Future removeLayer(String layerId) async { + if (_map.getLayer(layerId) != null) { + _interactiveFeatureLayerIds.remove(layerId); + _map.removeLayer(layerId); + } + } + + @override + Future setFilter(String layerId, dynamic filter) async { + _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 { + final data = _makeFeatureCollection(geojson); + _addedFeaturesByLayer[sourceId] = data; + _map.addSource(sourceId, { + "type": 'geojson', + "data": geojson, // pass the raw string here to avoid errors + if (promoteId != null) "promoteId": promoteId + }); + } + + Feature _makeFeature(Map geojsonFeature) { + return Feature( + geometry: Geometry( + type: geojsonFeature["geometry"]["type"], + coordinates: geojsonFeature["geometry"]["coordinates"]), + properties: geojsonFeature["properties"], + id: geojsonFeature["properties"]?["id"] ?? geojsonFeature["id"]); + } + + FeatureCollection _makeFeatureCollection(Map geojson) { + return FeatureCollection( + features: [for (final f in geojson["features"] ?? []) _makeFeature(f)]); + } + + @override + Future setGeoJsonSource( + String sourceId, Map geojson) async { + final source = _map.getSource(sourceId) as GeoJsonSource; + final data = _makeFeatureCollection(geojson); + _addedFeaturesByLayer[sourceId] = data; + source.setData(data); + } + + @override + Future addCircleLayer( + 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, "circle", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction); + } + + @override + Future addFillLayer( + 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", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + 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, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + return _addLayer(sourceId, layerId, properties, "line", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction); + } + + @override + Future addSymbolLayer( + 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, "symbol", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction); + } + + @override + Future addHillshadeLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + return _addLayer(sourceId, layerId, properties, "hillshade", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + 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, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom}) async { + await _addLayer(sourceId, layerId, properties, "raster", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + enableInteraction: false); + } + + Future _addLayer(String sourceId, String layerId, + Map properties, String layerType, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + final layout = Map.fromEntries( + properties.entries.where((entry) => isLayoutProperty(entry.key))); + final paint = Map.fromEntries( + properties.entries.where((entry) => !isLayoutProperty(entry.key))); + + removeLayer(layerId); + + _map.addLayer({ + 'id': layerId, + 'type': layerType, + 'source': sourceId, + 'layout': layout, + 'paint': paint, + if (sourceLayer != null) 'source-layer': sourceLayer, + if (minzoom != null) 'minzoom': minzoom, + if (maxzoom != null) 'maxzoom': maxzoom, + if (filter != null) 'filter': filter, + }, belowLayerId); + + if (enableInteraction) { + _interactiveFeatureLayerIds.add(layerId); + if (layerType == "fill") { + _map.on('mousemove', layerId, _onMouseEnterFeature); + } else { + _map.on('mouseenter', layerId, _onMouseEnterFeature); + } + _map.on('mouseleave', layerId, _onMouseLeaveFeature); + if (_dragEnabled) _map.on('mousedown', layerId, _onMouseDown); + } + } + + void _onMouseEnterFeature(_) { + if (_draggedFeatureId == null) { + _map.getCanvas().style.cursor = 'pointer'; + } + } + + void _onMouseLeaveFeature(_) { + _map.getCanvas().style.cursor = ''; + } + + @override + void setGestures( + {required bool rotateGesturesEnabled, + required bool scrollGesturesEnabled, + required bool tiltGesturesEnabled, + required bool zoomGesturesEnabled, + required bool doubleClickZoomEnabled}) { + if (rotateGesturesEnabled && + scrollGesturesEnabled && + tiltGesturesEnabled && + zoomGesturesEnabled) { + _map.keyboard.enable(); + } else { + _map.keyboard.disable(); + } + + if (scrollGesturesEnabled) { + _map.dragPan.enable(); + } else { + _map.dragPan.disable(); + } + + if (zoomGesturesEnabled) { + _map.doubleClickZoom.enable(); + _map.boxZoom.enable(); + _map.scrollZoom.enable(); + _map.touchZoomRotate.enable(); + } else { + _map.doubleClickZoom.disable(); + _map.boxZoom.disable(); + _map.scrollZoom.disable(); + _map.touchZoomRotate.disable(); + } + + if (doubleClickZoomEnabled) { + _map.doubleClickZoom.enable(); + } else { + _map.doubleClickZoom.disable(); + } + + if (rotateGesturesEnabled) { + _map.touchZoomRotate.enableRotation(); + } else { + _map.touchZoomRotate.disableRotation(); + } + + // dragRotate is shared by both gestures + if (tiltGesturesEnabled && rotateGesturesEnabled) { + _map.dragRotate.enable(); + } else { + _map.dragRotate.disable(); + } + } + + @override + Future addSource(String sourceId, SourceProperties source) async { + _map.addSource(sourceId, source.toJson()); + } + + Future addImageSource( + String imageSourceId, Uint8List bytes, LatLngQuad coordinates) { + // TODO: implement addImageSource + 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) { + // TODO: implement addLayer + throw UnimplementedError(); + } + + @override + Future addLayerBelow(String imageLayerId, String imageSourceId, + String belowLayerId, double? minzoom, double? maxzoom) { + // TODO: implement addLayerBelow + throw UnimplementedError(); + } + + @override + Future updateContentInsets(EdgeInsets insets, bool animated) { + // TODO: implement updateContentInsets + throw UnimplementedError(); + } + + @override + Future setFeatureForGeoJsonSource( + String sourceId, Map geojsonFeature) async { + final source = _map.getSource(sourceId) as GeoJsonSource?; + final data = _addedFeaturesByLayer[sourceId]; + + if (source != null && data != null) { + final feature = _makeFeature(geojsonFeature); + final features = data.features.toList(); + final index = features.indexWhere((f) => f.id == feature.id); + if (index >= 0) { + features[index] = feature; + final newData = FeatureCollection(features: features); + _addedFeaturesByLayer[sourceId] = newData; + + source.setData(newData); + } + } + } + + @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/lib/src/options_sink.dart b/mapbox_gl_web/lib/src/options_sink.dart index 9c326975d..e12f85a1e 100644 --- a/mapbox_gl_web/lib/src/options_sink.dart +++ b/mapbox_gl_web/lib/src/options_sink.dart @@ -11,16 +11,16 @@ abstract class MapboxMapOptionsSink { void setMinMaxZoomPreference(num? min, num? max); - void setRotateGesturesEnabled(bool rotateGesturesEnabled); - - void setScrollGesturesEnabled(bool scrollGesturesEnabled); - - void setTiltGesturesEnabled(bool tiltGesturesEnabled); + void setGestures({ + required bool rotateGesturesEnabled, + required bool scrollGesturesEnabled, + required bool tiltGesturesEnabled, + required bool zoomGesturesEnabled, + required bool doubleClickZoomEnabled, + }); void setTrackCameraPosition(bool trackCameraPosition); - void setZoomGesturesEnabled(bool zoomGesturesEnabled); - void setMyLocationEnabled(bool myLocationEnabled); void setMyLocationTrackingMode(int myLocationTrackingMode); @@ -29,9 +29,11 @@ abstract class MapboxMapOptionsSink { void setLogoViewMargins(int x, int y); - void setCompassGravity(int gravity); + void setCompassAlignment(CompassViewPosition position); void setCompassViewMargins(int x, int y); + void setAttributionButtonAlignment(AttributionButtonPosition position); + void setAttributionButtonMargins(int x, int y); } diff --git a/mapbox_gl_web/pubspec.yaml b/mapbox_gl_web/pubspec.yaml index 0fd2c4b68..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.12.0 +version: 0.16.0 homepage: https://github.com/tobrun/flutter-mapbox-gl flutter: @@ -20,8 +20,8 @@ dependencies: git: url: https://github.com/tobrun/flutter-mapbox-gl.git path: mapbox_gl_platform_interface - mapbox_gl_dart: ^0.2.0-nullsafety - image: ^3.0.0 + mapbox_gl_dart: ^0.2.1 + image: '>=3.0.0 <5.0.0' dependency_overrides: mapbox_gl_platform_interface: diff --git a/pubspec.lock b/pubspec.lock index 6307528c2..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.1.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.12.0" + version: "0.16.0" mapbox_gl_web: dependency: "direct main" description: path: mapbox_gl_web relative: true source: path - version: "0.12.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.3.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.0" + 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.12.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 f692d044a..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.12.0 +version: 0.16.0 homepage: https://github.com/tobrun/flutter-mapbox-gl dependencies: @@ -14,6 +14,7 @@ dependencies: git: url: https://github.com/tobrun/flutter-mapbox-gl.git path: mapbox_gl_web + collection: ^1.15.0 dependency_overrides: mapbox_gl_platform_interface: @@ -33,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/input/style.json b/scripts/input/style.json new file mode 100644 index 000000000..f04cf74c7 --- /dev/null +++ b/scripts/input/style.json @@ -0,0 +1,5806 @@ +{ + "$version":8, + "$root":{ + "version":{ + "required":true, + "type":"enum", + "values":[ + 8 + ], + "doc":"Style specification version number. Must be 8.", + "example":8 + }, + "name":{ + "type":"string", + "doc":"A human-readable name for the style.", + "example":"Bright" + }, + "metadata":{ + "type":"*", + "doc":"Arbitrary properties useful to track with the stylesheet, but do not influence rendering. Properties should be prefixed to avoid collisions, like 'mapbox:'." + }, + "center":{ + "type":"array", + "value":"number", + "doc":"Default map center in longitude and latitude. The style center will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example":[ + -73.9749, + 40.7736 + ] + }, + "zoom":{ + "type":"number", + "doc":"Default zoom level. The style zoom will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example":12.5 + }, + "bearing":{ + "type":"number", + "default":0, + "period":360, + "units":"degrees", + "doc":"Default bearing, in degrees. The bearing is the compass direction that is \"up\"; for example, a bearing of 90° orients the map so that east is up. This value will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example":29 + }, + "pitch":{ + "type":"number", + "default":0, + "units":"degrees", + "doc":"Default pitch, in degrees. Zero is perpendicular to the surface, for a look straight down at the map, while a greater value like 60 looks ahead towards the horizon. The style pitch will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example":50 + }, + "light":{ + "type":"light", + "doc":"The global light source.", + "example":{ + "anchor":"viewport", + "color":"white", + "intensity":0.4 + } + }, + "sources":{ + "required":true, + "type":"sources", + "doc":"Data source specifications.", + "example":{ + "mapbox-streets":{ + "type":"vector", + "url":"mapbox://mapbox.mapbox-streets-v6" + } + } + }, + "sprite":{ + "type":"string", + "doc":"A base URL for retrieving the sprite image and metadata. The extensions `.png`, `.json` and scale factor `@2x.png` will be automatically appended. This property is required if any layer uses the `background-pattern`, `fill-pattern`, `line-pattern`, `fill-extrusion-pattern`, or `icon-image` properties. The URL must be absolute, containing the [scheme, authority and path components](https://en.wikipedia.org/wiki/URL#Syntax).", + "example":"mapbox://sprites/mapbox/bright-v8" + }, + "glyphs":{ + "type":"string", + "doc":"A URL template for loading signed-distance-field glyph sets in PBF format. The URL must include `{fontstack}` and `{range}` tokens. This property is required if any layer uses the `text-field` layout property. The URL must be absolute, containing the [scheme, authority and path components](https://en.wikipedia.org/wiki/URL#Syntax).", + "example":"mapbox://fonts/mapbox/{fontstack}/{range}.pbf" + }, + "transition":{ + "type":"transition", + "doc":"A global transition definition to use as a default across properties, to be used for timing transitions between one value and the next when no property-specific transition is set. Collision-based symbol fading is controlled independently of the style's `transition` property.", + "example":{ + "duration":300, + "delay":0 + } + }, + "layers":{ + "required":true, + "type":"array", + "value":"layer", + "doc":"Layers will be drawn in the order of this array.", + "example":[ + { + "id":"water", + "source":"mapbox-streets", + "source-layer":"water", + "type":"fill", + "paint":{ + "fill-color":"#00ffff" + } + } + ] + } + }, + "sources":{ + "*":{ + "type":"source", + "doc":"Specification of a data source. For vector and raster sources, either TileJSON or a URL to a TileJSON must be provided. For image and video sources, a URL must be provided. For GeoJSON sources, a URL or inline GeoJSON must be provided." + } + }, + "source":[ + "source_vector", + "source_raster", + "source_raster_dem", + "source_geojson", + "source_video", + "source_image" + ], + "source_vector":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "vector":{ + "doc":"A vector tile source." + } + }, + "doc":"The type of the source." + }, + "url":{ + "type":"string", + "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + }, + "tiles":{ + "type":"array", + "value":"string", + "doc":"An array of one or more tile source URLs, as in the TileJSON spec." + }, + "bounds":{ + "type":"array", + "value":"number", + "length":4, + "default":[ + -180, + -85.051129, + 180, + 85.051129 + ], + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + }, + "scheme":{ + "type":"enum", + "values":{ + "xyz":{ + "doc":"Slippy map tilenames scheme." + }, + "tms":{ + "doc":"OSGeo spec scheme." + } + }, + "default":"xyz", + "doc":"Influences the y direction of the tile coordinates. The global-mercator (aka Spherical Mercator) profile is assumed." + }, + "minzoom":{ + "type":"number", + "default":0, + "doc":"Minimum zoom level for which tiles are available, as in the TileJSON spec." + }, + "maxzoom":{ + "type":"number", + "default":22, + "doc":"Maximum zoom level for which tiles are available, as in the TileJSON spec. Data from tiles at the maxzoom are used when displaying the map at higher zoom levels." + }, + "attribution":{ + "type":"string", + "doc":"Contains an attribution to be displayed when the map is shown to a user." + }, + "promoteId":{ + "type":"promoteId", + "doc":"A property to use as a feature id (for feature state). Either a property name, or an object of the form `{: }`. If specified as a string for a vector tile source, the same property is used across all its source layers." + }, + "*":{ + "type":"*", + "doc":"Other keys to configure the data source." + } + }, + "source_raster":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "raster":{ + "doc":"A raster tile source." + } + }, + "doc":"The type of the source." + }, + "url":{ + "type":"string", + "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + }, + "tiles":{ + "type":"array", + "value":"string", + "doc":"An array of one or more tile source URLs, as in the TileJSON spec." + }, + "bounds":{ + "type":"array", + "value":"number", + "length":4, + "default":[ + -180, + -85.051129, + 180, + 85.051129 + ], + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + }, + "minzoom":{ + "type":"number", + "default":0, + "doc":"Minimum zoom level for which tiles are available, as in the TileJSON spec." + }, + "maxzoom":{ + "type":"number", + "default":22, + "doc":"Maximum zoom level for which tiles are available, as in the TileJSON spec. Data from tiles at the maxzoom are used when displaying the map at higher zoom levels." + }, + "tileSize":{ + "type":"number", + "default":512, + "units":"pixels", + "doc":"The minimum visual size to display tiles for this layer. Only configurable for raster layers." + }, + "scheme":{ + "type":"enum", + "values":{ + "xyz":{ + "doc":"Slippy map tilenames scheme." + }, + "tms":{ + "doc":"OSGeo spec scheme." + } + }, + "default":"xyz", + "doc":"Influences the y direction of the tile coordinates. The global-mercator (aka Spherical Mercator) profile is assumed." + }, + "attribution":{ + "type":"string", + "doc":"Contains an attribution to be displayed when the map is shown to a user." + }, + "*":{ + "type":"*", + "doc":"Other keys to configure the data source." + } + }, + "source_raster_dem":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "raster-dem":{ + "doc":"A RGB-encoded raster DEM source" + } + }, + "doc":"The type of the source." + }, + "url":{ + "type":"string", + "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + }, + "tiles":{ + "type":"array", + "value":"string", + "doc":"An array of one or more tile source URLs, as in the TileJSON spec." + }, + "bounds":{ + "type":"array", + "value":"number", + "length":4, + "default":[ + -180, + -85.051129, + 180, + 85.051129 + ], + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + }, + "minzoom":{ + "type":"number", + "default":0, + "doc":"Minimum zoom level for which tiles are available, as in the TileJSON spec." + }, + "maxzoom":{ + "type":"number", + "default":22, + "doc":"Maximum zoom level for which tiles are available, as in the TileJSON spec. Data from tiles at the maxzoom are used when displaying the map at higher zoom levels." + }, + "tileSize":{ + "type":"number", + "default":512, + "units":"pixels", + "doc":"The minimum visual size to display tiles for this layer. Only configurable for raster layers." + }, + "attribution":{ + "type":"string", + "doc":"Contains an attribution to be displayed when the map is shown to a user." + }, + "encoding":{ + "type":"enum", + "values":{ + "terrarium":{ + "doc":"Terrarium format PNG tiles. See https://aws.amazon.com/es/public-datasets/terrain/ for more info." + }, + "mapbox":{ + "doc":"Mapbox Terrain RGB tiles. See https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb for more info." + } + }, + "default":"mapbox", + "doc":"The encoding used by this source. Mapbox Terrain RGB is used by default" + }, + "*":{ + "type":"*", + "doc":"Other keys to configure the data source." + } + }, + "source_geojson":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "geojson":{ + "doc":"A GeoJSON data source." + } + }, + "doc":"The data type of the GeoJSON source." + }, + "data":{ + "type":"*", + "doc":"A URL to a GeoJSON file, or inline GeoJSON." + }, + "maxzoom":{ + "type":"number", + "default":18, + "doc":"Maximum zoom level at which to create vector tiles (higher means greater detail at high zoom levels)." + }, + "attribution":{ + "type":"string", + "doc":"Contains an attribution to be displayed when the map is shown to a user." + }, + "buffer":{ + "type":"number", + "default":128, + "maximum":512, + "minimum":0, + "doc":"Size of the tile buffer on each side. A value of 0 produces no buffer. A value of 512 produces a buffer as wide as the tile itself. Larger values produce fewer rendering artifacts near tile edges and slower performance." + }, + "tolerance":{ + "type":"number", + "default":0.375, + "doc":"Douglas-Peucker simplification tolerance (higher means simpler geometries and faster performance)." + }, + "cluster":{ + "type":"boolean", + "default":false, + "doc":"If the data is a collection of point features, setting this to true clusters the points by radius into groups. Cluster groups become new `Point` features in the source with additional properties:\n * `cluster` Is `true` if the point is a cluster \n * `cluster_id` A unqiue id for the cluster to be used in conjunction with the [cluster inspection methods](https://www.mapbox.com/mapbox-gl-js/api/#geojsonsource#getclusterexpansionzoom)\n * `point_count` Number of original points grouped into this cluster\n * `point_count_abbreviated` An abbreviated point count" + }, + "clusterRadius":{ + "type":"number", + "default":50, + "minimum":0, + "doc":"Radius of each cluster if clustering is enabled. A value of 512 indicates a radius equal to the width of a tile." + }, + "clusterMaxZoom":{ + "type":"number", + "doc":"Max zoom on which to cluster points if clustering is enabled. Defaults to one zoom less than maxzoom (so that last zoom features are not clustered)." + }, + "clusterProperties":{ + "type":"*", + "doc":"An object defining custom properties on the generated clusters if clustering is enabled, aggregating values from clustered points. Has the form `{\"property_name\": [operator, map_expression]}`. `operator` is any expression function that accepts at least 2 operands (e.g. `\"+\"` or `\"max\"`) — it accumulates the property value from clusters/points the cluster contains; `map_expression` produces the value of a single point.\n\nExample: `{\"sum\": [\"+\", [\"get\", \"scalerank\"]]}`.\n\nFor more advanced use cases, in place of `operator`, you can use a custom reduce expression that references a special `[\"accumulated\"]` value, e.g.:\n`{\"sum\": [[\"+\", [\"accumulated\"], [\"get\", \"sum\"]], [\"get\", \"scalerank\"]]}`" + }, + "lineMetrics":{ + "type":"boolean", + "default":false, + "doc":"Whether to calculate line distance metrics. This is required for line layers that specify `line-gradient` values." + }, + "generateId":{ + "type":"boolean", + "default":false, + "doc":"Whether to generate ids for the geojson features. When enabled, the `feature.id` property will be auto assigned based on its index in the `features` array, over-writing any previous values." + }, + "promoteId":{ + "type":"promoteId", + "doc":"A property to use as a feature id (for feature state). Either a property name, or an object of the form `{: }`." + } + }, + "source_video":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "video":{ + "doc":"A video data source." + } + }, + "doc":"The data type of the video source." + }, + "urls":{ + "required":true, + "type":"array", + "value":"string", + "doc":"URLs to video content in order of preferred format." + }, + "coordinates":{ + "required":true, + "doc":"Corners of video specified in longitude, latitude pairs.", + "type":"array", + "length":4, + "value":{ + "type":"array", + "length":2, + "value":"number", + "doc":"A single longitude, latitude pair." + } + } + }, + "source_image":{ + "type":{ + "required":true, + "type":"enum", + "values":{ + "image":{ + "doc":"An image data source." + } + }, + "doc":"The data type of the image source." + }, + "url":{ + "required":true, + "type":"string", + "doc":"URL that points to an image." + }, + "coordinates":{ + "required":true, + "doc":"Corners of image specified in longitude, latitude pairs.", + "type":"array", + "length":4, + "value":{ + "type":"array", + "length":2, + "value":"number", + "doc":"A single longitude, latitude pair." + } + } + }, + "layer":{ + "id":{ + "type":"string", + "doc":"Unique layer name.", + "required":true + }, + "type":{ + "type":"enum", + "values":{ + "fill":{ + "doc":"A filled polygon with an optional stroked border.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + }, + "line":{ + "doc":"A stroked line.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + }, + "symbol":{ + "doc":"An icon or a text label.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + }, + "circle":{ + "doc":"A filled circle.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + }, + "heatmap":{ + "doc":"A heatmap.", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "fill-extrusion":{ + "doc":"An extruded (3D) polygon.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + } + }, + "raster":{ + "doc":"Raster map textures such as satellite imagery.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + }, + "hillshade":{ + "doc":"Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB and Mapzen Terrarium tiles.", + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "background":{ + "doc":"The background color or pattern of the map.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + } + } + }, + "doc":"Rendering type of this layer.", + "required":true + }, + "metadata":{ + "type":"*", + "doc":"Arbitrary properties useful to track with the layer, but do not influence rendering. Properties should be prefixed to avoid collisions, like 'mapbox:'." + }, + "source":{ + "type":"string", + "doc":"Name of a source description to be used for this layer. Required for all layer types except `background`." + }, + "source-layer":{ + "type":"string", + "doc":"Layer to use from a vector tile source. Required for vector tile sources; prohibited for all other source types, including GeoJSON sources." + }, + "minzoom":{ + "type":"number", + "minimum":0, + "maximum":24, + "doc":"The minimum zoom level for the layer. At zoom levels less than the minzoom, the layer will be hidden." + }, + "maxzoom":{ + "type":"number", + "minimum":0, + "maximum":24, + "doc":"The maximum zoom level for the layer. At zoom levels equal to or greater than the maxzoom, the layer will be hidden." + }, + "filter":{ + "type":"filter", + "doc":"A expression specifying conditions on source features. Only features that match the filter are displayed. Zoom expressions in filters are only evaluated at integer zoom levels. The `feature-state` expression is not supported in filter expressions." + }, + "layout":{ + "type":"layout", + "doc":"Layout properties for the layer." + }, + "paint":{ + "type":"paint", + "doc":"Default paint properties for this layer." + } + }, + "layout":[ + "layout_fill", + "layout_line", + "layout_circle", + "layout_heatmap", + "layout_fill-extrusion", + "layout_symbol", + "layout_raster", + "layout_hillshade", + "layout_background" + ], + "layout_background":{ + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_fill":{ + "fill-sort-key":{ + "type":"number", + "doc":"Sorts features in ascending order based on this value. Features with a higher sort key will appear above features with a lower sort key.", + "sdk-support":{ + "basic functionality":{ + "js":"1.2.0" + }, + "data-driven styling":{ + "js":"1.2.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_circle":{ + "circle-sort-key":{ + "type":"number", + "doc":"Sorts features in ascending order based on this value. Features with a higher sort key will appear above features with a lower sort key.", + "sdk-support":{ + "basic functionality":{ + "js":"1.2.0" + }, + "data-driven styling":{ + "js":"1.2.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_heatmap":{ + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "property-type":"constant" + } + }, + "layout_fill-extrusion":{ + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "property-type":"constant" + } + }, + "layout_line":{ + "line-cap":{ + "type":"enum", + "values":{ + "butt":{ + "doc":"A cap with a squared-off end which is drawn to the exact endpoint of the line." + }, + "round":{ + "doc":"A cap with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line." + }, + "square":{ + "doc":"A cap with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width." + } + }, + "default":"butt", + "doc":"The display of line endings.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "line-join":{ + "type":"enum", + "values":{ + "bevel":{ + "doc":"A join with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width." + }, + "round":{ + "doc":"A join with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line." + }, + "miter":{ + "doc":"A join with a sharp, angled corner which is drawn with the outer sides beyond the endpoint of the path until they meet." + } + }, + "default":"miter", + "doc":"The display of lines when joining.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.40.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "line-miter-limit":{ + "type":"number", + "default":2, + "doc":"Used to automatically convert miter joins to bevel joins for sharp angles.", + "requires":[ + { + "line-join":"miter" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "line-round-limit":{ + "type":"number", + "default":1.05, + "doc":"Used to automatically convert round joins to miter joins for shallow angles.", + "requires":[ + { + "line-join":"round" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "line-sort-key":{ + "type":"number", + "doc":"Sorts features in ascending order based on this value. Features with a higher sort key will appear above features with a lower sort key.", + "sdk-support":{ + "basic functionality":{ + "js":"1.2.0" + }, + "data-driven styling":{ + "js":"1.2.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_symbol":{ + "symbol-placement":{ + "type":"enum", + "values":{ + "point":{ + "doc":"The label is placed at the point where the geometry is located." + }, + "line":{ + "doc":"The label is placed along the line of the geometry. Can only be used on `LineString` and `Polygon` geometries." + }, + "line-center":{ + "doc":"The label is placed at the center of the line of the geometry. Can only be used on `LineString` and `Polygon` geometries. Note that a single feature in a vector tile may contain multiple line geometries." + } + }, + "default":"point", + "doc":"Label placement relative to its geometry.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "`line-center` value":{ + "js":"0.47.0", + "android":"6.4.0", + "ios":"4.3.0", + "macos":"0.10.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "symbol-spacing":{ + "type":"number", + "default":250, + "minimum":1, + "units":"pixels", + "doc":"Distance between two symbol anchors.", + "requires":[ + { + "symbol-placement":"line" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "symbol-avoid-edges":{ + "type":"boolean", + "default":false, + "doc":"If true, the symbols will not cross tile edges to avoid mutual collisions. Recommended in layers that don't have enough padding in the vector tile to prevent collisions, or if it is a point symbol layer placed after a line symbol layer. When using a client that supports global collision detection, like Mapbox GL JS version 0.42.0 or greater, enabling this property is not needed to prevent clipped labels at tile boundaries.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "symbol-sort-key":{ + "type":"number", + "doc":"Sorts features in ascending order based on this value. Features with lower sort keys are drawn and placed first. When `icon-allow-overlap` or `text-allow-overlap` is `false`, features with a lower sort key will have priority during placement. When `icon-allow-overlap` or `text-allow-overlap` is set to `true`, features with a higher sort key will overlap over features with a lower sort key.", + "sdk-support":{ + "basic functionality":{ + "js":"0.53.0", + "android":"7.4.0", + "ios":"4.11.0", + "macos":"0.14.0" + }, + "data-driven styling":{ + "js":"0.53.0", + "android":"7.4.0", + "ios":"4.11.0", + "macos":"0.14.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "symbol-z-order":{ + "type":"enum", + "values":{ + "auto":{ + "doc":"If `symbol-sort-key` is set, sort based on that. Otherwise sort symbols by their y-position relative to the viewport." + }, + "viewport-y":{ + "doc":"Symbols will be sorted by their y-position relative to the viewport." + }, + "source":{ + "doc":"Symbols will be rendered in the same order as the source data with no sorting applied." + } + }, + "default":"auto", + "doc":"Controls the order in which overlapping symbols in the same layer are rendered", + "sdk-support":{ + "basic functionality":{ + "js":"0.49.0", + "android":"6.6.0", + "ios":"4.5.0", + "macos":"0.12.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-allow-overlap":{ + "type":"boolean", + "default":false, + "doc":"If true, the icon will be visible even if it collides with other previously drawn symbols.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-ignore-placement":{ + "type":"boolean", + "default":false, + "doc":"If true, other symbols can be visible even if they collide with the icon.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-optional":{ + "type":"boolean", + "default":false, + "doc":"If true, text will display without their corresponding icons when the icon collides with other symbols and the text does not.", + "requires":[ + "icon-image", + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-rotation-alignment":{ + "type":"enum", + "values":{ + "map":{ + "doc":"When `symbol-placement` is set to `point`, aligns icons east-west. When `symbol-placement` is set to `line` or `line-center`, aligns icon x-axes with the line." + }, + "viewport":{ + "doc":"Produces icons whose x-axes are aligned with the x-axis of the viewport, regardless of the value of `symbol-placement`." + }, + "auto":{ + "doc":"When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line` or `line-center`, this is equivalent to `map`." + } + }, + "default":"auto", + "doc":"In combination with `symbol-placement`, determines the rotation behavior of icons.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "`auto` value":{ + "js":"0.25.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.3.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-size":{ + "type":"number", + "default":1, + "minimum":0, + "units":"factor of the original icon size", + "doc":"Scales the original size of the icon by the provided factor. The new pixel size of the image will be the original pixel size multiplied by `icon-size`. 1 is the original size; 3 triples the size of the image.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.35.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "icon-text-fit":{ + "type":"enum", + "values":{ + "none":{ + "doc":"The icon is displayed at its intrinsic aspect ratio." + }, + "width":{ + "doc":"The icon is scaled in the x-dimension to fit the width of the text." + }, + "height":{ + "doc":"The icon is scaled in the y-dimension to fit the height of the text." + }, + "both":{ + "doc":"The icon is scaled in both x- and y-dimensions." + } + }, + "default":"none", + "doc":"Scales the icon to fit around the associated text.", + "requires":[ + "icon-image", + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.21.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.2.1" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-text-fit-padding":{ + "type":"array", + "value":"number", + "length":4, + "default":[ + 0, + 0, + 0, + 0 + ], + "units":"pixels", + "doc":"Size of the additional area added to dimensions determined by `icon-text-fit`, in clockwise order: top, right, bottom, left.", + "requires":[ + "icon-image", + "text-field", + { + "icon-text-fit":[ + "both", + "width", + "height" + ] + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.21.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.2.1" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-image":{ + "type":"resolvedImage", + "doc":"Name of image in sprite to use for drawing an image background.", + "tokens":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.35.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "icon-rotate":{ + "type":"number", + "default":0, + "period":360, + "units":"degrees", + "doc":"Rotates the icon clockwise.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.21.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "icon-padding":{ + "type":"number", + "default":2, + "minimum":0, + "units":"pixels", + "doc":"Size of the additional area around the icon bounding box used for detecting symbol collisions.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-keep-upright":{ + "type":"boolean", + "default":false, + "doc":"If true, the icon may be flipped to prevent it from being rendered upside-down.", + "requires":[ + "icon-image", + { + "icon-rotation-alignment":"map" + }, + { + "symbol-placement":[ + "line", + "line-center" + ] + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-offset":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "doc":"Offset distance of icon from its anchor. Positive values indicate right and down, while negative values indicate left and up. Each component is multiplied by the value of `icon-size` to obtain the final offset in pixels. When combined with `icon-rotate` the offset will be as if the rotated direction was up.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "icon-anchor":{ + "type":"enum", + "values":{ + "center":{ + "doc":"The center of the icon is placed closest to the anchor." + }, + "left":{ + "doc":"The left side of the icon is placed closest to the anchor." + }, + "right":{ + "doc":"The right side of the icon is placed closest to the anchor." + }, + "top":{ + "doc":"The top of the icon is placed closest to the anchor." + }, + "bottom":{ + "doc":"The bottom of the icon is placed closest to the anchor." + }, + "top-left":{ + "doc":"The top left corner of the icon is placed closest to the anchor." + }, + "top-right":{ + "doc":"The top right corner of the icon is placed closest to the anchor." + }, + "bottom-left":{ + "doc":"The bottom left corner of the icon is placed closest to the anchor." + }, + "bottom-right":{ + "doc":"The bottom right corner of the icon is placed closest to the anchor." + } + }, + "default":"center", + "doc":"Part of the icon placed closest to the anchor.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.40.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + }, + "data-driven styling":{ + "js":"0.40.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "icon-pitch-alignment":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The icon is aligned to the plane of the map." + }, + "viewport":{ + "doc":"The icon is aligned to the plane of the viewport." + }, + "auto":{ + "doc":"Automatically matches the value of `icon-rotation-alignment`." + } + }, + "default":"auto", + "doc":"Orientation of icon when map is pitched.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.39.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-pitch-alignment":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The text is aligned to the plane of the map." + }, + "viewport":{ + "doc":"The text is aligned to the plane of the viewport." + }, + "auto":{ + "doc":"Automatically matches the value of `text-rotation-alignment`." + } + }, + "default":"auto", + "doc":"Orientation of text when map is pitched.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.21.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.2.1" + }, + "`auto` value":{ + "js":"0.25.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.3.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-rotation-alignment":{ + "type":"enum", + "values":{ + "map":{ + "doc":"When `symbol-placement` is set to `point`, aligns text east-west. When `symbol-placement` is set to `line` or `line-center`, aligns text x-axes with the line." + }, + "viewport":{ + "doc":"Produces glyphs whose x-axes are aligned with the x-axis of the viewport, regardless of the value of `symbol-placement`." + }, + "auto":{ + "doc":"When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line` or `line-center`, this is equivalent to `map`." + } + }, + "default":"auto", + "doc":"In combination with `symbol-placement`, determines the rotation behavior of the individual glyphs forming the text.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "`auto` value":{ + "js":"0.25.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.3.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-field":{ + "type":"formatted", + "default":"", + "tokens":true, + "doc":"Value to use for a text label. If a plain `string` is provided, it will be treated as a `formatted` with default/inherited formatting options.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-font":{ + "type":"array", + "value":"string", + "default":[ + "Open Sans Regular", + "Arial Unicode MS Regular" + ], + "doc":"Font stack to use for displaying text.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-size":{ + "type":"number", + "default":16, + "minimum":0, + "units":"pixels", + "doc":"Font size.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.35.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-max-width":{ + "type":"number", + "default":10, + "minimum":0, + "units":"ems", + "doc":"The maximum line width for text wrapping.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.40.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-line-height":{ + "type":"number", + "default":1.2, + "units":"ems", + "doc":"Text leading value for multi-line text.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-letter-spacing":{ + "type":"number", + "default":0, + "units":"ems", + "doc":"Text tracking amount.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.40.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-justify":{ + "type":"enum", + "values":{ + "auto":{ + "doc":"The text is aligned towards the anchor position." + }, + "left":{ + "doc":"The text is aligned to the left." + }, + "center":{ + "doc":"The text is centered." + }, + "right":{ + "doc":"The text is aligned to the right." + } + }, + "default":"center", + "doc":"Text justification options.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.39.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + }, + "auto":{ + "js":"0.54.0", + "android":"7.4.0", + "ios":"4.10.0", + "macos":"0.14.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-radial-offset":{ + "type":"number", + "units":"ems", + "default":0, + "doc":"Radial offset of text, in the direction of the symbol's anchor. Useful in combination with `text-variable-anchor`, which defaults to using the two-dimensional `text-offset` if present.", + "sdk-support":{ + "basic functionality":{ + "js":"0.54.0", + "android":"7.4.0", + "ios":"4.10.0", + "macos":"0.14.0" + }, + "data-driven styling":{ + "js":"0.54.0", + "android":"7.4.0", + "ios":"4.10.0", + "macos":"0.14.0" + } + }, + "requires":[ + "text-field" + ], + "property-type":"data-driven", + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + } + }, + "text-variable-anchor":{ + "type":"array", + "value":"enum", + "values":{ + "center":{ + "doc":"The center of the text is placed closest to the anchor." + }, + "left":{ + "doc":"The left side of the text is placed closest to the anchor." + }, + "right":{ + "doc":"The right side of the text is placed closest to the anchor." + }, + "top":{ + "doc":"The top of the text is placed closest to the anchor." + }, + "bottom":{ + "doc":"The bottom of the text is placed closest to the anchor." + }, + "top-left":{ + "doc":"The top left corner of the text is placed closest to the anchor." + }, + "top-right":{ + "doc":"The top right corner of the text is placed closest to the anchor." + }, + "bottom-left":{ + "doc":"The bottom left corner of the text is placed closest to the anchor." + }, + "bottom-right":{ + "doc":"The bottom right corner of the text is placed closest to the anchor." + } + }, + "requires":[ + "text-field", + { + "symbol-placement":[ + "point" + ] + } + ], + "doc":"To increase the chance of placing high-priority labels on the map, you can provide an array of `text-anchor` locations: the renderer will attempt to place the label at each location, in order, before moving onto the next label. Use `text-justify: auto` to choose justification based on anchor position. To apply an offset, use the `text-radial-offset` or the two-dimensional `text-offset`.", + "sdk-support":{ + "basic functionality":{ + "js":"0.54.0", + "android":"7.4.0", + "ios":"4.10.0", + "macos":"0.14.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-anchor":{ + "type":"enum", + "values":{ + "center":{ + "doc":"The center of the text is placed closest to the anchor." + }, + "left":{ + "doc":"The left side of the text is placed closest to the anchor." + }, + "right":{ + "doc":"The right side of the text is placed closest to the anchor." + }, + "top":{ + "doc":"The top of the text is placed closest to the anchor." + }, + "bottom":{ + "doc":"The bottom of the text is placed closest to the anchor." + }, + "top-left":{ + "doc":"The top left corner of the text is placed closest to the anchor." + }, + "top-right":{ + "doc":"The top right corner of the text is placed closest to the anchor." + }, + "bottom-left":{ + "doc":"The bottom left corner of the text is placed closest to the anchor." + }, + "bottom-right":{ + "doc":"The bottom right corner of the text is placed closest to the anchor." + } + }, + "default":"center", + "doc":"Part of the text placed closest to the anchor.", + "requires":[ + "text-field", + { + "!":"text-variable-anchor" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.39.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-max-angle":{ + "type":"number", + "default":45, + "units":"degrees", + "doc":"Maximum angle change between adjacent characters.", + "requires":[ + "text-field", + { + "symbol-placement":[ + "line", + "line-center" + ] + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-writing-mode":{ + "type":"array", + "value":"enum", + "values":{ + "horizontal":{ + "doc":"If a text's language supports horizontal writing mode, symbols with point placement would be laid out horizontally." + }, + "vertical":{ + "doc":"If a text's language supports vertical writing mode, symbols with point placement would be laid out vertically." + } + }, + "doc":"The property allows control over a symbol's orientation. Note that the property values act as a hint, so that a symbol whose language doesn’t support the provided orientation will be laid out in its natural orientation. Example: English point symbol will be rendered horizontally even if array value contains single 'vertical' enum value. The order of elements in an array define priority order for the placement of an orientation variant.", + "requires":[ + "text-field", + { + "symbol-placement":[ + "point" + ] + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"1.3.0", + "android":"8.3.0", + "ios":"5.3.0", + "macos":"0.14.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-rotate":{ + "type":"number", + "default":0, + "period":360, + "units":"degrees", + "doc":"Rotates the text clockwise.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.35.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-padding":{ + "type":"number", + "default":2, + "minimum":0, + "units":"pixels", + "doc":"Size of the additional area around the text bounding box used for detecting symbol collisions.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-keep-upright":{ + "type":"boolean", + "default":true, + "doc":"If true, the text may be flipped vertically to prevent it from being rendered upside-down.", + "requires":[ + "text-field", + { + "text-rotation-alignment":"map" + }, + { + "symbol-placement":[ + "line", + "line-center" + ] + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-transform":{ + "type":"enum", + "values":{ + "none":{ + "doc":"The text is not altered." + }, + "uppercase":{ + "doc":"Forces all letters to be displayed in uppercase." + }, + "lowercase":{ + "doc":"Forces all letters to be displayed in lowercase." + } + }, + "default":"none", + "doc":"Specifies how to capitalize text, similar to the CSS `text-transform` property.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-offset":{ + "type":"array", + "doc":"Offset distance of text from its anchor. Positive values indicate right and down, while negative values indicate left and up. If used with text-variable-anchor, input values will be taken as absolute values. Offsets along the x- and y-axis will be applied automatically based on the anchor position.", + "value":"number", + "units":"ems", + "length":2, + "default":[ + 0, + 0 + ], + "requires":[ + "text-field", + { + "!":"text-radial-offset" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.35.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"data-driven" + }, + "text-allow-overlap":{ + "type":"boolean", + "default":false, + "doc":"If true, the text will be visible even if it collides with other previously drawn symbols.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-ignore-placement":{ + "type":"boolean", + "default":false, + "doc":"If true, other symbols can be visible even if they collide with the text.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-optional":{ + "type":"boolean", + "default":false, + "doc":"If true, icons will display without their corresponding text when the text collides with other symbols and the icon does not.", + "requires":[ + "text-field", + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_raster":{ + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "property-type":"constant" + } + }, + "layout_hillshade":{ + "visibility":{ + "type":"enum", + "values":{ + "visible":{ + "doc":"The layer is shown." + }, + "none":{ + "doc":"The layer is not shown." + } + }, + "default":"visible", + "doc":"Whether this layer is displayed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "property-type":"constant" + } + }, + "filter":{ + "type":"array", + "value":"*", + "doc":"A filter selects specific features from a layer." + }, + "filter_operator":{ + "type":"enum", + "values":{ + "==":{ + "doc":"`[\"==\", key, value]` equality: `feature[key] = value`" + }, + "!=":{ + "doc":"`[\"!=\", key, value]` inequality: `feature[key] ≠ value`" + }, + ">":{ + "doc":"`[\">\", key, value]` greater than: `feature[key] > value`" + }, + ">=":{ + "doc":"`[\">=\", key, value]` greater than or equal: `feature[key] ≥ value`" + }, + "<":{ + "doc":"`[\"<\", key, value]` less than: `feature[key] < value`" + }, + "<=":{ + "doc":"`[\"<=\", key, value]` less than or equal: `feature[key] ≤ value`" + }, + "in":{ + "doc":"`[\"in\", key, v0, ..., vn]` set inclusion: `feature[key] ∈ {v0, ..., vn}`" + }, + "!in":{ + "doc":"`[\"!in\", key, v0, ..., vn]` set exclusion: `feature[key] ∉ {v0, ..., vn}`" + }, + "all":{ + "doc":"`[\"all\", f0, ..., fn]` logical `AND`: `f0 ∧ ... ∧ fn`" + }, + "any":{ + "doc":"`[\"any\", f0, ..., fn]` logical `OR`: `f0 ∨ ... ∨ fn`" + }, + "none":{ + "doc":"`[\"none\", f0, ..., fn]` logical `NOR`: `¬f0 ∧ ... ∧ ¬fn`" + }, + "has":{ + "doc":"`[\"has\", key]` `feature[key]` exists" + }, + "!has":{ + "doc":"`[\"!has\", key]` `feature[key]` does not exist" + } + }, + "doc":"The filter operator." + }, + "geometry_type":{ + "type":"enum", + "values":{ + "Point":{ + "doc":"Filter to point geometries." + }, + "LineString":{ + "doc":"Filter to line geometries." + }, + "Polygon":{ + "doc":"Filter to polygon geometries." + } + }, + "doc":"The geometry type for the filter to select." + }, + "function":{ + "expression":{ + "type":"expression", + "doc":"An expression." + }, + "stops":{ + "type":"array", + "doc":"An array of stops.", + "value":"function_stop" + }, + "base":{ + "type":"number", + "default":1, + "minimum":0, + "doc":"The exponential base of the interpolation curve. It controls the rate at which the result increases. Higher values make the result increase more towards the high end of the range. With `1` the stops are interpolated linearly." + }, + "property":{ + "type":"string", + "doc":"The name of a feature property to use as the function input.", + "default":"$zoom" + }, + "type":{ + "type":"enum", + "values":{ + "identity":{ + "doc":"Return the input value as the output value." + }, + "exponential":{ + "doc":"Generate an output by interpolating between stops just less than and just greater than the function input." + }, + "interval":{ + "doc":"Return the output value of the stop just less than the function input." + }, + "categorical":{ + "doc":"Return the output value of the stop equal to the function input." + } + }, + "doc":"The interpolation strategy to use in function evaluation.", + "default":"exponential" + }, + "colorSpace":{ + "type":"enum", + "values":{ + "rgb":{ + "doc":"Use the RGB color space to interpolate color values" + }, + "lab":{ + "doc":"Use the LAB color space to interpolate color values." + }, + "hcl":{ + "doc":"Use the HCL color space to interpolate color values, interpolating the Hue, Chroma, and Luminance channels individually." + } + }, + "doc":"The color space in which colors interpolated. Interpolating colors in perceptual color spaces like LAB and HCL tend to produce color ramps that look more consistent and produce colors that can be differentiated more easily than those interpolated in RGB space.", + "default":"rgb" + }, + "default":{ + "type":"*", + "required":false, + "doc":"A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:\n* In categorical functions, when the feature value does not match any of the stop domain values.\n* In property and zoom-and-property functions, when a feature does not contain a value for the specified property.\n* In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a `circle-color` property but the feature property value is not a string or not a valid color).\n* In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.\nIf no default is provided, the style property's default is used in these circumstances." + } + }, + "function_stop":{ + "type":"array", + "minimum":0, + "maximum":24, + "value":[ + "number", + "color" + ], + "length":2, + "doc":"Zoom level and value pair." + }, + "expression":{ + "type":"array", + "value":"*", + "minimum":1, + "doc":"An expression defines a function that can be used for data-driven style properties or feature filters." + }, + "expression_name":{ + "doc":"", + "type":"enum", + "values":{ + "let":{ + "doc":"Binds expressions to named variables, which can then be referenced in the result expression using [\"var\", \"variable_name\"].", + "group":"Variable binding", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "var":{ + "doc":"References variable bound using \"let\".", + "group":"Variable binding", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "literal":{ + "doc":"Provides a literal array or object value.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "array":{ + "doc":"Asserts that the input is an array (optionally with a specific item type and length). If, when the input expression is evaluated, it is not of the asserted type, then this assertion will cause the whole expression to be aborted.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "at":{ + "doc":"Retrieves an item from an array.", + "group":"Lookup", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "in":{ + "doc":"Determines whether an item exists in an array or a substring exists in a string.", + "group":"Lookup", + "sdk-support":{ + "basic functionality":{ + "js":"1.6.0" + } + } + }, + "case":{ + "doc":"Selects the first output whose corresponding test condition evaluates to true, or the fallback value otherwise.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "match":{ + "doc":"Selects the output whose label value matches the input value, or the fallback value if no match is found. The input can be any expression (e.g. `[\"get\", \"building_type\"]`). Each label must be either:\n * a single literal value; or\n * an array of literal values, whose values must be all strings or all numbers (e.g. `[100, 101]` or `[\"c\", \"b\"]`). The input matches if any of the values in the array matches, similar to the `\"in\"` operator.\n\nEach label must be unique. If the input type does not match the type of the labels, the result will be the fallback value.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "coalesce":{ + "doc":"Evaluates each expression in turn until the first non-null value is obtained, and returns that value.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "step":{ + "doc":"Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. Returns the output value of the stop just less than the input, or the first output if the input is less than the first stop.", + "group":"Ramps, scales, curves", + "sdk-support":{ + "basic functionality":{ + "js":"0.42.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "interpolate":{ + "doc":"Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, or `color`.\n\nInterpolation types:\n- `[\"linear\"]`: interpolates linearly between the pair of stops just less than and just greater than the input.\n- `[\"exponential\", base]`: interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: interpolates using the cubic bezier curve defined by the given control points.", + "group":"Ramps, scales, curves", + "sdk-support":{ + "basic functionality":{ + "js":"0.42.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "interpolate-hcl":{ + "doc":"Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color`, and the interpolation is performed in the Hue-Chroma-Luminance color space.", + "group":"Ramps, scales, curves", + "sdk-support":{ + "basic functionality":{ + "js":"0.49.0" + } + } + }, + "interpolate-lab":{ + "doc":"Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color`, and the interpolation is performed in the CIELAB color space.", + "group":"Ramps, scales, curves", + "sdk-support":{ + "basic functionality":{ + "js":"0.49.0" + } + } + }, + "ln2":{ + "doc":"Returns mathematical constant ln(2).", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "pi":{ + "doc":"Returns the mathematical constant pi.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "e":{ + "doc":"Returns the mathematical constant e.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "typeof":{ + "doc":"Returns a string describing the type of the given value.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "string":{ + "doc":"Asserts that the input value is a string. If multiple values are provided, each one is evaluated in order until a string is obtained. If none of the inputs are strings, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "number":{ + "doc":"Asserts that the input value is a number. If multiple values are provided, each one is evaluated in order until a number is obtained. If none of the inputs are numbers, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "boolean":{ + "doc":"Asserts that the input value is a boolean. If multiple values are provided, each one is evaluated in order until a boolean is obtained. If none of the inputs are booleans, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "object":{ + "doc":"Asserts that the input value is an object. If multiple values are provided, each one is evaluated in order until an object is obtained. If none of the inputs are objects, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "collator":{ + "doc":"Returns a `collator` for use in locale-dependent comparison operations. The `case-sensitive` and `diacritic-sensitive` options default to `false`. The `locale` argument specifies the IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale. Use `resolved-locale` to test the results of locale fallback behavior.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + "format":{ + "doc":"Returns `formatted` text containing annotations for use in mixed-format `text-field` entries. For a `text-field` entries of a string type, following option object's properties are supported: If set, the `text-font` value overrides the font specified by the root layout properties. If set, the `font-scale` value specifies a scaling factor relative to the `text-size` specified in the root layout properties. If set, the `text-color` value overrides the color specified by the root paint properties for this layer.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.48.0", + "android":"6.7.0", + "ios":"4.6.0", + "macos":"0.12.0" + }, + "text-font":{ + "js":"0.48.0", + "android":"6.7.0", + "ios":"4.6.0", + "macos":"0.12.0" + }, + "font-scale":{ + "js":"0.48.0", + "android":"6.7.0", + "ios":"4.6.0", + "macos":"0.12.0" + }, + "text-color":{ + "js":"1.3.0", + "android":"7.3.0", + "ios":"4.10.0", + "macos":"0.14.0" + }, + "image":{ + "js":"1.6.0" + } + } + }, + "image":{ + "doc":"Returns an `image` type for use in `icon-image`, `*-pattern` entries and as a section in the `format` expression. If set, the `image` argument will check that the requested image exists in the style and will return either the resolved image name or `null`, depending on whether or not the image is currently in the style. This validation process is synchronous and requires the image to have been added to the style before requesting it in the `image` argument.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"1.4.0", + "android":"8.6.0", + "ios":"5.6.0" + } + } + }, + "number-format":{ + "doc":"Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.54.0" + } + } + }, + "to-string":{ + "doc":"Converts the input value to a string. If the input is `null`, the result is `\"\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "to-number":{ + "doc":"Converts the input value to a number, if possible. If the input is `null` or `false`, the result is 0. If the input is `true`, the result is 1. If the input is a string, it is converted to a number as specified by the [\"ToNumber Applied to the String Type\" algorithm](https://tc39.github.io/ecma262/#sec-tonumber-applied-to-the-string-type) of the ECMAScript Language Specification. If multiple values are provided, each one is evaluated in order until the first successful conversion is obtained. If none of the inputs can be converted, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "to-boolean":{ + "doc":"Converts the input value to a boolean. The result is `false` when then input is an empty string, 0, `false`, `null`, or `NaN`; otherwise it is `true`.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "to-rgba":{ + "doc":"Returns a four-element array containing the input color's red, green, blue, and alpha components, in that order.", + "group":"Color", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "to-color":{ + "doc":"Converts the input value to a color. If multiple values are provided, each one is evaluated in order until the first successful conversion is obtained. If none of the inputs can be converted, the expression is an error.", + "group":"Types", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "rgb":{ + "doc":"Creates a color value from red, green, and blue components, which must range between 0 and 255, and an alpha component of 1. If any component is out of range, the expression is an error.", + "group":"Color", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "rgba":{ + "doc":"Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between 0 and 1. If any component is out of range, the expression is an error.", + "group":"Color", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "get":{ + "doc":"Retrieves a property value from the current feature's properties, or from another object if a second argument is provided. Returns null if the requested property is missing.", + "group":"Lookup", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "has":{ + "doc":"Tests for the presence of an property value in the current feature's properties, or from another object if a second argument is provided.", + "group":"Lookup", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "length":{ + "doc":"Gets the length of an array or string.", + "group":"Lookup", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "properties":{ + "doc":"Gets the feature properties object. Note that in some cases, it may be more efficient to use [\"get\", \"property_name\"] directly.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "feature-state":{ + "doc":"Retrieves a property value from the current feature's state. Returns null if the requested property is not present on the feature's state. A feature's state is not part of the GeoJSON or vector tile data, and must be set programmatically on each feature. Features are identified by their `id` attribute, which must be an integer or a string that can be cast to an integer. Note that [\"feature-state\"] can only be used with paint properties that support data-driven styling.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.46.0" + } + } + }, + "geometry-type":{ + "doc":"Gets the feature's geometry type: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "id":{ + "doc":"Gets the feature's id, if it has one.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "zoom":{ + "doc":"Gets the current zoom level. Note that in style layout and paint properties, [\"zoom\"] may only appear as the input to a top-level \"step\" or \"interpolate\" expression.", + "group":"Zoom", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "heatmap-density":{ + "doc":"Gets the kernel density estimation of a pixel in a heatmap layer, which is a relative measure of how many data points are crowded around a particular pixel. Can only be used in the `heatmap-color` property.", + "group":"Heatmap", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "line-progress":{ + "doc":"Gets the progress along a gradient line. Can only be used in the `line-gradient` property.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.6.0", + "macos":"0.12.0" + } + } + }, + "accumulated":{ + "doc":"Gets the value of a cluster property accumulated so far. Can only be used in the `clusterProperties` option of a clustered GeoJSON source.", + "group":"Feature data", + "sdk-support":{ + "basic functionality":{ + "js":"0.53.0" + } + } + }, + "+":{ + "doc":"Returns the sum of the inputs.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "*":{ + "doc":"Returns the product of the inputs.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "-":{ + "doc":"For two inputs, returns the result of subtracting the second input from the first. For a single input, returns the result of subtracting it from 0.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "/":{ + "doc":"Returns the result of floating point division of the first input by the second.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "%":{ + "doc":"Returns the remainder after integer division of the first input by the second.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "^":{ + "doc":"Returns the result of raising the first input to the power specified by the second.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "sqrt":{ + "doc":"Returns the square root of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.42.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "log10":{ + "doc":"Returns the base-ten logarithm of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "ln":{ + "doc":"Returns the natural logarithm of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "log2":{ + "doc":"Returns the base-two logarithm of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "sin":{ + "doc":"Returns the sine of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "cos":{ + "doc":"Returns the cosine of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "tan":{ + "doc":"Returns the tangent of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "asin":{ + "doc":"Returns the arcsine of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "acos":{ + "doc":"Returns the arccosine of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "atan":{ + "doc":"Returns the arctangent of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "min":{ + "doc":"Returns the minimum value of the inputs.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "max":{ + "doc":"Returns the maximum value of the inputs.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "round":{ + "doc":"Rounds the input to the nearest integer. Halfway values are rounded away from zero. For example, `[\"round\", -1.5]` evaluates to -2.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "abs":{ + "doc":"Returns the absolute value of the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "ceil":{ + "doc":"Returns the smallest integer that is greater than or equal to the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "floor":{ + "doc":"Returns the largest integer that is less than or equal to the input.", + "group":"Math", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "==":{ + "doc":"Returns `true` if the input values are equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + "!=":{ + "doc":"Returns `true` if the input values are not equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + ">":{ + "doc":"Returns `true` if the first input is strictly greater than the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + "<":{ + "doc":"Returns `true` if the first input is strictly less than the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + ">=":{ + "doc":"Returns `true` if the first input is greater than or equal to the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + "<=":{ + "doc":"Returns `true` if the first input is less than or equal to the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "collator":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + }, + "all":{ + "doc":"Returns `true` if all the inputs are `true`, `false` otherwise. The inputs are evaluated in order, and evaluation is short-circuiting: once an input expression evaluates to `false`, the result is `false` and no further input expressions are evaluated.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "any":{ + "doc":"Returns `true` if any of the inputs are `true`, `false` otherwise. The inputs are evaluated in order, and evaluation is short-circuiting: once an input expression evaluates to `true`, the result is `true` and no further input expressions are evaluated.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "!":{ + "doc":"Logical negation. Returns `true` if the input is `false`, and `false` if the input is `true`.", + "group":"Decision", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "is-supported-script":{ + "doc":"Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the the `mapbox-gl-rtl-text` plugin is not in use in Mapbox GL JS).", + "group":"String", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.6.0" + } + } + }, + "upcase":{ + "doc":"Returns the input string converted to uppercase. Follows the Unicode Default Case Conversion algorithm and the locale-insensitive case mappings in the Unicode Character Database.", + "group":"String", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "downcase":{ + "doc":"Returns the input string converted to lowercase. Follows the Unicode Default Case Conversion algorithm and the locale-insensitive case mappings in the Unicode Character Database.", + "group":"String", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "concat":{ + "doc":"Returns a `string` consisting of the concatenation of the inputs. Each input is converted to a string as if by `to-string`.", + "group":"String", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + } + }, + "resolved-locale":{ + "doc":"Returns the IETF language tag of the locale being used by the provided `collator`. This can be used to determine the default system locale, or to determine if a requested locale was successfully loaded.", + "group":"String", + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + } + } + } + }, + "light":{ + "anchor":{ + "type":"enum", + "default":"viewport", + "values":{ + "map":{ + "doc":"The position of the light source is aligned to the rotation of the map." + }, + "viewport":{ + "doc":"The position of the light source is aligned to the rotation of the viewport." + } + }, + "property-type":"data-constant", + "transition":false, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "doc":"Whether extruded geometries are lit relative to the map or viewport.", + "example":"map", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + } + }, + "position":{ + "type":"array", + "default":[ + 1.15, + 210, + 30 + ], + "length":3, + "value":"number", + "property-type":"data-constant", + "transition":true, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "doc":"Position of the light source relative to lit (extruded) geometries, in [r radial coordinate, a azimuthal angle, p polar angle] where r indicates the distance from the center of the base of an object to its light, a indicates the position of the light relative to 0° (0° when `light.anchor` is set to `viewport` corresponds to the top of the viewport, or 0° when `light.anchor` is set to `map` corresponds to due north, and degrees proceed clockwise), and p indicates the height of the light (from 0°, directly above, to 180°, directly below).", + "example":[ + 1.5, + 90, + 80 + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + } + }, + "color":{ + "type":"color", + "property-type":"data-constant", + "default":"#ffffff", + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "transition":true, + "doc":"Color tint for lighting extruded geometries.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + } + }, + "intensity":{ + "type":"number", + "property-type":"data-constant", + "default":0.5, + "minimum":0, + "maximum":1, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "transition":true, + "doc":"Intensity of lighting (on a scale from 0 to 1). Higher numbers will present as more extreme contrast.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + } + } + }, + "paint":[ + "paint_fill", + "paint_line", + "paint_circle", + "paint_heatmap", + "paint_fill-extrusion", + "paint_symbol", + "paint_raster", + "paint_hillshade", + "paint_background" + ], + "paint_fill":{ + "fill-antialias":{ + "type":"boolean", + "default":true, + "doc":"Whether or not the fill should be antialiased.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-opacity":{ + "type":"number", + "default":1, + "minimum":0, + "maximum":1, + "doc":"The opacity of the entire fill layer. In contrast to the `fill-color`, this value will also affect the 1px stroke around the fill, if the stroke is used.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.21.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-color":{ + "type":"color", + "default":"#000000", + "doc":"The color of the filled part of this layer. This color can be specified as `rgba` with an alpha component and the color's opacity will not affect the opacity of the 1px stroke, if it is used.", + "transition":true, + "requires":[ + { + "!":"fill-pattern" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.19.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-outline-color":{ + "type":"color", + "doc":"The outline color of the fill. Matches the value of `fill-color` if unspecified.", + "transition":true, + "requires":[ + { + "!":"fill-pattern" + }, + { + "fill-antialias":true + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.19.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"The geometry's offset. Values are [x, y] where negatives indicate left and up, respectively.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The fill is translated relative to the map." + }, + "viewport":{ + "doc":"The fill is translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `fill-translate`.", + "default":"map", + "requires":[ + "fill-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-pattern":{ + "type":"resolvedImage", + "transition":true, + "doc":"Name of image in sprite to use for drawing image 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.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.49.0", + "android":"6.5.0", + "macos":"0.11.0", + "ios":"4.4.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"cross-faded-data-driven" + } + }, + "paint_fill-extrusion":{ + "fill-extrusion-opacity":{ + "type":"number", + "default":1, + "minimum":0, + "maximum":1, + "doc":"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.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-extrusion-color":{ + "type":"color", + "default":"#000000", + "doc":"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.", + "transition":true, + "requires":[ + { + "!":"fill-extrusion-pattern" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + }, + "data-driven styling":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-extrusion-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"The geometry's offset. Values are [x, y] where negatives indicate left and up (on the flat plane), respectively.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-extrusion-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The fill extrusion is translated relative to the map." + }, + "viewport":{ + "doc":"The fill extrusion is translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `fill-extrusion-translate`.", + "default":"map", + "requires":[ + "fill-extrusion-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "fill-extrusion-pattern":{ + "type":"resolvedImage", + "transition":true, + "doc":"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.", + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + }, + "data-driven styling":{ + "js":"0.49.0", + "android":"6.5.0", + "macos":"0.11.0", + "ios":"4.4.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"cross-faded-data-driven" + }, + "fill-extrusion-height":{ + "type":"number", + "default":0, + "minimum":0, + "units":"meters", + "doc":"The height with which to extrude this layer.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + }, + "data-driven styling":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-extrusion-base":{ + "type":"number", + "default":0, + "minimum":0, + "units":"meters", + "doc":"The height with which to extrude the base of this layer. Must be less than or equal to `fill-extrusion-height`.", + "transition":true, + "requires":[ + "fill-extrusion-height" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + }, + "data-driven styling":{ + "js":"0.27.0", + "android":"5.1.0", + "ios":"3.6.0", + "macos":"0.5.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "fill-extrusion-vertical-gradient":{ + "type":"boolean", + "default":true, + "doc":"Whether to apply a vertical gradient to the sides of a fill-extrusion layer. If true, sides will be shaded slightly darker farther down.", + "transition":false, + "sdk-support":{ + "basic functionality":{ + "js":"0.50.0", + "ios":"4.7.0", + "macos":"0.13.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "paint_line":{ + "line-opacity":{ + "type":"number", + "doc":"The opacity at which the line will be drawn.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-color":{ + "type":"color", + "doc":"The color with which the line will be drawn.", + "default":"#000000", + "transition":true, + "requires":[ + { + "!":"line-pattern" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.23.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"The geometry's offset. Values are [x, y] where negatives indicate left and up, respectively.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "line-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The line is translated relative to the map." + }, + "viewport":{ + "doc":"The line is translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `line-translate`.", + "default":"map", + "requires":[ + "line-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "line-width":{ + "type":"number", + "default":1, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Stroke thickness.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.39.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-gap-width":{ + "type":"number", + "default":0, + "minimum":0, + "doc":"Draws a line casing outside of a line's actual path. Value indicates the width of the inner gap.", + "transition":true, + "units":"pixels", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-offset":{ + "type":"number", + "default":0, + "doc":"The line's offset. For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a negative value to the left. For polygon features, a positive value results in an inset, and a negative value results in an outset.", + "transition":true, + "units":"pixels", + "sdk-support":{ + "basic functionality":{ + "js":"0.12.1", + "android":"3.0.0", + "ios":"3.1.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-blur":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Blur applied to the line, in pixels.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "line-dasharray":{ + "type":"array", + "value":"number", + "doc":"Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width. To convert a dash length to pixels, multiply the length by the current line width. Note that GeoJSON sources with `lineMetrics: true` specified won't render dashed lines to the expected scale. Also note that zoom-dependent expressions will be evaluated only at integer zoom levels.", + "minimum":0, + "transition":true, + "units":"line widths", + "requires":[ + { + "!":"line-pattern" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"cross-faded" + }, + "line-pattern":{ + "type":"resolvedImage", + "transition":true, + "doc":"Name of image in sprite to use for drawing image lines. For seamless patterns, image width must be a factor of two (2, 4, 8, ..., 512). Note that zoom-dependent expressions will be evaluated only at integer zoom levels.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.49.0", + "android":"6.5.0", + "macos":"0.11.0", + "ios":"4.4.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom", + "feature" + ] + }, + "property-type":"cross-faded-data-driven" + }, + "line-gradient":{ + "type":"color", + "doc":"Defines a gradient with which to color a line feature. Can only be used with GeoJSON sources that specify `\"lineMetrics\": true`.", + "transition":false, + "requires":[ + { + "!":"line-dasharray" + }, + { + "!":"line-pattern" + }, + { + "source":"geojson", + "has":{ + "lineMetrics":true + } + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.45.0", + "android":"6.5.0", + "ios":"4.4.0", + "macos":"0.11.0" + }, + "data-driven styling":{ + + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "line-progress" + ] + }, + "property-type":"color-ramp" + } + }, + "paint_circle":{ + "circle-radius":{ + "type":"number", + "default":5, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Circle radius.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.18.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-color":{ + "type":"color", + "default":"#000000", + "doc":"The fill color of the circle.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.18.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-blur":{ + "type":"number", + "default":0, + "doc":"Amount to blur the circle. 1 blurs the circle such that only the centerpoint is full opacity.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.20.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-opacity":{ + "type":"number", + "doc":"The opacity at which the circle will be drawn.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.20.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"The geometry's offset. Values are [x, y] where negatives indicate left and up, respectively.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "circle-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The circle is translated relative to the map." + }, + "viewport":{ + "doc":"The circle is translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `circle-translate`.", + "default":"map", + "requires":[ + "circle-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "circle-pitch-scale":{ + "type":"enum", + "values":{ + "map":{ + "doc":"Circles are scaled according to their apparent distance to the camera." + }, + "viewport":{ + "doc":"Circles are not scaled." + } + }, + "default":"map", + "doc":"Controls the scaling behavior of the circle when the map is pitched.", + "sdk-support":{ + "basic functionality":{ + "js":"0.21.0", + "android":"4.2.0", + "ios":"3.4.0", + "macos":"0.2.1" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "circle-pitch-alignment":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The circle is aligned to the plane of the map." + }, + "viewport":{ + "doc":"The circle is aligned to the plane of the viewport." + } + }, + "default":"viewport", + "doc":"Orientation of circle when map is pitched.", + "sdk-support":{ + "basic functionality":{ + "js":"0.39.0", + "android":"5.2.0", + "ios":"3.7.0", + "macos":"0.6.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "circle-stroke-width":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"The width of the circle's stroke. Strokes are placed outside of the `circle-radius`.", + "sdk-support":{ + "basic functionality":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-stroke-color":{ + "type":"color", + "default":"#000000", + "doc":"The stroke color of the circle.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "circle-stroke-opacity":{ + "type":"number", + "doc":"The opacity of the circle's stroke.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + }, + "data-driven styling":{ + "js":"0.29.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + } + }, + "paint_heatmap":{ + "heatmap-radius":{ + "type":"number", + "default":30, + "minimum":1, + "transition":true, + "units":"pixels", + "doc":"Radius of influence of one heatmap point in pixels. Increasing the value makes the heatmap smoother, but less detailed.", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "data-driven styling":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "heatmap-weight":{ + "type":"number", + "default":1, + "minimum":0, + "transition":false, + "doc":"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.", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "data-driven styling":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "heatmap-intensity":{ + "type":"number", + "default":1, + "minimum":0, + "transition":true, + "doc":"Similar to `heatmap-weight` but controls the intensity of the heatmap globally. Primarily used for adjusting the heatmap based on zoom level.", + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "heatmap-color":{ + "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" + ], + "doc":"Defines the color of each pixel based on its density value in a heatmap. Should be an expression that uses `[\"heatmap-density\"]` as input.", + "transition":false, + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + }, + "data-driven styling":{ + + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "heatmap-density" + ] + }, + "property-type":"color-ramp" + }, + "heatmap-opacity":{ + "type":"number", + "doc":"The global opacity at which the heatmap layer will be drawn.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.41.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "paint_symbol":{ + "icon-opacity":{ + "doc":"The opacity at which the icon will be drawn.", + "type":"number", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "icon-color":{ + "type":"color", + "default":"#000000", + "transition":true, + "doc":"The color of the icon. This can only be used with sdf icons.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "icon-halo-color":{ + "type":"color", + "default":"rgba(0, 0, 0, 0)", + "transition":true, + "doc":"The color of the icon's halo. Icon halos can only be used with SDF icons.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "icon-halo-width":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Distance of halo to the icon outline.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "icon-halo-blur":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Fade out the halo towards the outside.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "icon-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"Distance that the icon's anchor is moved from its original placement. Positive values indicate right and down, while negative values indicate left and up.", + "requires":[ + "icon-image" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "icon-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"Icons are translated relative to the map." + }, + "viewport":{ + "doc":"Icons are translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `icon-translate`.", + "default":"map", + "requires":[ + "icon-image", + "icon-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-opacity":{ + "type":"number", + "doc":"The opacity at which the text will be drawn.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "text-color":{ + "type":"color", + "doc":"The color with which the text will be drawn.", + "default":"#000000", + "transition":true, + "overridable":true, + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "text-halo-color":{ + "type":"color", + "default":"rgba(0, 0, 0, 0)", + "transition":true, + "doc":"The color of the text's halo, which helps it stand out from backgrounds.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "text-halo-width":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"Distance of halo to the font outline. Max text halo width is 1/4 of the font-size.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "text-halo-blur":{ + "type":"number", + "default":0, + "minimum":0, + "transition":true, + "units":"pixels", + "doc":"The halo's fadeout distance towards the outside.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + "js":"0.33.0", + "android":"5.0.0", + "ios":"3.5.0", + "macos":"0.4.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom", + "feature", + "feature-state" + ] + }, + "property-type":"data-driven" + }, + "text-translate":{ + "type":"array", + "value":"number", + "length":2, + "default":[ + 0, + 0 + ], + "transition":true, + "units":"pixels", + "doc":"Distance that the text's anchor is moved from its original placement. Positive values indicate right and down, while negative values indicate left and up.", + "requires":[ + "text-field" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "text-translate-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The text is translated relative to the map." + }, + "viewport":{ + "doc":"The text is translated relative to the viewport." + } + }, + "doc":"Controls the frame of reference for `text-translate`.", + "default":"map", + "requires":[ + "text-field", + "text-translate" + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "paint_raster":{ + "raster-opacity":{ + "type":"number", + "doc":"The opacity at which the image will be drawn.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-hue-rotate":{ + "type":"number", + "default":0, + "period":360, + "transition":true, + "units":"degrees", + "doc":"Rotates hues around the color wheel.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-brightness-min":{ + "type":"number", + "doc":"Increase or reduce the brightness of the image. The value is the minimum brightness.", + "default":0, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-brightness-max":{ + "type":"number", + "doc":"Increase or reduce the brightness of the image. The value is the maximum brightness.", + "default":1, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-saturation":{ + "type":"number", + "doc":"Increase or reduce the saturation of the image.", + "default":0, + "minimum":-1, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-contrast":{ + "type":"number", + "doc":"Increase or reduce the contrast of the image.", + "default":0, + "minimum":-1, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-resampling":{ + "type":"enum", + "doc":"The resampling/interpolation method to use for overscaling, also known as texture magnification filter", + "values":{ + "linear":{ + "doc":"(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" + }, + "nearest":{ + "doc":"Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" + } + }, + "default":"linear", + "sdk-support":{ + "basic functionality":{ + "js":"0.47.0", + "android":"6.3.0", + "ios":"4.2.0", + "macos":"0.9.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "raster-fade-duration":{ + "type":"number", + "default":300, + "minimum":0, + "transition":false, + "units":"milliseconds", + "doc":"Fade duration when a new tile is added.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "paint_hillshade":{ + "hillshade-illumination-direction":{ + "type":"number", + "default":335, + "minimum":0, + "maximum":359, + "doc":"The direction of the light source used to generate the hillshading with 0 as the top of the viewport if `hillshade-illumination-anchor` is set to `viewport` and due north if `hillshade-illumination-anchor` is set to `map`.", + "transition":false, + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "hillshade-illumination-anchor":{ + "type":"enum", + "values":{ + "map":{ + "doc":"The hillshade illumination is relative to the north direction." + }, + "viewport":{ + "doc":"The hillshade illumination is relative to the top of the viewport." + } + }, + "default":"viewport", + "doc":"Direction of light source when map is rotated.", + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "hillshade-exaggeration":{ + "type":"number", + "doc":"Intensity of the hillshade", + "default":0.5, + "minimum":0, + "maximum":1, + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "hillshade-shadow-color":{ + "type":"color", + "default":"#000000", + "doc":"The shading color of areas that face away from the light source.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "hillshade-highlight-color":{ + "type":"color", + "default":"#FFFFFF", + "doc":"The shading color of areas that faces towards the light source.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "hillshade-accent-color":{ + "type":"color", + "default":"#000000", + "doc":"The shading color used to accentuate rugged terrain like sharp cliffs and gorges.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.43.0", + "android":"6.0.0", + "ios":"4.0.0", + "macos":"0.7.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "paint_background":{ + "background-color":{ + "type":"color", + "default":"#000000", + "doc":"The color with which the background will be drawn.", + "transition":true, + "requires":[ + { + "!":"background-pattern" + } + ], + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + }, + "background-pattern":{ + "type":"resolvedImage", + "transition":true, + "doc":"Name of image in sprite to use for drawing an image background. 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.", + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + }, + "data-driven styling":{ + + } + }, + "expression":{ + "interpolated":false, + "parameters":[ + "zoom" + ] + }, + "property-type":"cross-faded" + }, + "background-opacity":{ + "type":"number", + "default":1, + "minimum":0, + "maximum":1, + "doc":"The opacity at which the background will be drawn.", + "transition":true, + "sdk-support":{ + "basic functionality":{ + "js":"0.10.0", + "android":"2.0.1", + "ios":"2.0.0", + "macos":"0.1.0" + } + }, + "expression":{ + "interpolated":true, + "parameters":[ + "zoom" + ] + }, + "property-type":"data-constant" + } + }, + "transition":{ + "duration":{ + "type":"number", + "default":300, + "minimum":0, + "units":"milliseconds", + "doc":"Time allotted for transitions to complete." + }, + "delay":{ + "type":"number", + "default":0, + "minimum":0, + "units":"milliseconds", + "doc":"Length of time before a transition begins." + } + }, + "property-type":{ + "data-driven":{ + "type":"property-type", + "doc":"Property is interpolable and can be represented using a property expression." + }, + "cross-faded":{ + "type":"property-type", + "doc":"Property is non-interpolable; rather, its values will be cross-faded to smoothly transition between integer zooms." + }, + "cross-faded-data-driven":{ + "type":"property-type", + "doc":"Property is non-interpolable; rather, its values will be cross-faded to smoothly transition between integer zooms. It can be represented using a property expression." + }, + "color-ramp":{ + "type":"property-type", + "doc":"Property should be specified using a color ramp from which the output color can be sampled based on a property calculation." + }, + "data-constant":{ + "type":"property-type", + "doc":"Property is interpolable but cannot be represented using a property expression." + }, + "constant":{ + "type":"property-type", + "doc":"Property is constant across all zoom levels and property values." + } + }, + "promoteId":{ + "*":{ + "type":"string", + "doc":"A name of a feature property to use as ID for feature state." + } + } +} \ No newline at end of file diff --git a/scripts/lib/conversions.dart b/scripts/lib/conversions.dart new file mode 100644 index 000000000..016f8f86a --- /dev/null +++ b/scripts/lib/conversions.dart @@ -0,0 +1,61 @@ +const renamedIosProperties = { + "iconImage": "iconImageName", + "iconRotate": "iconRotation", + "iconSize": "iconScale", + "iconKeepUpright": "keepsIconUpright", + "iconTranslate": "iconTranslation", + "iconTranslateAnchor": "iconTranslationAnchor", + "iconAllowOverlap": "iconAllowsOverlap", + "iconIgnorePlacement": "iconIgnoresPlacement", + "textTranslate": "textTranslation", + "textTranslateAnchor": "textTranslationAnchor", + "textIgnorePlacement": "textIgnoresPlacement", + "textField": "text", + "textFont": "textFontNames", + "textSize": "textFontSize", + "textMaxWidth": "maximumTextWidth", + "textJustify": "textJustification", + "textMaxAngle": "maximumTextAngle", + "textWritingMode": "textWritingModes", + "textRotate": "textRotation", + "textKeepUpright": "keepsTextUpright", + "textAllowOverlap": "textAllowsOverlap", + "symbolAvoidEdges": "symbolAvoidsEdges", + "circleTranslate": "circleTranslation", + "circleTranslateAnchor": "circleTranslationAnchor", + "circlePitchScale": "circleScaleAlignment", + "lineTranslate": "lineTranslation", + "lineTranslateAnchor": "lineTranslationAnchor", + "lineDasharray": "lineDashPattern", + "fillAntialias": "fillAntialiased", + "fillTranslate": "fillTranslation", + "fillTranslateAnchor": "fillTranslationAnchor", + "rasterHueRotate": "rasterHueRotation", + "rasterResampling": "rasterResamplingMode", + "visibility": "isVisible", + "rasterBrightnessMin": "minimumRasterBrightness", + "rasterBrightnessMax": "maximumRasterBrightness", + "fillExtrusionTranslate": "fillExtrusionTranslation", + "fillExtrusionTranslateAnchor": "fillExtrusionTranslationAnchor", + "fillExtrusionVerticalGradient": "fillExtrusionHasVerticalGradient", +}; + +const dartTypeMappingTable = { + "string": "String", + "array": "List", + "number": "double", + "enum": "String", + "promoteId": "String", + "boolean": "bool", + "*": "Object" +}; + +const swiftTypeMappingTable = { + "string": "String", + "array": "Array", + "number": "Double", + "enum": "String", + "promoteId": "String", + "boolean": "Bool", + "*": "Object" +}; diff --git a/scripts/lib/generate.dart b/scripts/lib/generate.dart new file mode 100644 index 000000000..3ee9fed6b --- /dev/null +++ b/scripts/lib/generate.dart @@ -0,0 +1,246 @@ +import 'dart:io'; +import 'dart:convert'; + +import 'package:mustache_template/mustache_template.dart'; +import 'package:recase/recase.dart'; + +import 'conversions.dart'; + +main() async { + var styleJson = + jsonDecode(await new File('scripts/input/style.json').readAsString()); + + final layerTypes = [ + "symbol", + "circle", + "line", + "fill", + "fill-extrusion", + "raster", + "hillshade", + "heatmap" + ]; + final sourceTypes = [ + "vector", + "raster", + "raster_dem", + "geojson", + "video", + "image" + ]; + + final renderContext = { + "layerTypes": [ + for (var type in layerTypes) + { + "type": type, + "typePascal": ReCase(type).pascalCase, + "typeCamel": ReCase(type).camelCase, + "paint_properties": buildStyleProperties(styleJson, "paint_$type"), + "layout_properties": buildStyleProperties(styleJson, "layout_$type"), + }, + ], + "sourceTypes": [ + for (var type in sourceTypes) + { + "type": type.replaceAll("_", "-"), + "typePascal": ReCase(type).pascalCase, + "typeCamel": ReCase(type).camelCase, + "properties": buildSourceProperties(styleJson, "source_$type"), + }, + ], + 'expressions': buildExpressionProperties(styleJson) + }; + + // required for deduplication + renderContext["all_layout_properties"] = [ + for (final type in renderContext["layerTypes"]!) + ...type["layout_properties"].map((p) => p["value"]).toList() + ].toSet().map((p) => {"property": p}).toList(); + + final templates = [ + "android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java", + "ios/Classes/LayerPropertyConverter.swift", + "lib/src/layer_expressions.dart", + "lib/src/layer_properties.dart", + "mapbox_gl_web/lib/src/layer_tools.dart", + "mapbox_gl_platform_interface/lib/src/source_properties.dart", + ]; + + for (var template in templates) await render(renderContext, template); +} + +Future render( + Map renderContext, + String path, +) async { + final pathItems = path.split("/"); + final filename = pathItems.removeLast(); + final outputPath = pathItems.join("/"); + + print("Rendering $filename"); + var templateFile = + await File('scripts/templates/$filename.template').readAsString(); + + var template = Template(templateFile); + var outputFile = File('$outputPath/$filename'); + + outputFile.writeAsString(template.renderString(renderContext)); +} + +List> buildStyleProperties( + Map styleJson, String key) { + final Map items = styleJson[key]; + + return items.entries.map((e) => buildStyleProperty(e.key, e.value)).toList(); +} + +Map buildStyleProperty( + String key, Map value) { + final camelCase = ReCase(key).camelCase; + return { + 'value': key, + 'isVisibilityProperty': key == "visibility", + 'requiresLiteral': key == "icon-image", + 'isIosAsCamelCase': renamedIosProperties.containsKey(camelCase), + 'iosAsCamelCase': renamedIosProperties[camelCase], + 'doc': value["doc"], + 'docSplit': buildDocSplit(value).map((s) => {"part": s}).toList(), + 'valueAsCamelCase': camelCase + }; +} + +List> buildSourceProperties( + Map styleJson, String key) { + final Map items = styleJson[key]; + + return items.entries + .where((e) => e.key != "*" && e.key != "type") + .map((e) => buildSourceProperty(e.key, e.value)) + .toList(); +} + +Map buildSourceProperty( + String key, Map value) { + final camelCase = ReCase(key).camelCase; + final typeDart = dartTypeMappingTable[value["type"]]; + final typeSwift = swiftTypeMappingTable[value["type"]]; + final nestedTypeDart = dartTypeMappingTable[value["value"]] ?? + dartTypeMappingTable[value["value"]?["type"]]; + final nestedTypeSwift = swiftTypeMappingTable[value["value"]] ?? + swiftTypeMappingTable[value["value"]?["type"]]; + + var defaultValue = value["default"]; + if (defaultValue is List) { + defaultValue = "const" + defaultValue.toString(); + } else if (defaultValue is String) { + defaultValue = '"$defaultValue"'; + } + + return { + 'value': key, + 'doc': value["doc"], + 'default': defaultValue, + 'hasDefault': value["default"] != null, + 'type': nestedTypeDart == null ? typeDart : "$typeDart<$nestedTypeDart>", + 'typeSwift': + nestedTypeSwift == null ? typeSwift : "$typeSwift<$nestedTypeSwift>", + 'docSplit': buildDocSplit(value).map((s) => {"part": s}).toList(), + 'valueAsCamelCase': camelCase + }; +} + +List buildDocSplit(Map item) { + final defaultValue = item["default"]; + final maxValue = item["maximum"]; + final minValue = item["minimum"]; + final type = item["type"]; + final Map? sdkSupport = item["sdk-support"]; + + final Map? values = item["values"]; + final result = splitIntoChunks(item["doc"]!, 70); + if (type != null) { + result.add(""); + result.add("Type: $type"); + if (defaultValue != null) result.add(" default: $defaultValue"); + if (minValue != null) result.add(" minimum: $minValue"); + if (maxValue != null) result.add(" maximum: $maxValue"); + if (values != null) { + result.add("Options:"); + for (var value in values.entries) { + result.add(" \"${value.key}\""); + result.addAll( + splitIntoChunks("${value.value["doc"]}", 70, prefix: " ")); + } + } + } + if (sdkSupport != null) { + final Map? basic = sdkSupport["basic functionality"]; + final Map? dataDriven = sdkSupport["data-driven styling"]; + + result.add(""); + result.add("Sdk Support:"); + if (basic != null && basic.isNotEmpty) { + result.add(" basic functionality with " + basic.keys.join(", ")); + } + if (dataDriven != null && dataDriven.isNotEmpty) { + result.add(" data-driven styling with " + dataDriven.keys.join(", ")); + } + } + + return result; +} + +List splitIntoChunks(String input, int lineLength, + {String prefix = ""}) { + final words = input.split(" "); + final chunks = []; + + String chunk = ""; + for (var word in words) { + final nextChunk = chunk.length == 0 ? prefix + word : chunk + " " + word; + if (nextChunk.length > lineLength || chunk.endsWith("\n")) { + chunks.add(chunk.replaceAll("\n", "")); + chunk = prefix + word; + } else { + chunk = nextChunk; + } + } + chunks.add(chunk); + + return chunks; +} + +List> buildExpressionProperties( + Map styleJson) { + final Map items = styleJson["expression_name"]["values"]; + + final renamed = { + "var": "varExpression", + "in": "inExpression", + "case": "caseExpression", + "to-string": "toStringExpression", + "+": "plus", + "*": "multiply", + "-": "minus", + "%": "precent", + ">": "larger", + ">=": "largerOrEqual", + "<": "smaller", + "<=": "smallerOrEqual", + "!=": "notEqual", + "==": "equal", + "/": "divide", + "^": "xor", + "!": "not", + }; + + return items.entries + .map((e) => { + 'value': e.key, + 'doc': e.value["doc"], + 'docSplit': buildDocSplit(e.value).map((s) => {"part": s}).toList(), + 'valueAsCamelCase': new ReCase(renamed[e.key] ?? e.key).camelCase + }) + .toList(); +} diff --git a/scripts/pubspec.lock b/scripts/pubspec.lock new file mode 100644 index 000000000..fed710049 --- /dev/null +++ b/scripts/pubspec.lock @@ -0,0 +1,19 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + mustache_template: + dependency: "direct main" + description: + name: mustache_template + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + recase: + dependency: "direct main" + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" +sdks: + dart: ">=2.12.0 <3.0.0" diff --git a/scripts/pubspec.yaml b/scripts/pubspec.yaml new file mode 100644 index 000000000..f25e07ce1 --- /dev/null +++ b/scripts/pubspec.yaml @@ -0,0 +1,11 @@ +name: mapbox_code_gen +description: code generation for flutter-mapbox-gl + +version: 0.0.1 + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + mustache_template: ^2.0.0 + recase: ^4.0.0 diff --git a/scripts/templates/LayerPropertyConverter.java.template b/scripts/templates/LayerPropertyConverter.java.template new file mode 100644 index 000000000..25c56680e --- /dev/null +++ b/scripts/templates/LayerPropertyConverter.java.template @@ -0,0 +1,67 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +package com.mapbox.mapboxgl; + +import com.mapbox.mapboxsdk.style.expressions.Expression; +import com.mapbox.mapboxsdk.style.layers.PropertyFactory; +import com.mapbox.mapboxsdk.style.layers.PropertyValue; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + + +import static com.mapbox.mapboxgl.Convert.toMap; + +class LayerPropertyConverter { +{{#layerTypes}} + static PropertyValue[] interpret{{typePascal}}LayerProperties(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()) { + {{#paint_properties}} + case "{{value}}": + properties.add(PropertyFactory.{{valueAsCamelCase}}(expression)); + break; + {{/paint_properties}} + {{#layout_properties}} + {{^isVisibilityProperty}} + {{^requiresLiteral}} + case "{{value}}": + properties.add(PropertyFactory.{{valueAsCamelCase}}(expression)); + {{/requiresLiteral}} + {{/isVisibilityProperty}} + {{#requiresLiteral}} + case "{{value}}": + if(jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()){ + properties.add(PropertyFactory.iconImage(jsonElement.getAsString())); + }else{ + properties.add(PropertyFactory.iconImage(expression)); + } + {{/requiresLiteral}} + {{#isVisibilityProperty}} + case "{{value}}": + properties.add(PropertyFactory.{{valueAsCamelCase}}(entry.getValue())); + {{/isVisibilityProperty}} + break; + {{/layout_properties}} + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + +{{/layerTypes}} +} \ No newline at end of file diff --git a/scripts/templates/LayerPropertyConverter.swift.template b/scripts/templates/LayerPropertyConverter.swift.template new file mode 100644 index 000000000..a06903db4 --- /dev/null +++ b/scripts/templates/LayerPropertyConverter.swift.template @@ -0,0 +1,77 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +import Mapbox +import MapboxAnnotationExtension + +class LayerPropertyConverter { +{{#layerTypes}} + 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}} + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression; + {{/isIosAsCamelCase}} + {{^isIosAsCamelCase}} + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression; + {{/isIosAsCamelCase}} + break; + {{/paint_properties}} + {{#layout_properties}} + case "{{{value}}}": + {{^isVisibilityProperty}} + {{#isIosAsCamelCase}} + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression; + {{/isIosAsCamelCase}} + {{^isIosAsCamelCase}} + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression; + {{/isIosAsCamelCase}} + {{/isVisibilityProperty}} + {{#isVisibilityProperty}} + {{typeCamel}}Layer.{{iosAsCamelCase}} = propertyValue == "visible"; + {{/isVisibilityProperty}} + break; + {{/layout_properties}} + + default: + break + } + } + } + +{{/layerTypes}} + private class func interpretExpression(propertyName: String, expression: String) -> NSExpression? { + let isColor = propertyName.contains("color"); + + do { + let json = try JSONSerialization.jsonObject(with: expression.data(using: .utf8)!, options: .fragmentsAllowed) + // this is required because NSExpression.init(mglJSONObject: json) fails to create + // a proper Expression if the data of is a hexString + if isColor { + if let color = json as? String { + return NSExpression(forConstantValue: UIColor(hexString: color)) + } + } + // this is required because NSExpression.init(mglJSONObject: json) fails to create + // a proper Expression if the data of a literal is an array + if let offset = json as? [Any]{ + if offset.count == 2 && offset.first is String && offset.first as? String == "literal" { + if let vector = offset.last as? [Any]{ + if(vector.count == 2) { + if let x = vector.first as? Double, let y = vector.last as? Double { + return NSExpression(forConstantValue: NSValue(cgVector: CGVector(dx: x, dy: y))) + } + + } + } + } + } + return NSExpression.init(mglJSONObject: json) + } catch { + } + return nil + } +} diff --git a/scripts/templates/layer_expressions.dart.template b/scripts/templates/layer_expressions.dart.template new file mode 100644 index 000000000..5f02944a8 --- /dev/null +++ b/scripts/templates/layer_expressions.dart.template @@ -0,0 +1,14 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl; + +class Expressions{ + {{#expressions}} + {{#docSplit}} + /// {{{part}}} + {{/docSplit}} + static const {{valueAsCamelCase}} = "{{{value}}}"; + + {{/expressions}} +} diff --git a/scripts/templates/layer_properties.dart.template b/scripts/templates/layer_properties.dart.template new file mode 100644 index 000000000..1ae6e1b30 --- /dev/null +++ b/scripts/templates/layer_properties.dart.template @@ -0,0 +1,80 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl; + +abstract class LayerProperties { + Map toJson(); +} +{{#layerTypes}} + +class {{typePascal}}LayerProperties implements LayerProperties { + // Paint Properties + {{#paint_properties}} + {{#docSplit}} + /// {{{part}}} + {{/docSplit}} + final dynamic {{valueAsCamelCase}}; + + {{/paint_properties}} + // Layout Properties + {{#layout_properties}} + {{#docSplit}} + /// {{{part}}} + {{/docSplit}} + final dynamic {{valueAsCamelCase}}; + + {{/layout_properties}} + const {{typePascal}}LayerProperties({ + {{#paint_properties}} + this.{{valueAsCamelCase}}, + {{/paint_properties}} + {{#layout_properties}} + this.{{valueAsCamelCase}}, + {{/layout_properties}} + }); + + {{typePascal}}LayerProperties copyWith({{typePascal}}LayerProperties changes) { + return {{typePascal}}LayerProperties( + {{#paint_properties}} + {{valueAsCamelCase}}: changes.{{valueAsCamelCase}} ?? {{valueAsCamelCase}}, + {{/paint_properties}} + {{#layout_properties}} + {{valueAsCamelCase}}: changes.{{valueAsCamelCase}} ?? {{valueAsCamelCase}}, + {{/layout_properties}} + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + {{#paint_properties}} + addIfPresent('{{value}}', {{valueAsCamelCase}}); + {{/paint_properties}} + {{#layout_properties}} + addIfPresent('{{value}}', {{valueAsCamelCase}}); + {{/layout_properties}} + return json; + } + + factory {{typePascal}}LayerProperties.fromJson(Map json) { + return {{typePascal}}LayerProperties( + {{#paint_properties}} + {{valueAsCamelCase}}: json['{{value}}'], + {{/paint_properties}} + {{#layout_properties}} + {{valueAsCamelCase}}: json['{{value}}'], + {{/layout_properties}} + ); + } + +} +{{/layerTypes}} + + diff --git a/scripts/templates/layer_tools.dart.template b/scripts/templates/layer_tools.dart.template new file mode 100644 index 000000000..b5e28b1ab --- /dev/null +++ b/scripts/templates/layer_tools.dart.template @@ -0,0 +1,12 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +const _layoutProperties = { +{{#all_layout_properties}} + "{{{property}}}", +{{/all_layout_properties}} +}; + +bool isLayoutProperty(String property){ + return _layoutProperties.contains(property); +} \ No newline at end of file diff --git a/scripts/templates/source_properties.dart.template b/scripts/templates/source_properties.dart.template new file mode 100644 index 000000000..6ca7389ae --- /dev/null +++ b/scripts/templates/source_properties.dart.template @@ -0,0 +1,68 @@ +// This file is generated by +// ./scripts/lib/generate.dart + +part of mapbox_gl_platform_interface; + +abstract class SourceProperties { + Map toJson(); +} +{{#sourceTypes}} + +class {{typePascal}}SourceProperties implements SourceProperties { + {{#properties}} + {{#docSplit}} + /// {{{part}}} + {{/docSplit}} + final {{{type}}}? {{valueAsCamelCase}}; + + {{/properties}} + const {{typePascal}}SourceProperties({ + {{#properties}} + {{^hasDefault}} + this.{{valueAsCamelCase}}, + {{/hasDefault}} + {{#hasDefault}} + this.{{valueAsCamelCase}} = {{{default}}}, + {{/hasDefault}} + {{/properties}} + }); + + {{typePascal}}SourceProperties copyWith( + {{#properties}} + {{{type}}}? {{valueAsCamelCase}}, + {{/properties}} + ) { + return {{typePascal}}SourceProperties( + {{#properties}} + {{valueAsCamelCase}}: {{valueAsCamelCase}} ?? this.{{valueAsCamelCase}}, + {{/properties}} + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + json["type"] = "{{type}}"; + {{#properties}} + addIfPresent('{{value}}', {{valueAsCamelCase}}); + {{/properties}} + return json; + } + + factory {{typePascal}}SourceProperties.fromJson(Map json) { + return {{typePascal}}SourceProperties( + {{#properties}} + {{valueAsCamelCase}}: json['{{value}}'], + {{/properties}} + ); + } + +} +{{/sourceTypes}} + +