From 34769d938006ddc73d79ababa306baf89f253a27 Mon Sep 17 00:00:00 2001 From: IcyTv Date: Sun, 15 Nov 2020 17:43:44 +0100 Subject: [PATCH] Merged (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose CompassViewPosition #344 (#346) Co-authored-by: emre.yalcin * Readme: location features on Android (#342) * Update CONTRIBUTING.md (#366) * Release 0.8.0 (#390) * [release] update CHANGELOG.md for release of v0.8.0 * [release] update version numbers to v0.8.0 * Update CONTRIBUTING.md (#401) Fix stale pull, issues & changelog urls in the contributing guide. * Fix data parameter for addLine and addCircle (#388) * Split padding values in CameraUpdate.newLatLngBounds() (#382) * Split padding values in CameraUpdate.newLatLngBounds() * Remove old unused code Co-authored-by: Tobrun * Re-enable attribution on android (#383) * Upgrade Android annotation plugin to v0.9 (#381) * web: ignore myLocationTrackingMode if myLocationEnabled is false (#363) * Add methods to access projection (#380) * remove bitmap; add projection access * Replace ScreenLocation with Point; expand iOS implementation * fix iOS with guard let * iOS: cast to NSObject * fix typo * round result of toScreenLocation() * Revert "round result of toScreenLocation()" This reverts commit 838726aa1a277ededcf4bd3fd41187bbc79210a6. * Docs: document rounding behaviour * Add Fill API support (#49) * [flutter] [android] - add fill support * Resolved merge conflict. * A first working version for ios (after some extensive rebasing). * Minor cleanup * Minor cleanup. * Fix broken build Android. * A working version for Android. * Minor cleanup. * Added fill pattern example. Works on Android not on iOS. Seems to break consecutive fills though. * For the first queried feature (when filter is set) create a fill. * Fix lint issue (unused method). * Updated code formatting. * Added interior polygon to iOS. * [docs] update readme support table * fixup Co-authored-by: Timothy Sealy * Listen to OnUserLocationUpdated to provide user location to app (#237) * Listen to OnUserLocationUpdated to provide user location to app While the `myLocationEnabled` property is set to `true`, this method is called whenever a new location update is received by the map view. iOS only, needs Android. I did check that the location properties carried here are also provided in Android's [Location][1] object. [1]: https://developer.android.com/reference/android/location/Location * add android, web; fix conflicts Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com> Co-authored-by: Tobrun * fix: correct bug on android where checking on activity lifecycles that were disposed (#266) Co-authored-by: leo cornillon Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com> * Add support for custom font stack in symbol options (#359) * fix memory leak caused by strong self reference (#370) * Basic ImageSource Support (#409) * Introduce LatLngQuad Introduce the LatLngQuad object which will be useful to pass in all the required parameters to the addSource() method we will define later. * Introduce addSource() Method Add the addSource(..) method to the mapbox_gl_platform_interface.dart * Add addSource MethodChannel * Place ImageSource Android - Introduce the PlaceSource page as a playground - Add 'addImageSource', 'removeImageSource', 'addLayer' & 'removeLayer' apis - Implement Android platform interface * iOS ImageSource Implementation Implement addImageSource, removeImageSource, addLayer & removeLayer on iOS. * Fix iOS CoordinateQuad Mapping Co-authored-by: Tobrun * Get meters per pixel at latitude (#416) * fix git refferences * fix git refferences * implementation of getMetersPerPixelAtLatitude * getMetersPerPixelAtLatitude * fix refference paths * Android implementation and Example updated. * added comments to getMetersPerPixelAtLatitude method * IOS implementation * Removed modified lines from pubspec.yaml files * web implementation Co-authored-by: Tobrun * fix onStyleLoadedCallback (#418) * fix onStyleLoadedCallback * fix onStyleLoadedCallback called before onMapCreated Co-authored-by: Tobrun * Release 0.9.0 * update version numbers for v0.9.0 release * fix ios onSymbolTapped Co-authored-by: emreuguryalcintr <50848628+emreuguryalcintr@users.noreply.github.com> Co-authored-by: emre.yalcin Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com> Co-authored-by: Tobrun Co-authored-by: cuberob Co-authored-by: qbouchat <48958612+qbouchat@users.noreply.github.com> Co-authored-by: Timothy Sealy Co-authored-by: Nathan Co-authored-by: Leo Cornillon Co-authored-by: leo cornillon Co-authored-by: Philip Lindberg Co-authored-by: Thomas Kröniger <30893720+thirteenthstep@users.noreply.github.com> Co-authored-by: cuberob Co-authored-by: GULERTOLGA Co-authored-by: Andrea Valenzano --- CHANGELOG.md | 39 ++- CONTRIBUTING.md | 11 +- README.md | 7 +- android/build.gradle | 2 +- .../java/com/mapbox/mapboxgl/Convert.java | 68 ++++- .../java/com/mapbox/mapboxgl/FillBuilder.java | 58 ++++ .../com/mapbox/mapboxgl/FillController.java | 74 +++++ .../com/mapbox/mapboxgl/FillOptionsSink.java | 27 ++ .../com/mapbox/mapboxgl/MapboxMapBuilder.java | 2 +- .../mapbox/mapboxgl/MapboxMapController.java | 214 ++++++++++++++- .../mapbox/mapboxgl/OnFillTappedListener.java | 7 + .../com/mapbox/mapboxgl/SymbolBuilder.java | 3 + .../com/mapbox/mapboxgl/SymbolController.java | 3 + .../mapbox/mapboxgl/SymbolOptionsSink.java | 2 + .../assets/fill/cat_silhouette_pattern.png | Bin 0 -> 972 bytes example/assets/sydney.png | Bin 0 -> 58396 bytes example/ios/Podfile | 83 +++--- example/lib/animate_camera.dart | 4 +- example/lib/main.dart | 4 + example/lib/map_ui.dart | 61 ++++- example/lib/move_camera.dart | 4 +- example/lib/place_fill.dart | 252 ++++++++++++++++++ example/lib/place_source.dart | 132 +++++++++ example/lib/place_symbol.dart | 31 ++- example/pubspec.yaml | 3 + ios/Classes/Convert.swift | 42 ++- ios/Classes/Extensions.swift | 20 ++ ios/Classes/MapboxMapController.swift | 130 ++++++++- lib/mapbox_gl.dart | 8 +- lib/src/bitmap.dart | 49 ---- lib/src/controller.dart | 167 ++++++++++-- lib/src/mapbox_map.dart | 33 ++- mapbox_gl_platform_interface/CHANGELOG.md | 17 +- .../lib/mapbox_gl_platform_interface.dart | 2 + .../lib/src/camera.dart | 14 +- .../lib/src/fill.dart | 89 +++++++ .../lib/src/location.dart | 86 ++++++ .../lib/src/mapbox_gl_platform_interface.dart | 49 ++++ .../lib/src/method_channel_mapbox_gl.dart | 160 ++++++++++- .../lib/src/symbol.dart | 4 + mapbox_gl_platform_interface/pubspec.yaml | 4 +- mapbox_gl_web/CHANGELOG.md | 16 +- mapbox_gl_web/lib/mapbox_gl_web.dart | 1 + mapbox_gl_web/lib/src/convert.dart | 14 +- .../lib/src/mapbox_map_controller.dart | 25 ++ mapbox_gl_web/pubspec.yaml | 4 +- pubspec.lock | 20 +- pubspec.yaml | 4 +- 48 files changed, 1843 insertions(+), 206 deletions(-) create mode 100644 android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java create mode 100644 android/src/main/java/com/mapbox/mapboxgl/FillController.java create mode 100644 android/src/main/java/com/mapbox/mapboxgl/FillOptionsSink.java create mode 100644 android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java create mode 100644 example/assets/fill/cat_silhouette_pattern.png create mode 100644 example/assets/sydney.png create mode 100644 example/lib/place_fill.dart create mode 100644 example/lib/place_source.dart delete mode 100644 lib/src/bitmap.dart create mode 100644 mapbox_gl_platform_interface/lib/src/fill.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index df882e874..ce9e1cd44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## 0.9.0, October 24. 2020 +* Fix data parameter for addLine and addCircle [#388](https://github.com/tobrun/flutter-mapbox-gl/pull/388) +* Re-enable attribution on Android [#383](https://github.com/tobrun/flutter-mapbox-gl/pull/383) +* Upgrade annotation plugin to v0.9 [#381](https://github.com/tobrun/flutter-mapbox-gl/pull/381) +* Breaking change: CameraUpdate.newLatLngBounds() now supports setting different padding values for left, top, right, bottom with default of 0 for all. Implementations using the old approach with only one padding value for all edges have to be updated. [#382](https://github.com/tobrun/flutter-mapbox-gl/pull/382) +* web:ignore myLocationTrackingMode if myLocationEnabled is false [#363](https://github.com/tobrun/flutter-mapbox-gl/pull/363) +* Add methods to access projection [#380](https://github.com/tobrun/flutter-mapbox-gl/pull/380) +* Add fill API support for Android and iOS [#49](https://github.com/tobrun/flutter-mapbox-gl/pull/49) +* Listen to OnUserLocationUpdated to provide user location to app [#237](https://github.com/tobrun/flutter-mapbox-gl/pull/237) +* Correct integration in Activity lifecycle on Android [#266](https://github.com/tobrun/flutter-mapbox-gl/pull/266) +* Add support for custom font stackn in symbol options [#359](https://github.com/tobrun/flutter-mapbox-gl/pull/359) +* Fix memory leak on iOS caused by strong self reference [#370](https://github.com/tobrun/flutter-mapbox-gl/pull/370) +* Basic ImageSource Support [#409](https://github.com/tobrun/flutter-mapbox-gl/pull/409) +* Get meters per pixel at latitude [#416](https://github.com/tobrun/flutter-mapbox-gl/pull/416) +* Fix onStyleLoadedCallback [#418](https://github.com/tobrun/flutter-mapbox-gl/pull/418) + +## 0.8.0, August 22, 2020 +- implementation of feature querying [#177](https://github.com/tobrun/flutter-mapbox-gl/pull/177) +- Batch create/delete of symbols [#279](https://github.com/tobrun/flutter-mapbox-gl/pull/279) +- Add multi map support [#315](https://github.com/tobrun/flutter-mapbox-gl/pull/315) +- Fix OnCameraIdle not being invoked [#313](https://github.com/tobrun/flutter-mapbox-gl/pull/313) +- Fix android zIndex symbol option [#312](https://github.com/tobrun/flutter-mapbox-gl/pull/312) +- Set dependencies from git [#319](https://github.com/tobrun/flutter-mapbox-gl/pull/319) +- Add line#getGeometry and symbol#getGeometry [#281](https://github.com/tobrun/flutter-mapbox-gl/pull/281) + ## 0.7.0, June 6, 2020 * Introduction of mapbox_gl_platform_interface library * Introduction of mapbox_gl_web library @@ -8,12 +33,12 @@ * Update mapbox depdendency to 9.2.0 (android) and 5.6.0 (iOS) * Long press handlers for both iOS as Android * Change default location tracking to none -* OnCameraIdle listener support -* Add image to style +* OnCameraIdle listener support +* Add image to style * Add animation duration to animateCamera * Content insets * Visible region support on iOS -* Numerous bug fixes +* Numerous bug fixes ## 0.0.5, December 21, 2019 * iOS support for annotation extensions (circle, symbol, line) @@ -25,13 +50,13 @@ * Last location API (Android) * Throttle max FPS of user location component (Android) * Fix for handling permission handling of the test application (Android) -* Support for loading symbol images from assets (iOS/Android) +* Support for loading symbol images from assets (iOS/Android) ## v0.0.4, Nov 2, 2019 * Update SDK to 8.4.0 (Android) and 5.4.0 (iOS) * Add support for sideloading offline maps (Android/iOS) * Add user tracking mode (iOS) -* Invert compassView.isHidden logic (iOS) +* Invert compassView.isHidden logic (iOS) * Specific swift version (iOS) ## v0.0.3, Mar 30, 2019 @@ -40,7 +65,7 @@ * Update codebase to AndroidX * Update Mapbox Maps SDK for Android to v7.3.0 -## v0.0.2, Mar 23, 2019 +## v0.0.2, Mar 23, 2019 * Support for iOS * Migration to embedded Android and iOS SDK View system * Style URL API @@ -51,5 +76,5 @@ * Location component (Android) * Camera API (Android) -## v0.0.1, May 7, 2018 +## v0.0.1, May 7, 2018 * Initial Android surface rendering POC diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a94abbd7..a160f0ad8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,15 +2,14 @@ We welcome contributions to this repository. Please follow these steps if you're interested in making contributions: -1. Please familiarize yourself with the [install process](https://github.com/mapbox/flutter-mapbox-gl#getting-started). +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. -2. Ensure that existing [pull requests](https://github.com/mapbox/flutter-mapbox-gl/pulls) and [issues](https://github.com/mapbox/flutter-mapbox-gl/issues) don’t already cover your contribution or question. +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. -3. Create a new branch that will contain your contributed code and eventually create a pull request once you're done making changes. - -4. If there are any changes that developers should be aware of, please update the [changelog](https://github.com/mapbox/flutter-mapbox-gl/blob/master/CHANGELOG.md) once your pull request has been merged to the `master` branch. +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. # 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/). \ No newline at end of file +You can learn more about our open source philosophy on [mapbox.com](https://www.mapbox.com/about/open/). diff --git a/README.md b/README.md index c9a65e303..570ea221f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This project is available on [pub.dev](https://pub.dev/packages/mapbox_gl), foll | 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 | | | | +| Fill | :white_check_mark: | :white_check_mark: | | ## Map Styles @@ -109,8 +109,11 @@ An offline region is a defined region of a map that is available for use in cond ## Location features +To enable location features in an **Android** application: -To enable location features in an iOS application: +You need to declare the `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` permission in the AndroidManifest.xml and starting from Android API level 23 also request it at runtime. The plugin does not handle this for you. The example app uses the flutter ['location' plugin](https://pub.dev/packages/location) for this. + +To enable location features in an **iOS** application: If you access your users' location, you should also add the following key to your Info.plist to explain why you need access to their location data: diff --git a/android/build.gradle b/android/build.gradle index e51e363ee..256fd342b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { } dependencies { implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.0" - implementation "com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9:0.8.0" + 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-v8:0.6.0" } diff --git a/android/src/main/java/com/mapbox/mapboxgl/Convert.java b/android/src/main/java/com/mapbox/mapboxgl/Convert.java index c42b1fecc..77046fca2 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/Convert.java +++ b/android/src/main/java/com/mapbox/mapboxgl/Convert.java @@ -5,7 +5,7 @@ package com.mapbox.mapboxgl; import android.graphics.Point; - +import com.mapbox.geojson.Polygon; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdate; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; @@ -75,8 +75,8 @@ 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)); + 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": @@ -100,7 +100,7 @@ static CameraUpdate toCameraUpdate(Object o, MapboxMap mapboxMap, float density) case "bearingTo": return CameraUpdateFactory.bearingTo(toFloat(data.get(1))); case "tiltTo": - return CameraUpdateFactory.tiltTo(toFloat(data.get(1))); + return CameraUpdateFactory.tiltTo(toFloat(data.get(1))); default: throw new IllegalArgumentException("Cannot interpret " + o + " as CameraUpdate"); } @@ -155,7 +155,7 @@ private static LatLngBounds toLatLngBounds(Object o) { return builder.build(); } - private static List toLatLngList(Object o) { + static List toLatLngList(Object o) { if (o == null) { return null; } @@ -168,6 +168,31 @@ private static List toLatLngList(Object o) { return latLngList; } + private static List> toLatLngListList(Object o) { + if (o == null) { + return null; + } + final List data = toList(o); + List> latLngListList = new ArrayList<>(); + for (int i = 0; i < data.size(); i++) { + List latLngList = toLatLngList(data.get(i)); + latLngListList.add(latLngList); + } + return latLngListList; + } + + static Polygon interpretListLatLng(List> geometry) { + List> points = new ArrayList<>(geometry.size()); + 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())); + } + points.add(innerPoints); + } + return Polygon.fromLngLats(points); + } + private static List toList(Object o) { return (List) o; } @@ -294,6 +319,10 @@ static void interpretSymbolOptions(Object o, SymbolOptionsSink sink) { 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)); @@ -423,7 +452,6 @@ static void interpretCircleOptions(Object o, CircleOptionsSink sink) { sink.setDraggable(toBoolean(draggable)); } } - static void interpretLineOptions(Object o, LineOptionsSink sink) { final Map data = toMap(o); final Object lineJoin = data.get("lineJoin"); @@ -477,4 +505,32 @@ static void interpretLineOptions(Object o, LineOptionsSink sink) { 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)); + } + } } \ 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 new file mode 100644 index 000000000..09adcd90a --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java @@ -0,0 +1,58 @@ +// 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(); + } + + 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 new file mode 100644 index 000000000..200b13f76 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/FillController.java @@ -0,0 +1,74 @@ +// 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; + } + + 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 new file mode 100644 index 000000000..849788103 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/FillOptionsSink.java @@ -0,0 +1,27 @@ +// 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/MapboxMapBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java index f33451602..f41a00d95 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java @@ -22,7 +22,7 @@ class MapboxMapBuilder implements MapboxMapOptionsSink { public final String TAG = getClass().getSimpleName(); private final MapboxMapOptions options = new MapboxMapOptions() .textureMode(true) - .attributionEnabled(false); + .attributionEnabled(true); private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private int myLocationTrackingMode = 0; diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java index 16d53cb6b..4b327539a 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java @@ -16,6 +16,7 @@ import android.graphics.PointF; import android.graphics.RectF; import android.location.Location; +import android.os.Build; import android.os.Bundle; import android.util.DisplayMetrics; import androidx.annotation.NonNull; @@ -40,6 +41,7 @@ import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.geometry.LatLngQuad; import com.mapbox.mapboxsdk.geometry.VisibleRegion; import com.mapbox.mapboxsdk.location.LocationComponent; import com.mapbox.mapboxsdk.location.LocationComponentOptions; @@ -52,10 +54,13 @@ import com.mapbox.mapboxsdk.maps.MapboxMapOptions; import com.mapbox.mapboxsdk.maps.Projection; import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.mapboxsdk.maps.Style; 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.Fill; +import com.mapbox.mapboxsdk.plugins.annotation.FillManager; import com.mapbox.mapboxsdk.plugins.annotation.OnAnnotationClickListener; import com.mapbox.mapboxsdk.plugins.annotation.Symbol; import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; @@ -82,6 +87,8 @@ import static com.mapbox.mapboxgl.MapboxMapsPlugin.STOPPED; import com.mapbox.mapboxsdk.plugins.localization.LocalizationPlugin; +import com.mapbox.mapboxsdk.style.layers.RasterLayer; +import com.mapbox.mapboxsdk.style.sources.ImageSource; /** * Controller of a single MapboxMaps MapView instance. @@ -96,11 +103,12 @@ final class MapboxMapController MapboxMap.OnMapLongClickListener, MapboxMapOptionsSink, MethodChannel.MethodCallHandler, - com.mapbox.mapboxsdk.maps.OnMapReadyCallback, + OnMapReadyCallback, OnCameraTrackingChangedListener, OnSymbolTappedListener, OnLineTappedListener, OnCircleTappedListener, + OnFillTappedListener, PlatformView { private static final String TAG = "MapboxMapController"; private final int id; @@ -112,9 +120,11 @@ final class MapboxMapController 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; @@ -127,6 +137,7 @@ final class MapboxMapController private final String styleStringInitial; private LocationComponent locationComponent = null; private LocationEngine locationEngine = null; + private LocationEngineCallback locationEngineCallback = null; private LocalizationPlugin localizationPlugin; private Style style; @@ -148,6 +159,7 @@ final class MapboxMapController this.symbols = new HashMap<>(); this.lines = new HashMap<>(); this.circles = new HashMap<>(); + this.fills = new HashMap<>(); this.density = context.getResources().getDisplayMetrics().density; methodChannel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/mapbox_maps_" + id); @@ -255,11 +267,30 @@ private void removeCircle(String circleId) { private CircleController circle(String circleId) { final CircleController circle = circles.get(circleId); if (circle == null) { - throw new IllegalArgumentException("Unknown symbol: " + circleId); + 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; @@ -310,6 +341,7 @@ public void onStyleLoaded(@NonNull Style style) { enableLineManager(style); enableSymbolManager(style); enableCircleManager(style); + enableFillManager(style); if (myLocationEnabled) { enableLocationComponent(style); } @@ -317,8 +349,7 @@ public void onStyleLoaded(@NonNull Style style) { // is fixed with 0.6.0 of annotations plugin mapboxMap.addOnMapClickListener(MapboxMapController.this); mapboxMap.addOnMapLongClickListener(MapboxMapController.this); - - localizationPlugin = new LocalizationPlugin(mapView, mapboxMap, style); + localizationPlugin = new LocalizationPlugin(mapView, mapboxMap, style); methodChannel.invokeMethod("map#onStyleLoaded", null); } @@ -347,6 +378,24 @@ private void enableLocationComponent(@NonNull Style style) { } } + 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("altitude", location.getAltitude()); + userLocation.put("bearing", location.getBearing()); + userLocation.put("horizontalAccuracy", location.getAccuracy()); + userLocation.put("verticalAccuracy", (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? location.getVerticalAccuracyMeters() : null); + userLocation.put("timestamp", location.getTime()); + + final Map arguments = new HashMap<>(1); + arguments.put("userLocation", userLocation); + methodChannel.invokeMethod("map#onUserLocationUpdated", arguments); + } + private void enableSymbolManager(@NonNull Style style) { if (symbolManager == null) { symbolManager = new SymbolManager(mapView, mapboxMap, style); @@ -374,6 +423,13 @@ private void enableCircleManager(@NonNull Style style) { } } + private void enableFillManager(@NonNull Style style) { + if (fillManager == null) { + fillManager = new FillManager(mapView, mapboxMap, style); + fillManager.addClickListener(MapboxMapController.this::onAnnotationClick); + } + } + @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { switch (call.method) { @@ -424,6 +480,29 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { 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#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) { @@ -510,12 +589,12 @@ public void onCancel() { result.success(reply); break; } - case "map#setTelemetryEnabled": { + 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); @@ -690,6 +769,30 @@ public void onError(@NonNull String message) { 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, true, this)); + result.success(fillId); + 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) { @@ -716,14 +819,47 @@ public void onFailure(@NonNull Exception exception) { } break; } - case "style#addImage":{ - if(style==null){ + 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; } + 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")); + style.addSource(new ImageSource(call.argument("name"), 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); + } + style.removeSource((String) call.argument("name")); + 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); + } + style.addLayer(new RasterLayer(call.argument("name"), call.argument("sourceId"))); + 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); + } + style.removeLayer((String) call.argument("name")); + result.success(null); + break; + } default: result.notImplemented(); } @@ -766,11 +902,12 @@ public void onCameraTrackingDismissed() { } @Override - public void onAnnotationClick(Annotation annotation) { + public boolean onAnnotationClick(Annotation annotation) { if (annotation instanceof Symbol) { final SymbolController symbolController = symbols.get(String.valueOf(annotation.getId())); if (symbolController != null) { symbolController.onTap(); + return true; } } @@ -778,6 +915,7 @@ public void onAnnotationClick(Annotation annotation) { final LineController lineController = lines.get(String.valueOf(annotation.getId())); if (lineController != null) { lineController.onTap(); + return true; } } @@ -785,8 +923,17 @@ public void onAnnotationClick(Annotation annotation) { final CircleController circleController = circles.get(String.valueOf(annotation.getId())); if (circleController != null) { circleController.onTap(); + return true; + } + } + if (annotation instanceof Fill) { + final FillController fillController = fills.get(String.valueOf(annotation.getId())); + if (fillController != null) { + fillController.onTap(); + return true; } } + return false; } @Override @@ -810,6 +957,13 @@ public void onCircleTapped(Circle circle) { 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); + } + @Override public boolean onMapClick(@NonNull LatLng point) { PointF pointf = mapboxMap.getProjection().toScreenLocation(point); @@ -836,7 +990,7 @@ public boolean onMapLongClick(@NonNull LatLng point) { @Override public void dispose() { - if (disposed) { + if (disposed || registrar.activity() == null) { return; } disposed = true; @@ -852,7 +1006,10 @@ public void dispose() { if (circleManager != null) { circleManager.onDestroy(); } - + if (fillManager != null) { + fillManager.onDestroy(); + } + stopListeningForLocationUpdates(); mapView.onDestroy(); registrar.activity().getApplication().unregisterActivityLifecycleCallbacks(this); } @@ -879,6 +1036,9 @@ public void onActivityResumed(Activity activity) { return; } mapView.onResume(); + if(myLocationEnabled){ + startListeningForLocationUpdates(); + } } @Override @@ -887,6 +1047,7 @@ public void onActivityPaused(Activity activity) { return; } mapView.onPause(); + stopListeningForLocationUpdates(); } @Override @@ -1043,13 +1204,42 @@ public void setAttributionButtonMargins(int x, int y) { } private void updateMyLocationEnabled() { - if(this.locationComponent == null && myLocationEnabled == true){ + if(this.locationComponent == null && myLocationEnabled){ enableLocationComponent(mapboxMap.getStyle()); } + if(myLocationEnabled){ + startListeningForLocationUpdates(); + }else { + stopListeningForLocationUpdates(); + } + 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()); + } + + @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){ + locationComponent.getLocationEngine().removeLocationUpdates(locationEngineCallback); + locationEngineCallback = null; + } + } + private void updateMyLocationTrackingMode() { int[] mapboxTrackingModes = new int[] {CameraMode.NONE, CameraMode.TRACKING, CameraMode.TRACKING_COMPASS, CameraMode.TRACKING_GPS}; locationComponent.setCameraMode(mapboxTrackingModes[this.myLocationTrackingMode]); diff --git a/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java b/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java new file mode 100644 index 000000000..27eb86425 --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/OnFillTappedListener.java @@ -0,0 +1,7 @@ +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/SymbolBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java index 5f0583138..93e2718af 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java @@ -49,6 +49,9 @@ 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); diff --git a/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java index bd101032f..348779527 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/SymbolController.java @@ -67,6 +67,9 @@ 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); diff --git a/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java b/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java index b6aa5fc0a..ee52e8672 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java +++ b/android/src/main/java/com/mapbox/mapboxgl/SymbolOptionsSink.java @@ -23,6 +23,8 @@ interface SymbolOptionsSink { void setIconAnchor(String iconAnchor); + void setFontNames(String[] fontNames); + void setTextField(String textField); void setTextSize(float textSize); diff --git a/example/assets/fill/cat_silhouette_pattern.png b/example/assets/fill/cat_silhouette_pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..6f5ece826b8e11c01e88c837810e83fe0099efd0 GIT binary patch literal 972 zcmeAS@N?(olHy`uVBq!ia0vp^4nSNn{1`6_P!I zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W> zdx@v7EBiwZ4iRC7c_+N51Ld!Hx;TbJ9DX~)H$&J_qHX@a8Bxmr61mwNXKS!|2d)xN zb`4qOy=#X9;_qS($pF4MM{yoVb*Vi5i-E!2&UHeYL?GEF%^Smn< z?rt$S`^dWALBZk$TVohYlVa3IrUy^k`1CW1n-6d@`z@ZnyhpP1VC?I~OZx;Q4F6TG z^zI6cTz#}p;Kg}?=tsvyL_S~ra%pn+`Jd(j%e9@iT&rXfI4AIVw;`uoosS`7;k8|p zzsvbJF1Wk)NVS&kpYA8E@oyv=zsx?TyC_dEV*a(?mNy@|sOcn&w%?d(7WPo4ab^q0 z#-^-4k|A!ob6>f~oXV5?Xzcv%cl5m#?Doet=)Q32TyZra`^9-io4{wQo9&%c(kl)s zg?_lMa)8w^R{3eJ`qBriwOJ3OJH3T&EM>Zvez9Wbl;0vN6)xy<{+}eN^`PA25WhkA z*+2UyHT_@a)sr_>rhfCPtN#PI>H@AV3s;=_o2C7#YP{r+id9?_LS1-+w%C7g$%s?|E$oR^x})X z!Hc)=SWEC2X=M^yi`LpHDA)!55|SyXD>f3Gy|~ z7X!Clnt0{Yws-Rt6zV@%xL+wdx883j_r)(ataonRFz31D+^TsOj>^9BwcR3d@%m1# zPYQF7EGgUXDK_iZ&a5B)xA&*q_Um`8$ZolLIOx@jjjji4%ma=@?p<&E@<34Pr?hpO z&xE{R++5xBdaKa3hbOIC(rdh|oaNH_swL0b{aUnWvio literal 0 HcmV?d00001 diff --git a/example/assets/sydney.png b/example/assets/sydney.png new file mode 100644 index 0000000000000000000000000000000000000000..fe49145456f72e78031a7aea51f264c612b80f4b GIT binary patch literal 58396 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 DW}bas literal 0 HcmV?d00001 diff --git a/example/ios/Podfile b/example/ios/Podfile index a563f857a..17ffc678f 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -15,58 +15,71 @@ def parse_KV_file(file, separator='=') if !File.exists? file_abs_path return []; end - pods_ary = [] + generated_key_values = {} skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - pods_ary.push({:name => podname, :path => podpath}); - else - puts "Invalid plugin specification: #{line}" - end - } - return pods_ary + File.foreach(file_abs_path) do |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + generated_key_values[podname] = podpath + else + puts "Invalid plugin specification: #{line}" + end + end + generated_key_values end target 'Runner' do + # Flutter Pod use_frameworks! - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') + copied_flutter_dir = File.join(__dir__, 'Flutter') + copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') + copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') + unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) + # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. + # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. + # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - # Flutter Pods - generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') - if generated_xcode_build_settings.empty? - puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." - end - generated_xcode_build_settings.map { |p| - if p[:name] == 'FLUTTER_FRAMEWORK_DIR' - symlink = File.join('.symlinks', 'flutter') - File.symlink(File.dirname(p[:path]), symlink) - pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) + generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') + unless File.exist?(generated_xcode_build_settings_path) + raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) + cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; + + unless File.exist?(copied_framework_path) + FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) end - } + unless File.exist?(copied_podspec_path) + FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) + end + end + + # Keep pod path relative so it can be checked into Podfile.lock. + pod 'Flutter', :path => 'Flutter' # Plugin Pods + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.map { |p| - symlink = File.join('.symlinks', 'plugins', p[:name]) - File.symlink(p[:path], symlink) - pod p[:name], :path => File.join(symlink, 'ios') - } + plugin_pods.each do |name, path| + symlink = File.join('.symlinks', 'plugins', name) + File.symlink(path, symlink) + pod name, :path => File.join(symlink, 'ios') + end end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' - config.build_settings['SWIFT_VERSION'] = '4.2' end end end diff --git a/example/lib/animate_camera.dart b/example/lib/animate_camera.dart index b0cdf4c5b..2c9bcf6cd 100644 --- a/example/lib/animate_camera.dart +++ b/example/lib/animate_camera.dart @@ -87,7 +87,9 @@ class AnimateCameraState extends State { southwest: const LatLng(-38.483935, 113.248673), northeast: const LatLng(-8.982446, 153.823821), ), - 10.0, + left: 10, + top: 5, + bottom: 25, ), ); }, diff --git a/example/lib/main.dart b/example/lib/main.dart index c6609acff..cc11e665f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -15,7 +15,9 @@ import 'map_ui.dart'; import 'move_camera.dart'; import 'page.dart'; import 'place_circle.dart'; +import 'place_source.dart'; import 'place_symbol.dart'; +import 'place_fill.dart'; import 'scrolling_map.dart'; final List _allPages = [ @@ -24,8 +26,10 @@ final List _allPages = [ AnimateCameraPage(), MoveCameraPage(), PlaceSymbolPage(), + PlaceSourcePage(), LinePage(), PlaceCirclePage(), + PlaceFillPage(), ScrollingMapPage(), OfflineRegionsPage(), ]; diff --git a/example/lib/map_ui.dart b/example/lib/map_ui.dart index 6e344495b..216a0ecd5 100644 --- a/example/lib/map_ui.dart +++ b/example/lib/map_ui.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; @@ -54,8 +57,9 @@ class MapUiBodyState extends State { bool _zoomGesturesEnabled = true; bool _myLocationEnabled = true; bool _telemetryEnabled = true; - MyLocationTrackingMode _myLocationTrackingMode = MyLocationTrackingMode.Tracking; + MyLocationTrackingMode _myLocationTrackingMode = MyLocationTrackingMode.None; List _featureQueryFilter; + Fill _selectedFill; @override void initState() { @@ -238,6 +242,38 @@ class MapUiBodyState extends State { ); } + _clearFill() { + if (_selectedFill != null) { + mapController.removeFill(_selectedFill); + setState(() { + _selectedFill = null; + }); + } + } + + _drawFill(features) async { + Map feature = jsonDecode(features[0]); + if (feature['geometry']['type'] == 'Polygon') { + var coordinates = feature['geometry']['coordinates']; + List> geometry = coordinates.map( + (ll) => ll.map( + (l) => LatLng(l[1],l[0]) + ).toList().cast() + ).toList().cast>(); + Fill fill = await mapController.addFill( + FillOptions( + geometry: geometry, + fillColor: "#FF0000", + fillOutlineColor: "#FF0000", + fillOpacity: 0.6, + ) + ); + setState(() { + _selectedFill = fill; + }); + } + } + @override Widget build(BuildContext context) { final MapboxMap mapboxMap = MapboxMap( @@ -260,12 +296,24 @@ class MapUiBodyState extends State { print("Map click: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); print("Filter $_featureQueryFilter"); List features = await mapController.queryRenderedFeatures(point, [], _featureQueryFilter); - if (features.length>0) { - print(features[0]); - } + print('# features: ${features.length}'); + _clearFill(); + if (features.length == 0 && _featureQueryFilter != null) { + Scaffold.of(context).showSnackBar(SnackBar(content: Text('QueryRenderedFeatures: No features found!'))); + } else { + _drawFill(features); + } }, onMapLongClick: (point, latLng) async { print("Map long press: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); + Point convertedPoint = await mapController.toScreenLocation(latLng); + LatLng convertedLatLng = await mapController.toLatLng(point); + print("Map long press converted: ${convertedPoint.x},${convertedPoint.y} ${convertedLatLng.latitude}/${convertedLatLng.longitude}"); + double metersPerPixel = await mapController.getMetersPerPixelAtLatitude(latLng.latitude); + + print ("Map long press The distance measured in meters at latitude ${latLng.latitude} is $metersPerPixel m"); + + List features = await mapController.queryRenderedFeatures(point, [], null); if (features.length>0) { print(features[0]); @@ -275,7 +323,10 @@ class MapUiBodyState extends State { this.setState(() { _myLocationTrackingMode = MyLocationTrackingMode.None; }); - } + }, + onUserLocationUpdated:(location){ + print("new location: ${location.position}, alt.: ${location.altitude}, bearing: ${location.bearing}, speed: ${location.speed}, horiz. accuracy: ${location.horizontalAccuracy}, vert. accuracy: ${location.verticalAccuracy}"); + }, ); final List columnChildren = [ diff --git a/example/lib/move_camera.dart b/example/lib/move_camera.dart index 3805b0200..4701dba5e 100644 --- a/example/lib/move_camera.dart +++ b/example/lib/move_camera.dart @@ -87,7 +87,9 @@ class MoveCameraState extends State { southwest: const LatLng(-38.483935, 113.248673), northeast: const LatLng(-8.982446, 153.823821), ), - 10.0, + left: 10, + top: 5, + bottom: 25, ), ); }, diff --git a/example/lib/place_fill.dart b/example/lib/place_fill.dart new file mode 100644 index 000000000..480c7dde2 --- /dev/null +++ b/example/lib/place_fill.dart @@ -0,0 +1,252 @@ +// 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 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:mapbox_gl_example/main.dart'; + +import 'page.dart'; + +class PlaceFillPage extends ExamplePage { + PlaceFillPage() : super(const Icon(Icons.check_circle), 'Place fill'); + + @override + Widget build(BuildContext context) { + return const PlaceFillBody(); + } +} + +class PlaceFillBody extends StatefulWidget { + const PlaceFillBody(); + + @override + State createState() => PlaceFillBodyState(); +} + +class PlaceFillBodyState extends State { + PlaceFillBodyState(); + + static final LatLng center = const LatLng(-33.86711, 151.1947171); + final String _fillPatternImage = "assets/fill/cat_silhouette_pattern.png"; + + MapboxMapController controller; + int _fillCount = 0; + Fill _selectedFill; + + void _onMapCreated(MapboxMapController controller) { + this.controller = controller; + controller.onFillTapped.add(_onFillTapped); + } + + void _onStyleLoaded() { + addImageFromAsset("assetImage", _fillPatternImage); + } + + /// 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); + } + + @override + void dispose() { + controller?.onFillTapped?.remove(_onFillTapped); + super.dispose(); + } + + void _onFillTapped(Fill fill) { + setState(() { + _selectedFill = fill; + }); + } + + void _updateSelectedFill(FillOptions changes) { + controller.updateFill(_selectedFill, changes); + } + + void _add() { + controller.addFill( + FillOptions(geometry: [ + [ + LatLng(-32.81711, 151.1447171), + LatLng(-32.81711, 152.2447171), + LatLng(-33.91711, 152.2447171), + LatLng(-33.91711, 151.1447171), + ], + [ + LatLng(-32.86711, 152.1947171), + LatLng(-33.86711, 151.1947171), + LatLng(-32.86711, 151.1947171), + LatLng(-33.86711, 152.1947171), + ] + ], + fillColor: "#FF0000", + fillOutlineColor: "#FF0000"), + ); + setState(() { + _fillCount += 1; + }); + } + + void _remove() { + controller.removeFill(_selectedFill); + setState(() { + _selectedFill = null; + _fillCount -= 1; + }); + } + + void _changePosition() { + //TODO: Implement change position. + } + + void _changeDraggable() { + bool draggable = _selectedFill.options.draggable; + if (draggable == null) { + // default value + draggable = false; + } + _updateSelectedFill( + FillOptions(draggable: !draggable), + ); + } + + Future _changeFillOpacity() async { + double current = _selectedFill.options.fillOpacity; + if (current == null) { + // default value + current = 1.0; + } + + _updateSelectedFill( + FillOptions(fillOpacity: current < 0.1 ? 1.0 : current * 0.75), + ); + } + + Future _changeFillColor() async { + String current = _selectedFill.options.fillColor; + if (current == null) { + // default value + current = "#FF0000"; + } + + _updateSelectedFill( + FillOptions(fillColor: "#FFFF00"), + ); + } + + Future _changeFillOutlineColor() async { + String current = _selectedFill.options.fillOutlineColor; + if (current == null) { + // default value + current = "#FF0000"; + } + + _updateSelectedFill( + FillOptions(fillOutlineColor: "#FFFF00"), + ); + } + + Future _changeFillPattern() async { + String current = _selectedFill.options.fillPattern == null ? "assetImage" : null; + _updateSelectedFill( + FillOptions(fillPattern: current), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: MapboxMap( + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + onStyleLoadedCallback: _onStyleLoaded, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 7.0, + ), + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + FlatButton( + child: const Text('add'), + onPressed: (_fillCount == 12) ? null : _add, + ), + FlatButton( + child: const Text('remove'), + onPressed: (_selectedFill == null) ? null : _remove, + ), + ], + ), + Column( + children: [ + FlatButton( + child: const Text('change fill-opacity'), + onPressed: (_selectedFill == null) + ? null + : _changeFillOpacity, + ), + FlatButton( + child: const Text('change fill-color'), + onPressed: (_selectedFill == null) + ? null + : _changeFillColor, + ), + FlatButton( + child: const Text('change fill-outline-color'), + onPressed: (_selectedFill == null) + ? null + : _changeFillOutlineColor, + ), + FlatButton( + child: const Text('change fill-pattern'), + onPressed: (_selectedFill == null) + ? null + : _changeFillPattern, + ), + FlatButton( + child: const Text('change position'), + onPressed: (_selectedFill == null) + ? null + : _changePosition, + ), + FlatButton( + child: const Text('toggle draggable'), + onPressed: (_selectedFill == null) + ? null + : _changeDraggable, + ), + ], + ), + ], + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/place_source.dart b/example/lib/place_source.dart new file mode 100644 index 000000000..0f5a0eb1c --- /dev/null +++ b/example/lib/place_source.dart @@ -0,0 +1,132 @@ +// 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 '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'; +import 'page.dart'; + +class PlaceSourcePage extends ExamplePage { + PlaceSourcePage() : super(const Icon(Icons.place), 'Place source'); + + @override + Widget build(BuildContext context) { + return const PlaceSymbolBody(); + } +} + +class PlaceSymbolBody extends StatefulWidget { + const PlaceSymbolBody(); + + @override + State createState() => PlaceSymbolBodyState(); +} + +class PlaceSymbolBodyState extends State { + PlaceSymbolBodyState(); + + bool sourceAdded = false; + MapboxMapController controller; + + void _onMapCreated(MapboxMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + /// Adds an asset image as a source to the currently displayed style + Future addImageSourceFromAsset(String name, String assetName) async { + final ByteData bytes = await rootBundle.load(assetName); + final Uint8List list = bytes.buffer.asUint8List(); + return controller.addImageSource( + name, + list, + LatLngQuad( + bottomRight: const LatLng(-33.86264728692581, 151.19916915893555), + bottomLeft: const LatLng(-33.86264728692581, 151.2288236618042), + topLeft: const LatLng(-33.84322353475214, 151.2288236618042), + topRight: const LatLng(-33.84322353475214, 151.19916915893555), + )); + } + + Future removeImageSource(String name){ + return controller.removeImageSource(name); + } + + Future addLayer(String layerName, String sourceId) { + return controller.addLayer(layerName, sourceId); + } + + Future removeLayer(String layerName) { + return controller.removeLayer(layerName); + } + + @override + Widget build(BuildContext context) { + return Column( + 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, + ), + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + FlatButton( + child: const Text('Add source (asset image)'), + onPressed: sourceAdded ? null : () => addImageSourceFromAsset("sydney", "assets/sydney.png").then((value) => setState(() => sourceAdded = true)), + ), + FlatButton( + child: const Text('Remove source (asset image)'), + onPressed: sourceAdded ? () async { + await removeLayer("imageLayer"); + removeImageSource("sydney").then((value) => setState(() => sourceAdded = false)); + } : null, + ), + FlatButton( + child: const Text('Show layer'), + onPressed: sourceAdded ? () => addLayer("imageLayer", "sydney") : null, + ), + FlatButton( + child: const Text('Hide layer'), + onPressed: sourceAdded ? () => removeLayer("imageLayer") : null, + ), + ], + ), + ], + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/place_symbol.dart b/example/lib/place_symbol.dart index 6ec86719b..38e7e4871 100644 --- a/example/lib/place_symbol.dart +++ b/example/lib/place_symbol.dart @@ -106,13 +106,28 @@ class PlaceSymbolBodyState extends State { } SymbolOptions _getSymbolOptions(String iconImage, int symbolCount){ - return SymbolOptions( - geometry: LatLng( - center.latitude + sin(symbolCount * pi / 6.0) / 20.0, - center.longitude + cos(symbolCount * pi / 6.0) / 20.0, - ), - iconImage: iconImage, + LatLng geometry = LatLng( + center.latitude + sin(symbolCount * pi / 6.0) / 20.0, + center.longitude + cos(symbolCount * pi / 6.0) / 20.0, ); + return iconImage == 'customFont' + ? SymbolOptions( + geometry: geometry, + iconImage: 'airport-15', + fontNames: ['DIN Offc Pro Bold', 'Arial Unicode MS Regular'], + textField: 'Airport', + textSize: 12.5, + textOffset: Offset(0, 0.8), + textAnchor: 'top', + textColor: '#000000', + textHaloBlur: 1, + textHaloColor: '#ffffff', + textHaloWidth: 0.8, + ) + : SymbolOptions( + geometry: geometry, + iconImage: iconImage, + ); } Future _addAll(String iconImage) async { @@ -334,6 +349,10 @@ class PlaceSymbolBodyState extends State { onPressed: () => (_symbolCount == 12) ? null : _add("networkImage"), //networkImage added to the style in _onStyleLoaded ), + FlatButton( + child: const Text('add (custom font)'), + onPressed: () => (_symbolCount == 12) ? null : _add("customFont"), + ) ], ), Column( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e79256f80..8cd2a3e1c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,7 @@ name: mapbox_gl_example description: Demonstrates how to use the mapbox_gl plugin. publish_to: 'none' +version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" @@ -48,10 +49,12 @@ flutter: # https://flutter.io/assets-and-images/#resolution-aware. assets: + - assets/fill/cat_silhouette_pattern.png - assets/symbols/custom-icon.png - assets/symbols/2.0x/custom-icon.png - assets/symbols/3.0x/custom-icon.png - assets/style.json + - assets/sydney.png # For details regarding adding assets from package dependencies, see # https://flutter.io/assets-and-images/#from-packages diff --git a/ios/Classes/Convert.swift b/ios/Classes/Convert.swift index e7dfd85bb..bcaed1638 100644 --- a/ios/Classes/Convert.swift +++ b/ios/Classes/Convert.swift @@ -67,8 +67,11 @@ class Convert { return camera case "newLatLngBounds": guard let bounds = cameraUpdate[1] as? [[Double]] else { return nil } - guard let padding = cameraUpdate[2] as? CGFloat else { return nil } - return mapView.cameraThatFitsCoordinateBounds(MGLCoordinateBounds.fromArray(bounds), edgePadding: UIEdgeInsets.init(top: padding, left: padding, bottom: padding, right: padding)) + guard let paddingLeft = cameraUpdate[2] as? CGFloat else { return nil } + 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)) case "newLatLngZoom": guard let coordinate = cameraUpdate[1] as? [Double] else { return nil } guard let zoom = cameraUpdate[2] as? Double else { return nil } @@ -176,6 +179,9 @@ class Convert { 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 } @@ -317,4 +323,36 @@ class Convert { 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 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])) + } + let polygon = MGLPolygonFeature(coordinates: linearRing, count: UInt(linearRing.count)) + polygons.append(polygon) + } + return polygons + } } diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift index 1b5039d99..461b0ffe2 100644 --- a/ios/Classes/Extensions.swift +++ b/ios/Classes/Extensions.swift @@ -19,6 +19,19 @@ extension MGLMapCamera { } } +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) + ] + } +} + extension CLLocationCoordinate2D { func toArray() -> [Double] { return [self.latitude, self.longitude] @@ -89,3 +102,10 @@ extension UIColor { return nil } } + + +extension Array { + var tail: Array { + return Array(self.dropFirst()) + } +} diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 6384ee51a..78db4227a 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -20,6 +20,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma private var symbolAnnotationController: MGLSymbolAnnotationController? private var circleAnnotationController: MGLCircleAnnotationController? private var lineAnnotationController: MGLLineAnnotationController? + private var fillAnnotationController: MGLPolygonAnnotationController? func view() -> UIView { return mapView @@ -38,7 +39,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma super.init() channel = FlutterMethodChannel(name: "plugins.flutter.io/mapbox_maps_\(viewId)", binaryMessenger: registrar.messenger()) - channel!.setMethodCallHandler(onMethodCall) + channel!.setMethodCallHandler{ [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } mapView.delegate = self @@ -165,6 +166,33 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma reply["sw"] = [visibleRegion.sw.latitude, visibleRegion.sw.longitude] as NSObject reply["ne"] = [visibleRegion.ne.latitude, visibleRegion.ne.longitude] as NSObject result(reply) + case "map#toScreenLocation": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let latitude = arguments["latitude"] as? Double else { return } + guard let longitude = arguments["longitude"] as? Double else { return } + let latlng = CLLocationCoordinate2DMake(latitude, longitude) + let returnVal = mapView.convert(latlng, toPointTo: mapView) + var reply = [String: NSObject]() + reply["x"] = returnVal.x as NSObject + reply["y"] = returnVal.y as NSObject + result(reply) + 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) + 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: y, 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 + result(reply) case "camera#move": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let cameraUpdate = arguments["cameraUpdate"] as? [Any] else { return } @@ -372,6 +400,51 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } } 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) + identifier = fill.identifier + } + result(identifier) + 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; + } + } + result(nil) case "style#addImage": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let name = arguments["name"] as? String else { return } @@ -386,6 +459,46 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma self.mapView.style?.setImage(image, forName: name) } result(nil) + case "style#addImageSource": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let name = arguments["name"] 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 }; + 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]) + ) + + let source = MGLImageSource(identifier: name, coordinateQuad: quad, image: image) + self.mapView.style?.addSource(source) + + result(nil) + case "style#removeImageSource": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let name = arguments["name"] as? String else { return } + guard let source = self.mapView.style?.source(withIdentifier: name) else { return } + self.mapView.style?.removeSource(source) + result(nil) + case "style#addLayer": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let name = arguments["name"] as? String else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + + guard let source = self.mapView.style?.source(withIdentifier: sourceId) else { return } + let layer = MGLRasterStyleLayer(identifier: name, source: source) + self.mapView.style?.addLayer(layer) + result(nil) + case "style#removeLayer": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let name = arguments["name"] as? String else { return } + guard let layer = self.mapView.style?.layer(withIdentifier: name) else { return } + self.mapView.style?.removeLayer(layer) + result(nil) default: result(FlutterMethodNotImplemented) } @@ -475,6 +588,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma * MGLAnnotationControllerDelegate */ func annotationController(_ annotationController: MGLAnnotationController, didSelect styleAnnotation: MGLStyleAnnotation) { + annotationController.deselectStyleAnnotation(styleAnnotation) guard let channel = channel else { return } @@ -485,6 +599,8 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma 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)"]) } } @@ -521,6 +637,10 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma circleAnnotationController!.annotationsInteractionEnabled = true circleAnnotationController?.delegate = self + fillAnnotationController = MGLPolygonAnnotationController(mapView: self.mapView) + fillAnnotationController!.annotationsInteractionEnabled = true + fillAnnotationController?.delegate = self + mapReadyResult?(nil) if let channel = channel { channel.invokeMethod("map#onStyleLoaded", arguments: nil) @@ -585,6 +705,14 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool { return true } + + 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() + ]); + } + } func mapView(_ mapView: MGLMapView, didChange mode: MGLUserTrackingMode, animated: Bool) { if let channel = channel { diff --git a/lib/mapbox_gl.dart b/lib/mapbox_gl.dart index b6c70bf0f..01cb5ed11 100644 --- a/lib/mapbox_gl.dart +++ b/lib/mapbox_gl.dart @@ -19,6 +19,7 @@ export 'package:mapbox_gl_platform_interface/mapbox_gl_platform_interface.dart' show LatLng, LatLngBounds, + LatLngQuad, CameraPosition, CameraUpdate, ArgumentCallbacks, @@ -29,12 +30,15 @@ export 'package:mapbox_gl_platform_interface/mapbox_gl_platform_interface.dart' MapboxStyles, MyLocationTrackingMode, MyLocationRenderMode, + CompassViewPosition, Circle, CircleOptions, Line, - LineOptions; + LineOptions, + Fill, + FillOptions; + -part 'src/bitmap.dart'; part 'src/controller.dart'; part 'src/mapbox_map.dart'; part 'src/global.dart'; diff --git a/lib/src/bitmap.dart b/lib/src/bitmap.dart deleted file mode 100644 index 5393ba594..000000000 --- a/lib/src/bitmap.dart +++ /dev/null @@ -1,49 +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. - -part of mapbox_gl; - -/// Defines a bitmap image. For a marker, this class can be used to set the -/// image of the marker icon. For a ground overlay, it can be used to set the -/// image to place on the surface of the earth. -class BitmapDescriptor { - const BitmapDescriptor._(this._json); - - static const double hueRed = 0.0; - static const double hueOrange = 30.0; - static const double hueYellow = 60.0; - static const double hueGreen = 120.0; - static const double hueCyan = 180.0; - static const double hueAzure = 210.0; - static const double hueBlue = 240.0; - static const double hueViolet = 270.0; - static const double hueMagenta = 300.0; - static const double hueRose = 330.0; - - /// Creates a BitmapDescriptor that refers to the default marker image. - static const BitmapDescriptor defaultMarker = - BitmapDescriptor._(['defaultMarker']); - - /// Creates a BitmapDescriptor that refers to a colorization of the default - /// marker image. For convenience, there is a predefined set of hue values. - /// See e.g. [hueYellow]. - static BitmapDescriptor defaultMarkerWithHue(double hue) { - assert(0.0 <= hue && hue < 360.0); - return BitmapDescriptor._(['defaultMarker', hue]); - } - - /// Creates a BitmapDescriptor using the name of a bitmap image in the assets - /// directory. - static BitmapDescriptor fromAsset(String assetName, {String package}) { - if (package == null) { - return BitmapDescriptor._(['fromAsset', assetName]); - } else { - return BitmapDescriptor._(['fromAsset', assetName, package]); - } - } - - final dynamic _json; - - dynamic _toJson() => _json; //ignore: unused_element -} diff --git a/lib/src/controller.dart b/lib/src/controller.dart index a0eb4372a..c447fe36b 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -9,6 +9,8 @@ typedef void OnMapLongClickCallback(Point point, LatLng coordinates); typedef void OnStyleLoadedCallback(); +typedef void OnUserLocationUpdated(UserLocation location); + typedef void OnCameraTrackingDismissedCallback(); typedef void OnCameraTrackingChangedCallback(MyLocationTrackingMode mode); @@ -38,8 +40,9 @@ class MapboxMapController extends ChangeNotifier { this.onMapLongClick, this.onCameraTrackingDismissed, this.onCameraTrackingChanged, - this.onCameraIdle, - this.onMapIdle}) + this.onMapIdle, + this.onUserLocationUpdated, + this.onCameraIdle}) : assert(_id != null) { _cameraPosition = initialCameraPosition; @@ -73,6 +76,13 @@ class MapboxMapController extends ChangeNotifier { } }); + MapboxGlPlatform.getInstance(_id).onFillTappedPlatform.add((fillId) { + final Fill fill = _fills[fillId]; + if (fill != null) { + onFillTapped(fill); + } + }); + MapboxGlPlatform.getInstance(_id).onCameraMoveStartedPlatform.add((_) { _isCameraMoving = true; notifyListeners(); @@ -132,22 +142,25 @@ class MapboxMapController extends ChangeNotifier { onMapIdle(); } }); + MapboxGlPlatform.getInstance(_id).onUserLocationUpdatedPlatform.add((location) { + onUserLocationUpdated?.call(location); + }); } - static Future init( - int id, CameraPosition initialCameraPosition, + static MapboxMapController init(int id, CameraPosition initialCameraPosition, {OnStyleLoadedCallback onStyleLoadedCallback, OnMapClickCallback onMapClick, + OnUserLocationUpdated onUserLocationUpdated, OnMapLongClickCallback onMapLongClick, OnCameraTrackingDismissedCallback onCameraTrackingDismissed, OnCameraTrackingChangedCallback onCameraTrackingChanged, OnCameraIdleCallback onCameraIdle, - OnMapIdleCallback onMapIdle}) async { + OnMapIdleCallback onMapIdle}) { assert(id != null); - await MapboxGlPlatform.getInstance(id).initPlatform(id); return MapboxMapController._(id, initialCameraPosition, onStyleLoadedCallback: onStyleLoadedCallback, onMapClick: onMapClick, + onUserLocationUpdated: onUserLocationUpdated, onMapLongClick: onMapLongClick, onCameraTrackingDismissed: onCameraTrackingDismissed, onCameraTrackingChanged: onCameraTrackingChanged, @@ -155,11 +168,18 @@ class MapboxMapController extends ChangeNotifier { onMapIdle: onMapIdle); } + static Future initPlatform(int id) async { + assert(id != null); + await MapboxGlPlatform.getInstance(id).initPlatform(id); + } + final OnStyleLoadedCallback onStyleLoadedCallback; final OnMapClickCallback onMapClick; final OnMapLongClickCallback onMapLongClick; + final OnUserLocationUpdated onUserLocationUpdated; + final OnCameraTrackingDismissedCallback onCameraTrackingDismissed; final OnCameraTrackingChangedCallback onCameraTrackingChanged; @@ -173,6 +193,9 @@ class MapboxMapController extends ChangeNotifier { /// Callbacks to receive tap events for symbols placed on this map. final ArgumentCallbacks onCircleTapped = ArgumentCallbacks(); + /// Callbacks to receive tap events for fills placed on this map. + final ArgumentCallbacks onFillTapped = ArgumentCallbacks(); + /// Callbacks to receive tap events for info windows on symbols final ArgumentCallbacks onInfoWindowTapped = ArgumentCallbacks(); @@ -194,10 +217,16 @@ class MapboxMapController extends ChangeNotifier { /// The current set of circles on this map. /// - /// The returned set will be a detached snapshot of the symbols collection. + /// The returned set will be a detached snapshot of the circles collection. Set get circles => Set.from(_circles.values); final Map _circles = {}; + /// 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 = {}; + /// True if the map camera is currently moving. bool get isCameraMoving => _isCameraMoving; bool _isCameraMoving = false; @@ -323,17 +352,17 @@ class MapboxMapController extends ChangeNotifier { /// been notified. Future addSymbol(SymbolOptions options, [Map data]) async { List result = await addSymbols([options], [data]); - + return result.first; } + Future> addSymbols(List options, + [List data]) async { + final List effectiveOptions = + options.map((o) => SymbolOptions.defaultOptions.copyWith(o)).toList(); - Future> addSymbols(List options, [List data]) async { - final List effectiveOptions = options.map( - (o) => SymbolOptions.defaultOptions.copyWith(o) - ).toList(); - - final symbols = await MapboxGlPlatform.getInstance(_id).addSymbols(effectiveOptions, data); + final symbols = await MapboxGlPlatform.getInstance(_id) + .addSymbols(effectiveOptions, data); symbols.forEach((s) => _symbols[s.id] = s); notifyListeners(); return symbols; @@ -386,7 +415,7 @@ class MapboxMapController extends ChangeNotifier { symbols.forEach((s) { assert(_symbols[s.id] == s); }); - + await _removeSymbols(symbols.map((s) => s.id)); notifyListeners(); } @@ -425,7 +454,7 @@ class MapboxMapController extends ChangeNotifier { final LineOptions effectiveOptions = LineOptions.defaultOptions.copyWith(options); final line = - await MapboxGlPlatform.getInstance(_id).addLine(effectiveOptions); + await MapboxGlPlatform.getInstance(_id).addLine(effectiveOptions, data); _lines[line.id] = line; notifyListeners(); return line; @@ -508,8 +537,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); + final circle = await MapboxGlPlatform.getInstance(_id) + .addCircle(effectiveOptions, data); _circles[circle.id] = circle; notifyListeners(); return circle; @@ -583,6 +612,63 @@ class MapboxMapController extends ChangeNotifier { _circles.remove(id); } + /// Adds a fill to the map, configured using the specified custom [options]. + /// + /// Change listeners are notified once the fill has been added on the + /// platform side. + /// + /// The returned [Future] completes with the added fill once listeners have + /// been notified. + Future addFill(FillOptions options, [Map data]) async { + final FillOptions effectiveOptions = + FillOptions.defaultOptions.copyWith(options); + final fill = await MapboxGlPlatform.getInstance(_id).addFill(effectiveOptions); + _fills[fill.id] = fill; + notifyListeners(); + return fill; + } + + /// Updates the specified [fill] with the given [changes]. The fill must + /// be a current member of the [fills] set. + /// + /// Change listeners are notified once the fill has been updated on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future updateFill(Fill fill, FillOptions changes) async { + assert(fill != null); + assert(_fills[fill.id] == fill); + assert(changes != null); + await MapboxGlPlatform.getInstance(_id).updateFill(fill, changes); + fill.options = fill.options.copyWith(changes); + notifyListeners(); + } + + /// Removes the specified [fill] from the map. The fill must be a current + /// member of the [fills] set. + /// + /// Change listeners are notified once the fill has been removed on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future removeFill(Fill fill) async { + assert(fill != null); + assert(_fills[fill.id] == fill); + await _removeFill(fill.id); + notifyListeners(); + } + + /// Helper method to remove a single fill from the map. Consumed by + /// [removeFill] and [clearFills]. + /// + /// The returned [Future] completes once the fill has been removed from + /// [_fills]. + Future _removeFill(String id) async { + await MapboxGlPlatform.getInstance(_id).removeFill(id); + + _fills.remove(id); + } + Future queryRenderedFeatures( Point point, List layerIds, List filter) async { return MapboxGlPlatform.getInstance(_id) @@ -671,4 +757,47 @@ class MapboxMapController extends ChangeNotifier { await MapboxGlPlatform.getInstance(_id) .setSymbolTextIgnorePlacement(enable); } -} + + /// Adds an image source to the style currently displayed in the map, so that it can later be referred to by the provided name. + Future addImageSource(String name, Uint8List bytes, LatLngQuad coordinates) { + return MapboxGlPlatform.getInstance(_id) + .addImageSource(name, bytes, coordinates); + } + + /// Removes previously added image source by name + Future removeImageSource(String name) { + return MapboxGlPlatform.getInstance(_id).removeImageSource(name); + } + + /// Adds layer with name + Future addLayer(String name, String sourceId) { + return MapboxGlPlatform.getInstance(_id).addLayer(name, sourceId); + } + + /// Removes layer by name + Future removeLayer(String name) { + return MapboxGlPlatform.getInstance(_id).removeLayer(name); + } + + /// Returns the point on the screen that corresponds to a geographical coordinate ([latLng]). The screen location is in screen pixels (not display pixels) relative to the top left of the map (not of the whole screen) + /// + /// Note: The resulting x and y coordinates are rounded to [int] on web, on other platforms they may differ very slightly (in the range of about 10^-10) from the actual nearest screen coordinate. + /// You therefore might want to round them appropriately, depending on your use case. + /// + /// Returns null if [latLng] is not currently visible on the map. + Future toScreenLocation(LatLng latLng) async { + return MapboxGlPlatform.getInstance(_id).toScreenLocation(latLng); + } + + /// 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); + } + + /// 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); + } + +} \ No newline at end of file diff --git a/lib/src/mapbox_map.dart b/lib/src/mapbox_map.dart index 1a765a848..9e4f8caff 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -30,6 +30,7 @@ class MapboxMap extends StatefulWidget { this.compassViewMargins, this.attributionButtonMargins, this.onMapClick, + this.onUserLocationUpdated, this.onMapLongClick, this.onCameraTrackingDismissed, this.onCameraTrackingChanged, @@ -37,15 +38,14 @@ class MapboxMap extends StatefulWidget { this.onMapIdle, }) : assert(initialCameraPosition != null); - /// If you want to use Mapbox hosted styles and map tiles, you need to provide a Mapbox access token. /// Obtain a free access token on [your Mapbox account page](https://www.mapbox.com/account/access-tokens/). /// The reccommended way is to use this parameter to set your access token, an alternative way to add your access tokens through external files is described in the plugin's wiki on Github. - /// + /// /// Note: You should not use this parameter AND set the access token through external files at the same time, and you should use the same token throughout the entire app. final String accessToken; - /// Please note: you should only add annotations (e.g. symbols or circles) after `onStyleLoadedCallback` has been called. + /// Please note: you should only add annotations (e.g. symbols or circles) after `onStyleLoadedCallback` has been called. final MapCreatedCallback onMapCreated; /// Called when the map style has been successfully loaded and the annotation managers have been enabled. @@ -114,7 +114,8 @@ class MapboxMap extends StatefulWidget { /// when the map tries to turn on the My Location layer. final bool myLocationEnabled; - /// The mode used to let the map's camera follow the device's physical location + /// The mode used to let the map's camera follow the device's physical location. + /// `myLocationEnabled` needs to be true for values other than `MyLocationTrackingMode.None` to work. final MyLocationTrackingMode myLocationTrackingMode; /// The mode to render the user location symbol @@ -146,9 +147,13 @@ class MapboxMap extends StatefulWidget { final OnMapClickCallback onMapClick; final OnMapClickCallback onMapLongClick; + /// While the `myLocationEnabled` property is set to `true`, this method is + /// called whenever a new location update is received by the map view. + final OnUserLocationUpdated onUserLocationUpdated; + /// Called when the map's camera no longer follows the physical device location, e.g. because the user moved the map final OnCameraTrackingDismissedCallback onCameraTrackingDismissed; - + /// Called when the location tracking mode changes final OnCameraTrackingChangedCallback onCameraTrackingChanged; @@ -211,15 +216,25 @@ class _MapboxMapState extends State { Future onPlatformViewCreated(int id) async { MapboxGlPlatform.addInstance(id, _mapboxGlPlatform); - final MapboxMapController controller = await MapboxMapController.init( - id, widget.initialCameraPosition, - onStyleLoadedCallback: widget.onStyleLoadedCallback, + final MapboxMapController controller = MapboxMapController.init( + id, + widget.initialCameraPosition, + onStyleLoadedCallback: () { + if (_controller.isCompleted) { + widget.onStyleLoadedCallback(); + } else { + _controller.future.then((_) => widget.onStyleLoadedCallback()); + } + }, onMapClick: widget.onMapClick, + onUserLocationUpdated: widget.onUserLocationUpdated, onMapLongClick: widget.onMapLongClick, onCameraTrackingDismissed: widget.onCameraTrackingDismissed, onCameraTrackingChanged: widget.onCameraTrackingChanged, onCameraIdle: widget.onCameraIdle, - onMapIdle: widget.onMapIdle); + onMapIdle: widget.onMapIdle + ); + await MapboxMapController.initPlatform(id); _controller.complete(controller); if (widget.onMapCreated != null) { widget.onMapCreated(controller); diff --git a/mapbox_gl_platform_interface/CHANGELOG.md b/mapbox_gl_platform_interface/CHANGELOG.md index c15887ba4..3b957979c 100644 --- a/mapbox_gl_platform_interface/CHANGELOG.md +++ b/mapbox_gl_platform_interface/CHANGELOG.md @@ -1,2 +1,17 @@ +## 0.9.0, October 24. 2020 +* Breaking change: CameraUpdate.newLatLngBounds() now supports setting different padding values for left, top, right, bottom with default of 0 for all. Implementations using the old approach with only one padding value for all edges have to be updated. [#382](https://github.com/tobrun/flutter-mapbox-gl/pull/382) +* Add methods to access projection [#380](https://github.com/tobrun/flutter-mapbox-gl/pull/380) +* Add fill API support for Android and iOS [#49](https://github.com/tobrun/flutter-mapbox-gl/pull/49) +* Listen to OnUserLocationUpdated to provide user location to app [#237](https://github.com/tobrun/flutter-mapbox-gl/pull/237) +* Add support for custom font stackn in symbol options [#359](https://github.com/tobrun/flutter-mapbox-gl/pull/359) +* Basic ImageSource Support [#409](https://github.com/tobrun/flutter-mapbox-gl/pull/409) +* Get meters per pixel at latitude [#416](https://github.com/tobrun/flutter-mapbox-gl/pull/416) + +## 0.8.0, August 22, 2020 +- implementation of feature querying [#177](https://github.com/tobrun/flutter-mapbox-gl/pull/177) +- Batch create/delete of symbols [#279](https://github.com/tobrun/flutter-mapbox-gl/pull/279) +- Add multi map support [#315](https://github.com/tobrun/flutter-mapbox-gl/pull/315) +- Add line#getGeometry and symbol#getGeometry [#281](https://github.com/tobrun/flutter-mapbox-gl/pull/281) + ## 0.7.0 -- Initial version \ No newline at end of file +- Initial version 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 0abe3163a..f5c1ed938 100644 --- a/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart +++ b/mapbox_gl_platform_interface/lib/mapbox_gl_platform_interface.dart @@ -15,5 +15,7 @@ part 'src/line.dart'; part 'src/location.dart'; part 'src/method_channel_mapbox_gl.dart'; part 'src/symbol.dart'; +part 'src/fill.dart'; part 'src/ui.dart'; part 'src/mapbox_gl_platform_interface.dart'; + diff --git a/mapbox_gl_platform_interface/lib/src/camera.dart b/mapbox_gl_platform_interface/lib/src/camera.dart index 747086ddf..bbf756944 100644 --- a/mapbox_gl_platform_interface/lib/src/camera.dart +++ b/mapbox_gl_platform_interface/lib/src/camera.dart @@ -108,15 +108,21 @@ class CameraUpdate { return CameraUpdate._(['newLatLng', latLng.toJson()]); } + /// Returns a camera update that transforms the camera so that the specified /// geographical bounding box is centered in the map view at the greatest - /// possible zoom level. A non-zero [padding] insets the bounding box from the - /// map view's edges. The camera's new tilt and bearing will both be 0.0. - static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { + /// possible zoom level. A non-zero [left], [top], [right] and [bottom] padding + /// insets the bounding box from the map view's edges. + /// The camera's new tilt and bearing will both be 0.0. + static CameraUpdate newLatLngBounds(LatLngBounds bounds, + {double left = 0, double top = 0, double right = 0, double bottom = 0}) { return CameraUpdate._([ 'newLatLngBounds', bounds.toList(), - padding, + left, + top, + right, + bottom, ]); } diff --git a/mapbox_gl_platform_interface/lib/src/fill.dart b/mapbox_gl_platform_interface/lib/src/fill.dart new file mode 100644 index 000000000..01cc709ce --- /dev/null +++ b/mapbox_gl_platform_interface/lib/src/fill.dart @@ -0,0 +1,89 @@ +// 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. + +part of mapbox_gl_platform_interface; + +class Fill { + @visibleForTesting + Fill(this._id, this.options, [this._data]); + + /// A unique identifier for this fill. + /// + /// The identifier is an arbitrary unique string. + final String _id; + String get id => _id; + + final Map _data; + Map get data => _data; + + /// The fill configuration options most recently applied programmatically + /// via the map controller. + /// + /// 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; +} + +/// Configuration options for [Fill] instances. +/// +/// When used to change configuration, null values will be interpreted as +/// "do not change this configuration option". +class FillOptions { + /// Creates a set of fill configuration options. + /// + /// By default, every non-specified field is null, meaning no desire to change + /// fill defaults or current configuration. + const FillOptions({ + this.fillOpacity, + this.fillColor, + this.fillOutlineColor, + this.fillPattern, + this.geometry, + this.draggable + }); + + final double fillOpacity; + final String fillColor; + final String fillOutlineColor; + final String fillPattern; + final List> geometry; + final bool draggable; + + static const FillOptions defaultOptions = FillOptions(); + + FillOptions copyWith(FillOptions changes) { + if (changes == null) { + return this; + } + return FillOptions( + fillOpacity: changes.fillOpacity ?? fillOpacity, + fillColor: changes.fillColor ?? fillColor, + fillOutlineColor: changes.fillOutlineColor ?? fillOutlineColor, + fillPattern: changes.fillPattern ?? fillPattern, + geometry: changes.geometry ?? geometry, + draggable: changes.draggable ?? draggable, + ); + } + + dynamic toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('fillOpacity', fillOpacity); + addIfPresent('fillColor', fillColor); + addIfPresent('fillOutlineColor', fillOutlineColor); + addIfPresent('fillPattern', fillPattern); + addIfPresent('geometry', + geometry?.map((List latLngList) => latLngList.map((LatLng latLng) => latLng.toJson())?.toList())?.toList()); + addIfPresent('draggable', draggable); + return json; + } +} \ No newline at end of file diff --git a/mapbox_gl_platform_interface/lib/src/location.dart b/mapbox_gl_platform_interface/lib/src/location.dart index 617943453..edd56e973 100644 --- a/mapbox_gl_platform_interface/lib/src/location.dart +++ b/mapbox_gl_platform_interface/lib/src/location.dart @@ -103,3 +103,89 @@ class LatLngBounds { @override int get hashCode => hashValues(southwest, northeast); } + +/// A geographical area representing a non-aligned quadrilateral +/// This class does not wrap values to the world bounds +class LatLngQuad { + LatLngQuad({@required this.topLeft, @required this.topRight, @required this.bottomRight, @required this.bottomLeft}) + : assert(topLeft != null), + assert(topRight != null), + assert(bottomRight != null), + assert(bottomLeft != null); + + final LatLng topLeft; + + final LatLng topRight; + + final LatLng bottomRight; + + final LatLng bottomLeft; + + dynamic toList() { + return [topLeft.toJson(), topRight.toJson(), bottomRight.toJson(), bottomLeft.toJson()]; + } + + @visibleForTesting + static LatLngQuad fromList(dynamic json) { + if (json == null) { + return null; + } + return LatLngQuad( + topLeft: LatLng._fromJson(json[0]), + topRight: LatLng._fromJson(json[1]), + bottomRight: LatLng._fromJson(json[2]), + bottomLeft: LatLng._fromJson(json[3]), + ); + } + + @override + String toString() { + return '$runtimeType($topLeft, $topRight, $bottomRight, $bottomLeft)'; + } + + @override + bool operator ==(Object o) { + return o is LatLngQuad && + o.topLeft == topLeft && + o.topRight == topRight && + o.bottomRight == bottomRight && + o.bottomLeft == bottomLeft; + } + + @override + int get hashCode => hashValues(topLeft, topRight, bottomRight, bottomLeft); + +} + +/// User's observed location +class UserLocation { + /// User's position in latitude and longitude + final LatLng position; + + /// User's altitude in meters + final double altitude; + + /// Direction user is traveling, measured in degrees + final double bearing; + + /// User's speed in meters per second + final double speed; + + /// The radius of uncertainty for the location, measured in meters + final double horizontalAccuracy; + + /// Accuracy of the altitude measurement, in meters + final double verticalAccuracy; + + /// Time the user's location was observed + final DateTime timestamp; + + const UserLocation( + {@required this.position, + @required this.altitude, + @required this.bearing, + @required this.speed, + @required this.horizontalAccuracy, + @required this.verticalAccuracy, + @required this.timestamp}); +} \ No newline at end of file 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 da022c739..abb7f13e6 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 @@ -34,6 +34,9 @@ abstract class MapboxGlPlatform { final ArgumentCallbacks onCircleTappedPlatform = ArgumentCallbacks(); + final ArgumentCallbacks onFillTappedPlatform = + ArgumentCallbacks(); + final ArgumentCallbacks onCameraMoveStartedPlatform = ArgumentCallbacks(); @@ -60,6 +63,8 @@ abstract class MapboxGlPlatform { ArgumentCallbacks(); final ArgumentCallbacks onMapIdlePlatform = ArgumentCallbacks(); + + final ArgumentCallbacks onUserLocationUpdatedPlatform = ArgumentCallbacks(); Future initPlatform(int id) async { throw UnimplementedError('initPlatform() has not been implemented.'); @@ -160,6 +165,18 @@ abstract class MapboxGlPlatform { throw UnimplementedError('removeCircle() has not been implemented.'); } + Future addFill(FillOptions options, [Map data]) async { + throw UnimplementedError('addFill() has not been implemented.'); + } + + FutureupdateFill(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 queryRenderedFeatures( Point point, List layerIds, List filter) async { throw UnimplementedError( @@ -210,4 +227,36 @@ abstract class MapboxGlPlatform { throw UnimplementedError( 'setSymbolTextIgnorePlacement() has not been implemented.'); } + + Future addImageSource(String name, Uint8List bytes, + LatLngQuad coordinates) async { + throw UnimplementedError('addImageSource() has not been implemented.'); + } + + Future removeImageSource(String name) async { + throw UnimplementedError('removeImageSource() has not been implemented.'); + } + + Future addLayer(String name, String sourceId) async { + throw UnimplementedError('addLayer() has not been implemented.'); + } + + Future removeLayer(String name) async { + throw UnimplementedError('removeLayer() has not been implemented.'); + } + + Future toScreenLocation(LatLng latLng) async{ + throw UnimplementedError( + 'toScreenLocation() 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.'); + } } 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 e1513daf7..0417a6b87 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 @@ -29,6 +29,12 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { onCircleTappedPlatform(circleId); } break; + case 'fill#onTap': + final String fillId = call.arguments['fill']; + if (fillId != null) { + onFillTappedPlatform(fillId); + } + break; case 'camera#onMoveStarted': onCameraMoveStartedPlatform(null); break; @@ -70,6 +76,20 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { case 'map#onIdle': onMapIdlePlatform(null); break; + case 'map#onUserLocationUpdated': + final dynamic userLocation = call.arguments['userLocation']; + if (onUserLocationUpdatedPlatform != null) { + onUserLocationUpdatedPlatform(UserLocation( + position: LatLng(userLocation['position'][0], userLocation['position'][1]), + altitude: userLocation['altitude'], + bearing: userLocation['bearing'], + speed: userLocation['speed'], + horizontalAccuracy: userLocation['horizontalAccuracy'], + verticalAccuracy: userLocation['verticalAccuracy'], + timestamp: DateTime.fromMillisecondsSinceEpoch(userLocation['timestamp']) + )); + } + break; default: throw MissingPluginException(); } @@ -182,23 +202,22 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future> addSymbols(List options, [List data]) async { + 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( + 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(); + Symbol(id, options.elementAt(i), + data != null && data.length > i ? data.elementAt(i) : null))) + .values + .toList(); return symbols; } @@ -212,7 +231,7 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future getSymbolLatLng(Symbol symbol) async{ + Future getSymbolLatLng(Symbol symbol) async { Map mapLatLng = await _channel.invokeMethod('symbol#getGeometry', { 'symbol': symbol._id, @@ -249,7 +268,7 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { } @override - Future> getLineLatLngs(Line line) async{ + Future> getLineLatLngs(Line line) async { List latLngList = await _channel.invokeMethod('line#getGeometry', { 'line': line._id, @@ -303,6 +322,32 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { }); } + @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 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 queryRenderedFeatures( Point point, List layerIds, List filter) async { @@ -453,4 +498,95 @@ class MethodChannelMapboxGl extends MapboxGlPlatform { return new Future.error(e); } } + + @override + Future addImageSource(String name, Uint8List bytes, + LatLngQuad coordinates) async { + try { + return await _channel.invokeMethod('style#addImageSource', { + "name": name, + "bytes": bytes, + "length": bytes.length, + "coordinates": coordinates.toList() + }); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future toScreenLocation(LatLng latLng) async { + try { + var screenPosMap = await _channel + .invokeMethod('map#toScreenLocation', { + 'latitude': latLng.latitude, + 'longitude':latLng.longitude, + }); + return Point(screenPosMap['x'], screenPosMap['y']); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future removeImageSource(String name) async { + try { + return await _channel.invokeMethod('style#removeImageSource', { + "name": name + }); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future addLayer(String name, String sourceId) async { + try { + return await _channel.invokeMethod('style#addLayer', { + "name": name, + "sourceId": sourceId + }); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future removeLayer(String name) async { + try { + return await _channel.invokeMethod('style#removeLayer', { + "name": name + }); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future toLatLng(Point screenLocation) async { + try { + var latLngMap = await _channel + .invokeMethod('map#toLatLng', { + 'x': screenLocation.x, + 'y':screenLocation.y, + }); + return LatLng(latLngMap['latitude'], latLngMap['longitude']); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + @override + Future getMetersPerPixelAtLatitude(double latitude) async{ + try { + var latLngMap = await _channel + .invokeMethod('map#getMetersPerPixelAtLatitude', { + 'latitude': latitude, + }); + return latLngMap['metersperpixel']; + } on PlatformException catch (e) { + return new Future.error(e); + } + } + } diff --git a/mapbox_gl_platform_interface/lib/src/symbol.dart b/mapbox_gl_platform_interface/lib/src/symbol.dart index 4f4ea50d4..9409cb069 100644 --- a/mapbox_gl_platform_interface/lib/src/symbol.dart +++ b/mapbox_gl_platform_interface/lib/src/symbol.dart @@ -49,6 +49,7 @@ class SymbolOptions { this.iconRotate, this.iconOffset, this.iconAnchor, + this.fontNames, this.textField, this.textSize, this.textMaxWidth, @@ -78,6 +79,7 @@ class SymbolOptions { final double iconRotate; final Offset iconOffset; final String iconAnchor; + final List fontNames; final String textField; final double textSize; final double textMaxWidth; @@ -113,6 +115,7 @@ class SymbolOptions { iconRotate: changes.iconRotate ?? iconRotate, iconOffset: changes.iconOffset ?? iconOffset, iconAnchor: changes.iconAnchor ?? iconAnchor, + fontNames: changes.fontNames ?? fontNames, textField: changes.textField ?? textField, textSize: changes.textSize ?? textSize, textMaxWidth: changes.textMaxWidth ?? textMaxWidth, @@ -152,6 +155,7 @@ class SymbolOptions { addIfPresent('iconRotate', iconRotate); addIfPresent('iconOffset', _offsetToJson(iconOffset)); addIfPresent('iconAnchor', iconAnchor); + addIfPresent('fontNames', fontNames); addIfPresent('textField', textField); addIfPresent('textSize', textSize); addIfPresent('textMaxWidth', textMaxWidth); diff --git a/mapbox_gl_platform_interface/pubspec.yaml b/mapbox_gl_platform_interface/pubspec.yaml index 62d650818..5b78d3a4a 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.7.0 +version: 0.9.0 homepage: https://github.com/tobrun/flutter-mapbox-gl dependencies: @@ -10,4 +10,4 @@ dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + flutter: ">=1.10.0 <2.0.0" diff --git a/mapbox_gl_web/CHANGELOG.md b/mapbox_gl_web/CHANGELOG.md index c15887ba4..0a04d5c80 100644 --- a/mapbox_gl_web/CHANGELOG.md +++ b/mapbox_gl_web/CHANGELOG.md @@ -1,2 +1,16 @@ +## 0.9.0, October 24. 2020 +* Breaking change: CameraUpdate.newLatLngBounds() now supports setting different padding values for left, top, right, bottom with default of 0 for all. Implementations using the old approach with only one padding value for all edges have to be updated. [#382](https://github.com/tobrun/flutter-mapbox-gl/pull/382) +* web:ignore myLocationTrackingMode if myLocationEnabled is false [#363](https://github.com/tobrun/flutter-mapbox-gl/pull/363) +* Add methods to access projection [#380](https://github.com/tobrun/flutter-mapbox-gl/pull/380) +* Listen to OnUserLocationUpdated to provide user location to app [#237](https://github.com/tobrun/flutter-mapbox-gl/pull/237) +* Get meters per pixel at latitude [#416](https://github.com/tobrun/flutter-mapbox-gl/pull/416) + +## 0.8.0, August 22, 2020 +- implementation of feature querying [#177](https://github.com/tobrun/flutter-mapbox-gl/pull/177) +- Allow setting accesstoken in flutter [#321](https://github.com/tobrun/flutter-mapbox-gl/pull/321) +- Batch create/delete of symbols [#279](https://github.com/tobrun/flutter-mapbox-gl/pull/279) +- Set dependencies from git [#319](https://github.com/tobrun/flutter-mapbox-gl/pull/319) +- Add multi map support [#315](https://github.com/tobrun/flutter-mapbox-gl/pull/315) + ## 0.7.0 -- Initial version \ No newline at end of file +- Initial version diff --git a/mapbox_gl_web/lib/mapbox_gl_web.dart b/mapbox_gl_web/lib/mapbox_gl_web.dart index df5ca795f..1ee7b279a 100644 --- a/mapbox_gl_web/lib/mapbox_gl_web.dart +++ b/mapbox_gl_web/lib/mapbox_gl_web.dart @@ -5,6 +5,7 @@ import 'dart:async'; // ignore_for_file: avoid_web_libraries_in_flutter import 'dart:html'; import 'dart:js'; +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; diff --git a/mapbox_gl_web/lib/src/convert.dart b/mapbox_gl_web/lib/src/convert.dart index 376cdb1a0..24ac53441 100644 --- a/mapbox_gl_web/lib/src/convert.dart +++ b/mapbox_gl_web/lib/src/convert.dart @@ -45,6 +45,7 @@ class Convert { sink.setMyLocationEnabled(options['myLocationEnabled']); } if (options.containsKey('myLocationTrackingMode')) { + //Should not be invoked before sink.setMyLocationEnabled() sink.setMyLocationTrackingMode(options['myLocationTrackingMode']); } if (options.containsKey('myLocationRenderMode')) { @@ -90,7 +91,10 @@ class Convert { ); case 'newLatLngBounds': final bounds = json[1]; - final padding = json[2]; + final left = json[2]; + final top = json[3]; + final right = json[4]; + final bottom = json[5]; final camera = mapboxMap.cameraForBounds( LngLatBounds( LngLat(bounds[0][1], bounds[0][0]), @@ -98,10 +102,10 @@ class Convert { ), { 'padding': { - 'top': padding, - 'bottom': padding, - 'left': padding, - 'right': padding + 'top': top, + 'bottom': bottom, + 'left': left, + 'right': right, } }); return camera; diff --git a/mapbox_gl_web/lib/src/mapbox_map_controller.dart b/mapbox_gl_web/lib/src/mapbox_map_controller.dart index 7f970fcac..4930247ff 100644 --- a/mapbox_gl_web/lib/src/mapbox_map_controller.dart +++ b/mapbox_gl_web/lib/src/mapbox_map_controller.dart @@ -400,6 +400,7 @@ class MapboxMapController extends MapboxGlPlatform ); _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, timestamp: DateTime.fromMillisecondsSinceEpoch(e.timestamp))); }); _geolocateControl.on('trackuserlocationstart', (_) { _onCameraTrackingChanged(true); @@ -542,6 +543,10 @@ class MapboxMapController extends MapboxGlPlatform @override void setMyLocationTrackingMode(int myLocationTrackingMode) { + if(_geolocateControl==null){ + //myLocationEnabled is false, ignore myLocationTrackingMode + return; + } if (myLocationTrackingMode == 0) { _addGeolocateControl(trackUserLocation: false); } else { @@ -611,4 +616,24 @@ class MapboxMapController extends MapboxGlPlatform _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 toLatLng(Point screenLocation) async { + var lngLat = _map.unproject(mapbox.Point(screenLocation.x, screenLocation.y)); + return LatLng(lngLat.lat, lngLat.lng); + } + + @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/pubspec.yaml b/mapbox_gl_web/pubspec.yaml index d5b3ed38a..40169b757 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.7.0 +version: 0.9.0 homepage: https://github.com/tobrun/flutter-mapbox-gl flutter: @@ -29,4 +29,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" \ No newline at end of file + flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/pubspec.lock b/pubspec.lock index 5edf668e7..1bf49dc00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -66,7 +66,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.13" + version: "2.1.14" js: dependency: transitive description: @@ -85,20 +85,16 @@ packages: dependency: "direct main" description: path: mapbox_gl_platform_interface - ref: HEAD - resolved-ref: fa9dac81b6d0f3a5f01688a6695ee938b71874e5 - url: "https://github.com/tobrun/flutter-mapbox-gl.git" - source: git - version: "0.7.0" + relative: true + source: path + version: "0.8.0" mapbox_gl_web: dependency: "direct main" description: path: mapbox_gl_web - ref: HEAD - resolved-ref: fa9dac81b6d0f3a5f01688a6695ee938b71874e5 - url: "https://github.com/tobrun/flutter-mapbox-gl.git" - source: git - version: "0.7.0" + relative: true + source: path + version: "0.8.0" meta: dependency: transitive description: @@ -145,7 +141,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.5.1" sdks: dart: ">=2.9.0-14.0.dev <3.0.0" flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index bbbd88de0..679724b64 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.7.0 +version: 0.9.0 homepage: https://github.com/tobrun/flutter-mapbox-gl dependencies: @@ -25,7 +25,7 @@ flutter: pluginClass: MapboxMapsPlugin web: default_package: mapbox_gl_web - + environment: sdk: ">=2.1.0 <3.0.0" # Flutter versions prior to 1.10 did not support the flutter.plugin.platforms map.