diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java index c15fc69be..0a99ecc31 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java @@ -7,6 +7,8 @@ import android.content.Context; import android.graphics.Point; import android.util.DisplayMetrics; +import android.util.Log; +import org.maplibre.android.location.engine.LocationEngineRequest; import org.maplibre.geojson.Polygon; import org.maplibre.android.camera.CameraPosition; import org.maplibre.android.camera.CameraUpdate; @@ -133,6 +135,17 @@ static LatLngBounds toLatLngBounds(Object o) { return builder.build(); } +static LocationEngineRequest toLocationEngineRequest(Object o) { + if (o == null) { + return null; + } + List data = toList(o); + return new LocationEngineRequest.Builder(toInt(data.get(0))) + .setPriority(toInt(data.get(1))) + .setDisplacement(toInt(data.get(2))) + .build(); +} + static List toLatLngList(Object o, boolean flippedOrder) { if (o == null) { return null; @@ -208,6 +221,12 @@ static String toString(Object o) { static void interpretMapLibreMapOptions(Object o, MapLibreMapOptionsSink sink, Context context) { final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final Map data = toMap(o); + + final Object locationEngineProperties = data.get("locationEngineProperties"); + if (locationEngineProperties != null) { + final List locationEnginePropertiesList = toList(locationEngineProperties); + sink.setLocationEngineProperties(toLocationEngineRequest(locationEnginePropertiesList)); + } final Object cameraTargetBounds = data.get("cameraTargetBounds"); if (cameraTargetBounds != null) { final List targetData = toList(cameraTargetBounds); diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreGPSLocationEngine.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreGPSLocationEngine.java new file mode 100644 index 000000000..2dba9674f --- /dev/null +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreGPSLocationEngine.java @@ -0,0 +1,140 @@ +package org.maplibre.maplibregl; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import org.maplibre.android.location.engine.LocationEngineCallback; +import org.maplibre.android.location.engine.LocationEngineRequest; +import org.maplibre.android.location.engine.LocationEngineResult; +import org.maplibre.android.location.engine.LocationEngineImpl; + + +public class MapLibreGPSLocationEngine implements LocationEngineImpl { + private static final String TAG = "GPSLocationEngine"; + final LocationManager locationManager; + + String currentProvider = LocationManager.PASSIVE_PROVIDER; + + public MapLibreGPSLocationEngine(@NonNull Context context) { + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + @NonNull + @Override + public LocationListener createListener(LocationEngineCallback callback) { + return new AndroidLocationEngineCallbackTransport(callback); + } + + @Override + public void getLastLocation(@NonNull LocationEngineCallback callback) + throws SecurityException { + Location lastLocation = getLastLocationFor(currentProvider); + if (lastLocation != null) { + callback.onSuccess(LocationEngineResult.create(lastLocation)); + return; + } + + for (String provider : locationManager.getAllProviders()) { + lastLocation = getLastLocationFor(provider); + if (lastLocation != null) { + callback.onSuccess(LocationEngineResult.create(lastLocation)); + return; + } + } + callback.onFailure(new Exception("Last location unavailable")); + } + + @SuppressLint("MissingPermission") + Location getLastLocationFor(String provider) throws SecurityException { + Location location = null; + try { + location = locationManager.getLastKnownLocation(provider); + } catch (IllegalArgumentException iae) { + Log.e(TAG, iae.toString()); + } + return location; + } + + @SuppressLint("MissingPermission") + @Override + public void requestLocationUpdates(@NonNull LocationEngineRequest request, + @NonNull LocationListener listener, + @Nullable Looper looper) throws SecurityException { + currentProvider = getBestProvider(request.getPriority()); + locationManager.requestLocationUpdates(currentProvider, request.getInterval(), request.getDisplacement(), + listener, looper); + } + + @SuppressLint("MissingPermission") + @Override + public void requestLocationUpdates(@NonNull LocationEngineRequest request, + @NonNull PendingIntent pendingIntent) throws SecurityException { + currentProvider = getBestProvider(request.getPriority()); + locationManager.requestLocationUpdates(currentProvider, request.getInterval(), + request.getDisplacement(), pendingIntent); + } + + @SuppressLint("MissingPermission") + @Override + public void removeLocationUpdates(@NonNull LocationListener listener) { + if (listener != null) { + locationManager.removeUpdates(listener); + } + } + + @Override + public void removeLocationUpdates(PendingIntent pendingIntent) { + if (pendingIntent != null) { + locationManager.removeUpdates(pendingIntent); + } + } + + private String getBestProvider(int priority) { + String provider = null; + if (priority != LocationEngineRequest.PRIORITY_NO_POWER) { + provider = LocationManager.GPS_PROVIDER; + } + return provider != null ? provider : LocationManager.PASSIVE_PROVIDER; + } + + + @VisibleForTesting + static final class AndroidLocationEngineCallbackTransport implements LocationListener { + private final LocationEngineCallback callback; + + AndroidLocationEngineCallbackTransport(LocationEngineCallback callback) { + this.callback = callback; + } + + @Override + public void onLocationChanged(Location location) { + callback.onSuccess(LocationEngineResult.create(location)); + } + + @Override + public void onStatusChanged(String s, int i, Bundle bundle) { + // noop + } + + @Override + public void onProviderEnabled(String s) { + // noop + } + + @Override + public void onProviderDisabled(String s) { + callback.onFailure(new Exception("Current provider disabled")); + } + } +} diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java index 96fae2af7..e283eb0c4 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import org.maplibre.android.camera.CameraPosition; import org.maplibre.android.geometry.LatLngBounds; +import org.maplibre.android.location.engine.LocationEngineRequest; import org.maplibre.android.maps.MapLibreMapOptions; import io.flutter.plugin.common.BinaryMessenger; @@ -23,6 +24,7 @@ class MapLibreMapBuilder implements MapLibreMapOptionsSink { private int myLocationRenderMode = 0; private String styleString = ""; private LatLngBounds bounds = null; + private LocationEngineRequest locationEngineRequest = null; MapLibreMapController build( int id, @@ -43,6 +45,10 @@ MapLibreMapController build( controller.setCameraTargetBounds(bounds); } + if(null != locationEngineRequest ){ + controller.setLocationEngineProperties(locationEngineRequest); + } + return controller; } @@ -206,4 +212,9 @@ public void setAttributionButtonMargins(int x, int y) { public void setDragEnabled(boolean enabled) { this.dragEnabled = enabled; } + + @Override + public void setLocationEngineProperties(@NonNull LocationEngineRequest locationEngineRequest) { + this.locationEngineRequest = locationEngineRequest; + } } diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java index c856ed80c..016070d2e 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java @@ -32,8 +32,13 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; + import org.maplibre.android.gestures.AndroidGesturesManager; import org.maplibre.android.gestures.MoveGestureDetector; +import org.maplibre.android.location.engine.LocationEngine; +import org.maplibre.android.location.engine.LocationEngineDefault; +import org.maplibre.android.location.engine.LocationEngineProxy; +import org.maplibre.android.location.engine.LocationEngineRequest; import org.maplibre.geojson.Feature; import org.maplibre.geojson.FeatureCollection; import org.maplibre.android.camera.CameraPosition; @@ -1863,6 +1868,20 @@ public void setCameraTargetBounds(LatLngBounds bounds) { this.bounds = bounds; } + @Override + public void setLocationEngineProperties(LocationEngineRequest locationEngineRequest){ + if(locationComponent != null){ + if(locationEngineRequest.getPriority() == LocationEngineRequest.PRIORITY_HIGH_ACCURACY){ + locationComponent.setLocationEngine(new LocationEngineProxy( + new MapLibreGPSLocationEngine(context))); + } else { + locationComponent.setLocationEngine( + LocationEngineDefault.INSTANCE.getDefaultLocationEngine(context)); + } + locationComponent.setLocationEngineRequest(locationEngineRequest); + } + } + @Override public void setCompassEnabled(boolean compassEnabled) { mapLibreMap.getUiSettings().setCompassEnabled(compassEnabled); diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt index 49fa1647f..11a29254c 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt @@ -4,6 +4,7 @@ package org.maplibre.maplibregl import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.location.engine.LocationEngineRequest /** Receiver of MapLibreMap configuration options. */ internal interface MapLibreMapOptionsSink { @@ -42,4 +43,6 @@ internal interface MapLibreMapOptionsSink { fun setAttributionButtonGravity(gravity: Int) fun setAttributionButtonMargins(x: Int, y: Int) + + fun setLocationEngineProperties(locationEngineRequest: LocationEngineRequest) } \ No newline at end of file diff --git a/maplibre_gl/lib/maplibre_gl.dart b/maplibre_gl/lib/maplibre_gl.dart index ee1312ae2..988a97cab 100644 --- a/maplibre_gl/lib/maplibre_gl.dart +++ b/maplibre_gl/lib/maplibre_gl.dart @@ -66,6 +66,9 @@ export 'package:maplibre_gl_platform_interface/maplibre_gl_platform_interface.da LatLngQuad, Line, LineOptions, + LocationEngineAndroidProperties, + LocationEnginePlatforms, + LocationPriority, MapLibreMethodChannel, MapLibrePlatform, MinMaxZoomPreference, diff --git a/maplibre_gl/lib/src/maplibre_map.dart b/maplibre_gl/lib/src/maplibre_map.dart index 15a9aebf1..f130c7d72 100644 --- a/maplibre_gl/lib/src/maplibre_map.dart +++ b/maplibre_gl/lib/src/maplibre_map.dart @@ -20,6 +20,7 @@ class MapLibreMap extends StatefulWidget { this.styleString = MapLibreStyles.demo, this.onMapCreated, this.onStyleLoadedCallback, + this.locationEnginePlatforms = LocationEnginePlatforms.defaultPlatform, this.gestureRecognizers, this.compassEnabled = true, this.cameraTargetBounds = CameraTargetBounds.unbounded, @@ -67,6 +68,10 @@ class MapLibreMap extends StatefulWidget { assert(annotationOrder.length <= 4), assert(annotationConsumeTapEvents.length > 0); + /// The properties for the platform-specific location engine. + /// Only has an impact if [myLocationEnabled] is set to true. + final LocationEnginePlatforms locationEnginePlatforms; + /// Defines the layer order of annotations displayed on map /// /// Any annotation type can only be contained once, so 0 to 4 types @@ -338,29 +343,30 @@ class _MapLibreMapState extends State { /// When used to change configuration, null values will be interpreted as /// "do not change this configuration option". class _MapLibreMapOptions { - _MapLibreMapOptions({ - this.compassEnabled, - this.cameraTargetBounds, - this.styleString, - this.minMaxZoomPreference, - required this.rotateGesturesEnabled, - required this.scrollGesturesEnabled, - required this.tiltGesturesEnabled, - required this.zoomGesturesEnabled, - required this.doubleClickZoomEnabled, - this.trackCameraPosition, - this.myLocationEnabled, - this.myLocationTrackingMode, - this.myLocationRenderMode, - this.logoViewMargins, - this.compassViewPosition, - this.compassViewMargins, - this.attributionButtonPosition, - this.attributionButtonMargins, - }); + _MapLibreMapOptions( + {this.compassEnabled, + this.cameraTargetBounds, + this.styleString, + this.minMaxZoomPreference, + required this.rotateGesturesEnabled, + required this.scrollGesturesEnabled, + required this.tiltGesturesEnabled, + required this.zoomGesturesEnabled, + required this.doubleClickZoomEnabled, + this.trackCameraPosition, + this.myLocationEnabled, + this.myLocationTrackingMode, + this.myLocationRenderMode, + this.logoViewMargins, + this.compassViewPosition, + this.compassViewMargins, + this.attributionButtonPosition, + this.attributionButtonMargins, + this.locationEnginePlatforms}); _MapLibreMapOptions.fromWidget(MapLibreMap map) : this( + locationEnginePlatforms: map.locationEnginePlatforms, compassEnabled: map.compassEnabled, cameraTargetBounds: map.cameraTargetBounds, styleString: map.styleString, @@ -418,6 +424,8 @@ class _MapLibreMapOptions { final Point? attributionButtonMargins; + final LocationEnginePlatforms? locationEnginePlatforms; + final _gestureGroup = { 'rotateGesturesEnabled', 'scrollGesturesEnabled', @@ -464,6 +472,7 @@ class _MapLibreMapOptions { addIfNonNull('attributionButtonPosition', attributionButtonPosition?.index); addIfNonNull( 'attributionButtonMargins', pointToArray(attributionButtonMargins)); + addIfNonNull('locationEngineProperties', locationEnginePlatforms?.toList()); return optionsMap; } diff --git a/maplibre_gl_platform_interface/lib/maplibre_gl_platform_interface.dart b/maplibre_gl_platform_interface/lib/maplibre_gl_platform_interface.dart index 6425cb736..e86d2c3de 100644 --- a/maplibre_gl_platform_interface/lib/maplibre_gl_platform_interface.dart +++ b/maplibre_gl_platform_interface/lib/maplibre_gl_platform_interface.dart @@ -1,6 +1,7 @@ library maplibre_gl_platform_interface; import 'dart:async'; +import 'dart:io'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -20,3 +21,4 @@ part 'src/fill.dart'; part 'src/ui.dart'; part 'src/maplibre_gl_platform_interface.dart'; part 'src/source_properties.dart'; +part 'src/location_engine_properties.dart'; diff --git a/maplibre_gl_platform_interface/lib/src/location_engine_properties.dart b/maplibre_gl_platform_interface/lib/src/location_engine_properties.dart new file mode 100644 index 000000000..79aba4cd0 --- /dev/null +++ b/maplibre_gl_platform_interface/lib/src/location_engine_properties.dart @@ -0,0 +1,103 @@ +part of '../maplibre_gl_platform_interface.dart'; + +/// iOS is not supported at the moment. +@immutable +class LocationEnginePlatforms { + /// The properties for the Android platform. + final LocationEngineAndroidProperties androidPlatform; + + /// If [androidPlatform] is not provided, it defaults to [LocationEngineAndroidProperties.defaultProperties]. + const LocationEnginePlatforms({ + this.androidPlatform = LocationEngineAndroidProperties.defaultProperties, + }); + + static const LocationEnginePlatforms defaultPlatform = + LocationEnginePlatforms(); + + List toList() { + if (Platform.isAndroid) return androidPlatform.toList(); + return []; + } +} + +@immutable +class LocationEngineAndroidProperties { + /// The interval in milliseconds for location updates. + final int interval; + + /// The minimum displacement in meters for location updates. + final int displacement; + + /// [LocationPriority.highAccuracy] only uses native GPS provider + /// [LocationPriority.balanced] uses a fused provider (network + GPS)-> better quality indoor + /// [LocationPriority.lowPower] only uses network provider + /// [LocationPriority.noPower] only receives location updates when another clients request them + /// + final LocationPriority priority; + + const LocationEngineAndroidProperties({ + required this.interval, + required this.displacement, + required this.priority, + }); + + static const LocationEngineAndroidProperties defaultProperties = + LocationEngineAndroidProperties( + interval: 1000, + displacement: 0, + priority: LocationPriority.balanced, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocationEngineAndroidProperties && + runtimeType == other.runtimeType && + interval == other.interval && + displacement == other.displacement && + priority == other.priority); + + @override + int get hashCode => + interval.hashCode ^ displacement.hashCode ^ priority.hashCode; + + @override + String toString() { + return 'LocationEngineAndroidProperties{ interval: $interval, displacement: $displacement, priority: $priority }'; + } + + LocationEngineAndroidProperties copyWith({ + int? interval, + int? displacement, + LocationPriority? priority, + }) { + return LocationEngineAndroidProperties( + interval: interval ?? this.interval, + displacement: displacement ?? this.displacement, + priority: priority ?? this.priority, + ); + } + + List toList() { + return [ + interval, + priority.index, + displacement, + ]; + } +} + +/// An enum representing the priority for location accuracy and power usage. +enum LocationPriority { + /// High accuracy, may consume more power. + highAccuracy, + + /// Balanced accuracy and power usage. + balanced, + + /// Low power usage, may be less accurate. + lowPower, + + /// No power usage, only receive location updates when other clients request them. + noPower, +}