From 6dca8cdc32da2f421608fec91da67ee126342415 Mon Sep 17 00:00:00 2001 From: Julian Bissekkou <36447137+JulianBissekkou@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:09:08 +0200 Subject: [PATCH] 53-support-basic-current-location-indicator (#69) * catch exceptions while parsing and fix poliline on mobile * add missing return * initial draft on iOS * add symbol methods to controller * parse symbol * implement useCourseSymbolOnMovement * implement new type of location display data source * finish iOS location indicator impl * implement android * fix bug for manual location source on android * add try catch * update docs --------- Co-authored-by: Julian Bissekkou Co-authored-by: sbergmair --- arcgis_map_sdk/lib/arcgis_map_sdk.dart | 1 + .../lib/src/arcgis_location_display.dart | 79 +++++++ .../lib/src/arcgis_map_controller.dart | 19 +- .../arcgis_map_sdk_android/ArcgisMapView.kt | 222 +++++++++++++++--- .../ManualLocationDataSource.kt | 27 +++ .../model/UserPosition.kt | 8 + .../model/ViewPadding.kt | 4 +- .../util/GraphicsParser.kt | 13 +- .../ios/Classes/ArcgisMapView.swift | 165 ++++++++++--- .../ios/Classes/GraphicsParser.swift | 6 +- .../Classes/ManualLocationDataSource.swift | 22 ++ .../ios/Classes/Models/UserPosition.swift | 15 ++ .../src/method_channel_arcgis_map_plugin.dart | 66 ++++++ .../lib/src/model_extension.dart | 9 + .../arcgis_map_sdk_platform_interface.dart | 1 + .../arcgis_map_sdk_platform_interface.dart | 54 +++++ .../lib/src/models/user_position.dart | 15 ++ .../android/app/src/main/AndroidManifest.xml | 4 + example/ios/Podfile.lock | 8 +- example/ios/Runner/Info.plist | 4 + .../lib/location_indicator_example_page.dart | 187 +++++++++++++++ example/lib/main.dart | 15 +- example/pubspec.lock | 80 +++++++ example/pubspec.yaml | 1 + 24 files changed, 951 insertions(+), 74 deletions(-) create mode 100644 arcgis_map_sdk/lib/src/arcgis_location_display.dart create mode 100644 arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ManualLocationDataSource.kt create mode 100644 arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/UserPosition.kt create mode 100644 arcgis_map_sdk_ios/ios/Classes/ManualLocationDataSource.swift create mode 100644 arcgis_map_sdk_ios/ios/Classes/Models/UserPosition.swift create mode 100644 arcgis_map_sdk_platform_interface/lib/src/models/user_position.dart create mode 100644 example/lib/location_indicator_example_page.dart diff --git a/arcgis_map_sdk/lib/arcgis_map_sdk.dart b/arcgis_map_sdk/lib/arcgis_map_sdk.dart index 5f4dabec2..e0fd4729e 100644 --- a/arcgis_map_sdk/lib/arcgis_map_sdk.dart +++ b/arcgis_map_sdk/lib/arcgis_map_sdk.dart @@ -1,6 +1,7 @@ // ignore: unnecessary_library_directive library arcgis_map; +export 'package:arcgis_map_sdk/src/arcgis_location_display.dart'; export 'package:arcgis_map_sdk/src/arcgis_map_controller.dart'; export 'package:arcgis_map_sdk/src/arcgis_map_sdk.dart'; export 'package:arcgis_map_sdk/src/model/map_status.dart'; diff --git a/arcgis_map_sdk/lib/src/arcgis_location_display.dart b/arcgis_map_sdk/lib/src/arcgis_location_display.dart new file mode 100644 index 000000000..59fd3cf5d --- /dev/null +++ b/arcgis_map_sdk/lib/src/arcgis_location_display.dart @@ -0,0 +1,79 @@ +import 'package:arcgis_map_sdk_platform_interface/arcgis_map_sdk_platform_interface.dart'; + +/// The use case for manual location displays is relevant when the application +/// has its own location stream obtained from a different source, such as a geolocator, +/// with specific settings. +/// +/// Instead of relying on ArcGIS to create a location client that fetches the position +/// again, this use case involves processing the locations retrieved by the application +/// and displaying the exact location processed by the application. +/// +/// This approach is beneficial when the application needs to manage its own location data +/// independently, without relying on additional calls to fetch the location. +class ArcgisManualLocationDisplay extends ArcgisLocationDisplay { + @override + String get type => "manual"; + + ArcgisManualLocationDisplay({super.mapId}); + + Future updateLocation(UserPosition position) { + _assertAttached(); + return ArcgisMapPlatform.instance + .updateLocationDisplaySourcePositionManually( + _mapId!, + position, + ); + } +} + +class ArcgisLocationDisplay { + int? _mapId; + final String type = "system"; + + ArcgisLocationDisplay({int? mapId}) : _mapId = mapId; + + void attachToMap(int mapId) => _mapId = mapId; + + void deattachFromMap() => _mapId = null; + + Future startSource() { + _assertAttached(); + return ArcgisMapPlatform.instance.startLocationDisplayDataSource(_mapId!); + } + + Future stopSource() { + _assertAttached(); + return ArcgisMapPlatform.instance.stopLocationDisplayDataSource(_mapId!); + } + + Future setDefaultSymbol(Symbol symbol) { + _assertAttached(); + return ArcgisMapPlatform.instance + .setLocationDisplayDefaultSymbol(_mapId!, symbol); + } + + Future setAccuracySymbol(Symbol symbol) { + _assertAttached(); + return ArcgisMapPlatform.instance + .setLocationDisplayAccuracySymbol(_mapId!, symbol); + } + + Future setPingAnimationSymbol(Symbol symbol) { + _assertAttached(); + return ArcgisMapPlatform.instance + .setLocationDisplayPingAnimationSymbol(_mapId!, symbol); + } + + Future setUseCourseSymbolOnMovement(bool useCourseSymbol) { + _assertAttached(); + return ArcgisMapPlatform.instance + .setUseCourseSymbolOnMovement(_mapId!, useCourseSymbol); + } + + void _assertAttached() { + assert( + _mapId != null, + "LocationDisplay has not been attached to any map. Make sure to call ArcgisMapController.setLocationDisplay.", + ); + } +} diff --git a/arcgis_map_sdk/lib/src/arcgis_map_controller.dart b/arcgis_map_sdk/lib/src/arcgis_map_controller.dart index 7fce8c26e..dd65272a7 100644 --- a/arcgis_map_sdk/lib/src/arcgis_map_controller.dart +++ b/arcgis_map_sdk/lib/src/arcgis_map_controller.dart @@ -1,3 +1,4 @@ +import 'package:arcgis_map_sdk/src/arcgis_location_display.dart'; import 'package:arcgis_map_sdk/src/model/map_status.dart'; import 'package:arcgis_map_sdk_platform_interface/arcgis_map_sdk_platform_interface.dart'; import 'package:flutter/services.dart'; @@ -7,7 +8,7 @@ typedef MapStatusListener = void Function(MapStatus status); class ArcgisMapController { ArcgisMapController._({ required this.mapId, - }) { + }) : _locationDisplay = ArcgisLocationDisplay(mapId: mapId) { ArcgisMapPlatform.instance.setMethodCallHandler( mapId: mapId, onCall: _onCall, @@ -16,6 +17,10 @@ class ArcgisMapController { final int mapId; + late ArcgisLocationDisplay _locationDisplay; + + ArcgisLocationDisplay get locationDisplay => _locationDisplay; + final _listeners = []; MapStatus _mapStatus = MapStatus.unknown; @@ -184,6 +189,7 @@ class ArcgisMapController { } /// Adds a listener that gets notified if the map status changes. + /// The listener can be removed by calling the [VoidCallback] returned by this function. VoidCallback addStatusChangeListener(MapStatusListener listener) { _listeners.add(listener); return () => _listeners.removeWhere((l) => l == listener); @@ -303,4 +309,15 @@ class ArcgisMapController { List getVisibleGraphicIds() { return ArcgisMapPlatform.instance.getVisibleGraphicIds(mapId); } + + Future setLocationDisplay(ArcgisLocationDisplay locationDisplay) { + return ArcgisMapPlatform.instance + .setLocationDisplay(mapId, locationDisplay.type) + .whenComplete( + () { + _locationDisplay.deattachFromMap(); + _locationDisplay = locationDisplay..attachToMap(mapId); + }, + ); + } } diff --git a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt index 06bce86b6..5dc953b97 100644 --- a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt +++ b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt @@ -17,6 +17,8 @@ import com.esri.arcgisruntime.loadable.LoadStatus.LOADED import com.esri.arcgisruntime.loadable.LoadStatus.LOADING import com.esri.arcgisruntime.loadable.LoadStatus.NOT_LOADED import com.esri.arcgisruntime.loadable.LoadStatusChangedEvent +import com.esri.arcgisruntime.location.AndroidLocationDataSource +import com.esri.arcgisruntime.location.LocationDataSource import com.esri.arcgisruntime.mapping.ArcGISMap import com.esri.arcgisruntime.mapping.Basemap import com.esri.arcgisruntime.mapping.BasemapStyle @@ -25,10 +27,12 @@ import com.esri.arcgisruntime.mapping.view.AnimationCurve import com.esri.arcgisruntime.mapping.view.Graphic import com.esri.arcgisruntime.mapping.view.GraphicsOverlay import com.esri.arcgisruntime.mapping.view.MapView +import com.esri.arcgisruntime.symbology.Symbol import com.google.gson.reflect.TypeToken import dev.fluttercommunity.arcgis_map_sdk_android.model.AnimationOptions import dev.fluttercommunity.arcgis_map_sdk_android.model.ArcgisMapOptions import dev.fluttercommunity.arcgis_map_sdk_android.model.LatLng +import dev.fluttercommunity.arcgis_map_sdk_android.model.UserPosition import dev.fluttercommunity.arcgis_map_sdk_android.model.ViewPadding import dev.fluttercommunity.arcgis_map_sdk_android.util.GraphicsParser import io.flutter.plugin.common.BinaryMessenger @@ -46,10 +50,10 @@ import kotlin.math.roundToInt * A starting point for documentation can be found here: https://developers.arcgis.com/android/maps-2d/tutorials/display-a-map/ * */ internal class ArcgisMapView( - context: Context, - private val viewId: Int, - private val binaryMessenger: BinaryMessenger, - private val mapOptions: ArcgisMapOptions, + private val context: Context, + private val viewId: Int, + private val binaryMessenger: BinaryMessenger, + private val mapOptions: ArcgisMapOptions, ) : PlatformView { private val view: View = LayoutInflater.from(context).inflate(R.layout.vector_map_view, null) @@ -61,7 +65,7 @@ internal class ArcgisMapView( private lateinit var centerPositionStreamHandler: CenterPositionStreamHandler private val methodChannel = - MethodChannel(binaryMessenger, "dev.fluttercommunity.arcgis_map_sdk/$viewId") + MethodChannel(binaryMessenger, "dev.fluttercommunity.arcgis_map_sdk/$viewId") override fun getView(): View = view @@ -96,18 +100,18 @@ internal class ArcgisMapView( mapView.addViewpointChangedListener { val center = mapView.visibleArea.extent.center val wgs84Center = - GeometryEngine.project(center, SpatialReferences.getWgs84()) as Point + GeometryEngine.project(center, SpatialReferences.getWgs84()) as Point centerPositionStreamHandler.add( - LatLng( - longitude = wgs84Center.x, - latitude = wgs84Center.y - ) + LatLng( + longitude = wgs84Center.x, + latitude = wgs84Center.y + ) ) } val viewPoint = Viewpoint( - mapOptions.initialCenter.latitude, mapOptions.initialCenter.longitude, - getMapScale(mapOptions.zoom.roundToInt()), + mapOptions.initialCenter.latitude, mapOptions.initialCenter.longitude, + getMapScale(mapOptions.zoom.roundToInt()), ) mapView.setViewpoint(viewPoint) @@ -142,20 +146,180 @@ internal class ArcgisMapView( "remove_graphic" -> onRemoveGraphic(call = call, result = result) "toggle_base_map" -> onToggleBaseMap(call = call, result = result) "retryLoad" -> onRetryLoad(result = result) + "location_display_start_data_source" -> onStartLocationDisplayDataSource(result) + "location_display_stop_data_source" -> onStopLocationDisplayDataSource(result) + "location_display_set_default_symbol" -> onSetLocationDisplayDefaultSymbol( + call, + result + ) + + "location_display_set_accuracy_symbol" -> onSetLocationDisplayAccuracySymbol( + call, + result + ) + + "location_display_set_ping_animation_symbol" -> onSetLocationDisplayPingAnimationSymbol( + call, + result + ) + + "location_display_set_use_course_symbol_on_move" -> onSetLocationDisplayUseCourseSymbolOnMove( + call, + result + ) + + "location_display_update_display_source_position_manually" -> onUpdateLocationDisplaySourcePositionManually( + call, + result + ) + + "location_display_set_data_source_type" -> onSetLocationDisplayDataSourceType( + call, + result + ) + else -> result.notImplemented() } } } + private fun onStartLocationDisplayDataSource(result: MethodChannel.Result) { + val future = mapView.locationDisplay.locationDataSource.startAsync() + future.addDoneListener { + try { + result.success(future.get()) + } catch (e: Exception) { + result.error("Error", e.message, null) + } + } + } + + private fun onStopLocationDisplayDataSource(result: MethodChannel.Result) { + val future = mapView.locationDisplay.locationDataSource.stopAsync() + future.addDoneListener { + try { + result.success(future.get()) + } catch (e: Exception) { + result.error("Error", e.message, null) + } + } + } + + private fun onSetLocationDisplayDefaultSymbol(call: MethodCall, result: MethodChannel.Result) { + operationWithSymbol(call, result) { symbol -> + mapView.locationDisplay.defaultSymbol = symbol + } + } + + private fun onSetLocationDisplayAccuracySymbol(call: MethodCall, result: MethodChannel.Result) { + operationWithSymbol(call, result) { symbol -> + mapView.locationDisplay.accuracySymbol = symbol + } + } + + private fun onSetLocationDisplayPingAnimationSymbol( + call: MethodCall, + result: MethodChannel.Result + ) { + operationWithSymbol(call, result) { symbol -> + mapView.locationDisplay.pingAnimationSymbol = symbol + } + } + + private fun onSetLocationDisplayUseCourseSymbolOnMove( + call: MethodCall, + result: MethodChannel.Result + ) { + try { + val active = call.arguments as Boolean + mapView.locationDisplay.isUseCourseSymbolOnMovement = active + result.success(true) + } catch (e: Exception) { + result.error("missing_data", "Invalid arguments.", null) + } + } + + private fun onUpdateLocationDisplaySourcePositionManually( + call: MethodCall, + result: MethodChannel.Result + ) { + try { + val dataSource = + mapView.locationDisplay.locationDataSource as ManualLocationDisplayDataSource + val optionParams = call.arguments as Map + val position = optionParams.parseToClass() + + dataSource.setNewLocation(position) + result.success(true) + } catch (e: Exception) { + result.error( + "invalid_state", + "Expected ManualLocationDataSource", + null + ) + } + } + + private fun onSetLocationDisplayDataSourceType(call: MethodCall, result: MethodChannel.Result) { + if (mapView.locationDisplay.locationDataSource.status == LocationDataSource.Status.STARTED) { + result.error( + "invalid_state", + "Current data source is running. Make sure to stop it before setting a new data source", + null + ) + return + } + + when (call.arguments) { + "manual" -> { + try { + mapView.locationDisplay.locationDataSource = ManualLocationDisplayDataSource() + result.success(true) + } catch (e: Exception) { + result.error("Error", "Setting datasource on mapview failed", null) + } + } + + "system" -> { + try { + mapView.locationDisplay.locationDataSource = AndroidLocationDataSource(context) + result.success(true) + } catch (e: Exception) { + result.error("Error", "Setting datasource on mapview failed", null) + } + } + + else -> result.error("invalid_data", "Unknown data source type ${call.arguments}", null) + } + + } + + + private fun operationWithSymbol( + call: MethodCall, + result: MethodChannel.Result, + function: (Symbol) -> Unit + ) { + try { + val map = call.arguments as Map + val symbol = GraphicsParser.parseSymbol(map) + function(symbol) + result.success(true) + } catch (e: Throwable) { + result.error("unknown_error", "Error while adding graphic. $e)", null) + return + } + } + private fun setupEventChannel() { zoomStreamHandler = ZoomStreamHandler() centerPositionStreamHandler = CenterPositionStreamHandler() EventChannel(binaryMessenger, "dev.fluttercommunity.arcgis_map_sdk/$viewId/zoom") - .setStreamHandler(zoomStreamHandler) + .setStreamHandler(zoomStreamHandler) EventChannel(binaryMessenger, "dev.fluttercommunity.arcgis_map_sdk/$viewId/centerPosition") - .setStreamHandler(centerPositionStreamHandler) + .setStreamHandler(centerPositionStreamHandler) } private fun onZoomIn(call: MethodCall, result: MethodChannel.Result) { @@ -211,10 +375,10 @@ internal class ArcgisMapView( // https://developers.arcgis.com/android/api-reference/reference/com/esri/arcgisruntime/mapping/view/MapView.html#setViewInsets(double,double,double,double) mapView.setViewInsets( - viewPadding.left, - viewPadding.top, - viewPadding.right, - viewPadding.bottom + viewPadding.left, + viewPadding.top, + viewPadding.right, + viewPadding.bottom ) result.success(true) @@ -239,7 +403,7 @@ internal class ArcgisMapView( } val existingIds = - defaultGraphicsOverlay.graphics.mapNotNull { it.attributes["id"] as? String } + defaultGraphicsOverlay.graphics.mapNotNull { it.attributes["id"] as? String } val newIds = newGraphic.mapNotNull { it.attributes["id"] as? String } if (existingIds.any(newIds::contains)) { @@ -279,8 +443,8 @@ internal class ArcgisMapView( val animationOptionMap = (arguments["animationOptions"] as Map?) val animationOptions = - if (animationOptionMap.isNullOrEmpty()) null - else animationOptionMap.parseToClass() + if (animationOptionMap.isNullOrEmpty()) null + else animationOptionMap.parseToClass() val scale = if (zoomLevel != null) { getMapScale(zoomLevel) @@ -290,9 +454,9 @@ internal class ArcgisMapView( val initialViewPort = Viewpoint(point.latitude, point.longitude, scale) val future = mapView.setViewpointAsync( - initialViewPort, - (animationOptions?.duration?.toFloat() ?: 0F) / 1000, - animationOptions?.animationCurve ?: AnimationCurve.LINEAR, + initialViewPort, + (animationOptions?.duration?.toFloat() ?: 0F) / 1000, + animationOptions?.animationCurve ?: AnimationCurve.LINEAR, ) future.addDoneListener { @@ -307,13 +471,13 @@ internal class ArcgisMapView( private fun onMoveCameraToPoints(call: MethodCall, result: MethodChannel.Result) { val arguments = call.arguments as Map val latLongs = (arguments["points"] as ArrayList>) - .map { p -> parseToClass(p) } + .map { p -> parseToClass(p) } val padding = arguments["padding"] as Double? val polyline = Polyline( - PointCollection(latLongs.map { latLng -> Point(latLng.longitude, latLng.latitude) }), - SpatialReferences.getWgs84() + PointCollection(latLongs.map { latLng -> Point(latLng.longitude, latLng.latitude) }), + SpatialReferences.getWgs84() ) val future = if (padding != null) mapView.setViewpointGeometryAsync(polyline.extent, padding) @@ -330,8 +494,8 @@ internal class ArcgisMapView( private fun onToggleBaseMap(call: MethodCall, result: MethodChannel.Result) { val newStyle = gson.fromJson( - call.arguments as String, - object : TypeToken() {}.type + call.arguments as String, + object : TypeToken() {}.type ) map.basemap = Basemap(newStyle) result.success(true) diff --git a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ManualLocationDataSource.kt b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ManualLocationDataSource.kt new file mode 100644 index 000000000..293b24bea --- /dev/null +++ b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ManualLocationDataSource.kt @@ -0,0 +1,27 @@ +package dev.fluttercommunity.arcgis_map_sdk_android + +import com.esri.arcgisruntime.location.LocationDataSource +import dev.fluttercommunity.arcgis_map_sdk_android.model.UserPosition +import dev.fluttercommunity.arcgis_map_sdk_android.model.toAGSPoint + +class ManualLocationDisplayDataSource : LocationDataSource() { + + override fun onStart() { + this.onStartCompleted(null) + } + + override fun onStop() { + + } + + fun setNewLocation(userPosition: UserPosition) { + val loc = Location( + userPosition.latLng.toAGSPoint(), + userPosition.accuracy ?: 0.0, + userPosition.velocity ?: 0.0, + userPosition.heading ?: 0.0, + false + ) + updateLocation(loc) + } +} diff --git a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/UserPosition.kt b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/UserPosition.kt new file mode 100644 index 000000000..31931e4bc --- /dev/null +++ b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/UserPosition.kt @@ -0,0 +1,8 @@ +package dev.fluttercommunity.arcgis_map_sdk_android.model + +data class UserPosition( + val latLng: LatLng, + val accuracy: Double?, + val heading: Double?, + val velocity: Double? +) diff --git a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/ViewPadding.kt b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/ViewPadding.kt index 2d14a0885..b40099743 100644 --- a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/ViewPadding.kt +++ b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/model/ViewPadding.kt @@ -1,8 +1,8 @@ package dev.fluttercommunity.arcgis_map_sdk_android.model -class ViewPadding( +data class ViewPadding( val left: Double, val top: Double, val right: Double, val bottom: Double, -) \ No newline at end of file +) diff --git a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/util/GraphicsParser.kt b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/util/GraphicsParser.kt index 08eaf2af2..cff99e2e3 100644 --- a/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/util/GraphicsParser.kt +++ b/arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/util/GraphicsParser.kt @@ -47,10 +47,11 @@ class GraphicsParser { private fun parsePoint(map: Map): List { val point = (map["point"] as Map).parseToClass() + val symbolMap = map["symbol"] as Map val pointGraphic = Graphic().apply { geometry = point.toAGSPoint() - symbol = parseSymbol(map) + symbol = parseSymbol(symbolMap) } return listOf(pointGraphic) @@ -58,6 +59,7 @@ class GraphicsParser { private fun parsePolyline(map: Map): List { val points = parseToClass>>>(map["paths"]!!) + val symbolMap = map["symbol"] as Map return points.map { subPoints -> Graphic().apply { @@ -72,26 +74,25 @@ class GraphicsParser { if (z != null) Point(x, y, z, SpatialReferences.getWgs84()) else Point(x, y, SpatialReferences.getWgs84()) })) - symbol = parseSymbol(map) + symbol = parseSymbol(symbolMap) } } } private fun parsePolygon(map: Map): List { val rings = parseToClass>>>(map["rings"]!!) + val symbolMap = map["symbol"] as Map return rings.map { ring -> Graphic().apply { geometry = Polygon(PointCollection(ring.map { LatLng(it[0], it[1]).toAGSPoint() })) - symbol = parseSymbol(map) + symbol = parseSymbol(symbolMap) } } } - private fun parseSymbol(map: Map): Symbol { - val symbolMap = map["symbol"] as Map - + fun parseSymbol(symbolMap: Map): Symbol { val symbol = when (val type = symbolMap["type"]) { "simple-marker" -> parseSimpleMarkerSymbol(symbolMap) "picture-marker" -> parsePictureMarkerSymbol(symbolMap) diff --git a/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift b/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift index 13831095f..a27e54a2c 100644 --- a/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift +++ b/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift @@ -35,23 +35,23 @@ class ArcgisMapView: NSObject, FlutterPlatformView { } init( - frame: CGRect, - viewIdentifier viewId: Int64, - mapOptions: ArcgisMapOptions, - binaryMessenger messenger: FlutterBinaryMessenger + frame: CGRect, + viewIdentifier viewId: Int64, + mapOptions: ArcgisMapOptions, + binaryMessenger messenger: FlutterBinaryMessenger ) { methodChannel = FlutterMethodChannel( - name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)", - binaryMessenger: messenger + name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)", + binaryMessenger: messenger ) zoomEventChannel = FlutterEventChannel( - name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)/zoom", - binaryMessenger: messenger + name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)/zoom", + binaryMessenger: messenger ) zoomEventChannel.setStreamHandler(zoomStreamHandler) centerPositionEventChannel = FlutterEventChannel( - name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)/centerPosition", - binaryMessenger: messenger + name: "dev.fluttercommunity.arcgis_map_sdk/\(viewId)/centerPosition", + binaryMessenger: messenger ) centerPositionEventChannel.setStreamHandler(centerPositionStreamHandler) @@ -65,7 +65,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView { print("setLicenseKey failed. \(error)") } } - + mapView = AGSMapView.init(frame: frame) super.init() @@ -139,6 +139,14 @@ class ArcgisMapView: NSObject, FlutterPlatformView { case "remove_graphic": onRemoveGraphic(call, result) case "toggle_base_map" : onToggleBaseMap(call, result) case "retryLoad" : onRetryLoad(call, result) + case "location_display_start_data_source" : onStartLocationDisplayDataSource(call, result) + case "location_display_stop_data_source" : onStopLocationDisplayDataSource(call, result) + case "location_display_set_default_symbol": onSetLocationDisplayDefaultSymbol(call, result) + case "location_display_set_accuracy_symbol": onSetLocationDisplayAccuracySymbol(call, result) + case "location_display_set_ping_animation_symbol" : onSetLocationDisplayPingAnimationSymbol(call, result) + case "location_display_set_use_course_symbol_on_move" : onSetLocationDisplayUseCourseSymbolOnMove(call, result) + case "location_display_update_display_source_position_manually" : onUpdateLocationDisplaySourcePositionManually(call, result) + case "location_display_set_data_source_type" : onSetLocationDisplayDataSourceType(call, result) default: result(FlutterError(code: "Unimplemented", message: "No method matching the name \(call.method)", details: nil)) } @@ -150,7 +158,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView { result(FlutterError(code: "unknown_error", message: "MapView.mapScale is NaN. Maybe the map is not completely loaded.", details: nil)) return } - + let lodFactor = (call.arguments! as! Dictionary)["lodFactor"]! as! Int let currentZoomLevel = getZoomLevel(mapView.mapScale) let totalZoomLevel = currentZoomLevel + lodFactor @@ -168,7 +176,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView { result(FlutterError(code: "unknown_error", message: "MapView.mapScale is NaN. Maybe the map is not completely loaded.", details: nil)) return } - + let lodFactor = (call.arguments! as! Dictionary)["lodFactor"]! as! Int let currentZoomLevel = getZoomLevel(mapView.mapScale) let totalZoomLevel = currentZoomLevel - lodFactor @@ -186,10 +194,10 @@ class ArcgisMapView: NSObject, FlutterPlatformView { let padding: ViewPadding = try! JsonUtil.objectOfJson(dict) mapView.contentInset = UIEdgeInsets( - top: padding.top, - left: padding.left, - bottom: padding.bottom, - right: padding.right + top: padding.top, + left: padding.left, + bottom: padding.bottom, + right: padding.right ) result(true) @@ -206,9 +214,9 @@ class ArcgisMapView: NSObject, FlutterPlatformView { let scale = zoomLevel != nil ? getMapScale(zoomLevel!) : mapView.mapScale mapView.setViewpoint( - AGSViewpoint(center: point.toAGSPoint(), scale: scale), - duration: (animationOptions?.duration ?? 0) / 1000, - curve: animationOptions?.arcgisAnimationCurve() ?? .linear + AGSViewpoint(center: point.toAGSPoint(), scale: scale), + duration: (animationOptions?.duration ?? 0) / 1000, + curve: animationOptions?.arcgisAnimationCurve() ?? .linear ) { success in result(success) } @@ -240,7 +248,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView { result(FlutterError(code: "unknown_error", message: "Error while adding graphic. \(error)", details: nil)) return } - + let existingIds = defaultGraphicsOverlay.graphics.compactMap { object in let graphic = object as! AGSGraphic @@ -316,15 +324,15 @@ class ArcgisMapView: NSObject, FlutterPlatformView { // don't set "isMagnifierEnabled" since we don't want to use this feature } -private func parseBaseMapStyle(_ string: String) -> AGSBasemapStyle { - let baseMapStyle = AGSBasemapStyle.allCases.first { enumValue in - enumValue.getJsonValue() == string - } - if baseMapStyle == nil { - NSLog("Warning: Could not find a base map style matching the input string. Defaulting to .arcGISImageryStandard.") + private func parseBaseMapStyle(_ string: String) -> AGSBasemapStyle { + let baseMapStyle = AGSBasemapStyle.allCases.first { enumValue in + enumValue.getJsonValue() == string + } + if baseMapStyle == nil { + NSLog("Warning: Could not find a base map style matching the input string. Defaulting to .arcGISImageryStandard.") + } + return baseMapStyle ?? .arcGISImageryStandard } - return baseMapStyle ?? .arcGISImageryStandard -} /** * Convert map scale to zoom level @@ -342,6 +350,105 @@ private func parseBaseMapStyle(_ string: String) -> AGSBasemapStyle { private func getMapScale(_ zoomLevel: Int) -> Double { 591657527 * (exp(-0.693 * Double(zoomLevel))) } + + + private func onStartLocationDisplayDataSource(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + mapView.locationDisplay.dataSource.start { error in + if let error = error { + let flutterError = FlutterError( + code: "generic_error", + message: "Failed to start data source: \(error.localizedDescription)", + details: nil + ) + result(flutterError) + } else { + result(true) + } + } + } + + private func onStopLocationDisplayDataSource(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + mapView.locationDisplay.dataSource.stop { + result(true) + } + } + + private func onSetLocationDisplayDefaultSymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + operationWithSymbol(call, result) { mapView.locationDisplay.defaultSymbol = $0 } + } + + private func onSetLocationDisplayAccuracySymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + operationWithSymbol(call, result) { mapView.locationDisplay.accuracySymbol = $0 } + } + + private func onSetLocationDisplayPingAnimationSymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + operationWithSymbol(call, result) { mapView.locationDisplay.pingAnimationSymbol = $0 } + } + + + private func onSetLocationDisplayUseCourseSymbolOnMove(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + guard let active = call.arguments as? Bool else { + result(FlutterError(code: "missing_data", message: "Invalid arguments.", details: nil)) + return + } + + mapView.locationDisplay.useCourseSymbolOnMovement = active + result(true) + } + + private func onUpdateLocationDisplaySourcePositionManually(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + let dataSource = mapView.locationDisplay.dataSource + guard let source = dataSource as? ManualLocationDataSource else { + result(FlutterError(code: "invalid_state", message: "Expected ManualLocationDataSource but got \(dataSource)", details: nil)) + return + } + + guard let dict = call.arguments as? Dictionary, let position: UserPosition = try? JsonUtil.objectOfJson(dict) else { + result(FlutterError(code: "missing_data", message: "Expected arguments to contain data of UserPosition.", details: nil)) + return + } + + source.setNewLocation(position) + result(true) + } + + private func onSetLocationDisplayDataSourceType(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + if(mapView.locationDisplay.dataSource.status == .started) { + result(FlutterError(code: "invalid_state", message: "Current data source is running. Make sure to stop it before setting a new data source", details: nil)) + return + } + + guard let type = call.arguments as? String else { + result(FlutterError(code: "missing_data", message: "Invalid argument, expected a type of data source as string.", details: nil)) + return + } + + switch(type) { + case "manual" : + mapView.locationDisplay.dataSource = ManualLocationDataSource() + result(true) + case "system" : + mapView.locationDisplay.dataSource = AGSCLLocationDataSource() + result(true) + default: + result(FlutterError(code: "invalid_data", message: "Unknown data source type \(String(describing: type))", details: nil)) + } + } + + private func operationWithSymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, handler: (AGSSymbol) -> Void) { + do { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "missing_data", message: "Invalid arguments", details: nil)) + return + } + let symbol = try GraphicsParser().parseSymbol(args) + handler(symbol) + result(true) + } + catch { + result(FlutterError(code: "unknown_error", message: "Error while adding graphic. \(error)", details: nil)) + } + } } extension AGSBasemapStyle: CaseIterable { diff --git a/arcgis_map_sdk_ios/ios/Classes/GraphicsParser.swift b/arcgis_map_sdk_ios/ios/Classes/GraphicsParser.swift index 62aebc2a6..4b771a015 100644 --- a/arcgis_map_sdk_ios/ios/Classes/GraphicsParser.swift +++ b/arcgis_map_sdk_ios/ios/Classes/GraphicsParser.swift @@ -56,11 +56,11 @@ class GraphicsParser { } return AGSPoint(x: array[0], y: array[1], spatialReference: .wgs84()) } - + let graphic = AGSGraphic() graphic.geometry = AGSPolyline(points: points) graphic.symbol = try! parseSymbol(dictionary["symbol"] as! Dictionary) - + return graphic } } @@ -82,7 +82,7 @@ class GraphicsParser { // region symbol parsing - private func parseSymbol(_ dictionary: [String: Any]) throws -> AGSSymbol { + func parseSymbol(_ dictionary: [String: Any]) throws -> AGSSymbol { let type = dictionary["type"] as! String; switch (type) { case "simple-marker": diff --git a/arcgis_map_sdk_ios/ios/Classes/ManualLocationDataSource.swift b/arcgis_map_sdk_ios/ios/Classes/ManualLocationDataSource.swift new file mode 100644 index 000000000..0d266a535 --- /dev/null +++ b/arcgis_map_sdk_ios/ios/Classes/ManualLocationDataSource.swift @@ -0,0 +1,22 @@ +// +// ManualLocationDataSource.swift +// arcgis_map_sdk_ios +// +// Created by Julian Bissekkou on 27.11.23. +// + +import Foundation +import ArcGIS + +class ManualLocationDataSource: AGSLocationDataSource { + public func setNewLocation(_ position: UserPosition) { + let loc = AGSLocation( + position: position.latLng.toAGSPoint(), + horizontalAccuracy: position.accuracy ?? 0, + velocity: position.velocity ?? 0, + course: position.heading ?? 0, + lastKnown: false + ) + didUpdate(loc) + } +} diff --git a/arcgis_map_sdk_ios/ios/Classes/Models/UserPosition.swift b/arcgis_map_sdk_ios/ios/Classes/Models/UserPosition.swift new file mode 100644 index 000000000..bbf2e5bf1 --- /dev/null +++ b/arcgis_map_sdk_ios/ios/Classes/Models/UserPosition.swift @@ -0,0 +1,15 @@ +// +// UserPosition.swift +// arcgis_map_sdk_ios +// +// Created by Julian Bissekkou on 27.11.23. +// + +import Foundation + +struct UserPosition: Codable { + let latLng: LatLng + let accuracy: Double? + let heading: Double? + let velocity: Double? +} diff --git a/arcgis_map_sdk_method_channel/lib/src/method_channel_arcgis_map_plugin.dart b/arcgis_map_sdk_method_channel/lib/src/method_channel_arcgis_map_plugin.dart index 508cb1f7c..014b45283 100644 --- a/arcgis_map_sdk_method_channel/lib/src/method_channel_arcgis_map_plugin.dart +++ b/arcgis_map_sdk_method_channel/lib/src/method_channel_arcgis_map_plugin.dart @@ -238,4 +238,70 @@ class MethodChannelArcgisMapPlugin extends ArcgisMapPlatform { 'getVisibleGraphicIds() has not been implemented.', ); } + + @override + Future startLocationDisplayDataSource(int mapId) { + return _methodChannelBuilder(mapId) + .invokeMethod("location_display_start_data_source"); + } + + @override + Future stopLocationDisplayDataSource(int mapId) { + return _methodChannelBuilder(mapId) + .invokeMethod("location_display_stop_data_source"); + } + + @override + Future setLocationDisplayDefaultSymbol(int mapId, Symbol symbol) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_set_default_symbol", + symbol.toJson(), + ); + } + + @override + Future setLocationDisplayAccuracySymbol(int mapId, Symbol symbol) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_set_accuracy_symbol", + symbol.toJson(), + ); + } + + @override + Future setLocationDisplayPingAnimationSymbol( + int mapId, + Symbol symbol, + ) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_set_ping_animation_symbol", + symbol.toJson(), + ); + } + + @override + Future setUseCourseSymbolOnMovement(int mapId, bool useCourseSymbol) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_set_use_course_symbol_on_move", + useCourseSymbol, + ); + } + + @override + Future updateLocationDisplaySourcePositionManually( + int mapId, + UserPosition position, + ) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_update_display_source_position_manually", + position.toMap(), + ); + } + + @override + Future setLocationDisplay(int mapId, String type) { + return _methodChannelBuilder(mapId).invokeMethod( + "location_display_set_data_source_type", + type, + ); + } } diff --git a/arcgis_map_sdk_method_channel/lib/src/model_extension.dart b/arcgis_map_sdk_method_channel/lib/src/model_extension.dart index 79999e6bb..f2b4a529e 100644 --- a/arcgis_map_sdk_method_channel/lib/src/model_extension.dart +++ b/arcgis_map_sdk_method_channel/lib/src/model_extension.dart @@ -20,6 +20,15 @@ extension LatLngJsonExtension on LatLng { }; } +extension UserPositionExtension on UserPosition { + Map toMap() => { + 'latLng': latLng.toMap(), + 'accuracy': accuracy, + 'heading': heading, + 'velocity': velocity, + }; +} + extension ArcgisMapOptionsJsonExtension on ArcgisMapOptions { Map toMap() { return { diff --git a/arcgis_map_sdk_platform_interface/lib/arcgis_map_sdk_platform_interface.dart b/arcgis_map_sdk_platform_interface/lib/arcgis_map_sdk_platform_interface.dart index 984039cc0..fffaabfd5 100644 --- a/arcgis_map_sdk_platform_interface/lib/arcgis_map_sdk_platform_interface.dart +++ b/arcgis_map_sdk_platform_interface/lib/arcgis_map_sdk_platform_interface.dart @@ -6,6 +6,7 @@ export 'package:arcgis_map_sdk_platform_interface/src/events/map_event.dart'; export 'package:arcgis_map_sdk_platform_interface/src/models/animation_options.dart'; export 'package:arcgis_map_sdk_platform_interface/src/models/basemap.dart'; export 'package:arcgis_map_sdk_platform_interface/src/models/ground.dart'; +export 'package:arcgis_map_sdk_platform_interface/src/models/user_position.dart'; export 'package:arcgis_map_sdk_platform_interface/src/models/view_position.dart'; export 'package:arcgis_map_sdk_platform_interface/src/types/arcgis_map_options.dart'; export 'package:arcgis_map_sdk_platform_interface/src/types/attributes.dart'; diff --git a/arcgis_map_sdk_platform_interface/lib/src/arcgis_map_sdk_platform_interface.dart b/arcgis_map_sdk_platform_interface/lib/src/arcgis_map_sdk_platform_interface.dart index 2347b9351..7e2d8c42f 100644 --- a/arcgis_map_sdk_platform_interface/lib/src/arcgis_map_sdk_platform_interface.dart +++ b/arcgis_map_sdk_platform_interface/lib/src/arcgis_map_sdk_platform_interface.dart @@ -223,4 +223,58 @@ class ArcgisMapPlatform extends PlatformInterface { 'getVisibleGraphicIds() has not been implemented.', ); } + + Future startLocationDisplayDataSource(int mapId) { + throw UnimplementedError( + 'startLocationDisplayDataSource() has not been implemented.', + ); + } + + Future stopLocationDisplayDataSource(int mapId) { + throw UnimplementedError( + 'stopLocationDisplayDataSource() has not been implemented.', + ); + } + + Future setLocationDisplayDefaultSymbol(int mapId, Symbol symbol) { + throw UnimplementedError( + 'setLocationDisplayDefaultSymbol() has not been implemented.', + ); + } + + Future setLocationDisplayAccuracySymbol(int mapId, Symbol symbol) { + throw UnimplementedError( + 'setLocationDisplayAccuracySymbol() has not been implemented.', + ); + } + + Future setLocationDisplayPingAnimationSymbol( + int mapId, + Symbol symbol, + ) { + throw UnimplementedError( + 'setLocationDisplayPingAnimationSymbol() has not been implemented.', + ); + } + + Future setUseCourseSymbolOnMovement(int mapId, bool useCourseSymbol) { + throw UnimplementedError( + 'setUseCourseSymbolOnMovement() has not been implemented.', + ); + } + + Future updateLocationDisplaySourcePositionManually( + int mapId, + UserPosition position, + ) { + throw UnimplementedError( + 'updateLocationDisplaySourcePositionManually() has not been implemented.', + ); + } + + Future setLocationDisplay(int mapId, String type) { + throw UnimplementedError( + 'setLocationDisplay() has not been implemented.', + ); + } } diff --git a/arcgis_map_sdk_platform_interface/lib/src/models/user_position.dart b/arcgis_map_sdk_platform_interface/lib/src/models/user_position.dart new file mode 100644 index 000000000..3d666b175 --- /dev/null +++ b/arcgis_map_sdk_platform_interface/lib/src/models/user_position.dart @@ -0,0 +1,15 @@ +import 'package:arcgis_map_sdk_platform_interface/arcgis_map_sdk_platform_interface.dart'; + +class UserPosition { + final LatLng latLng; + final double? accuracy; + final double? heading; + final double? velocity; + + const UserPosition({ + required this.latLng, + this.accuracy, + this.velocity, + this.heading, + }); +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 3f41384db..5d3c10d75 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,9 @@ + + + + + NSLocationAlwaysUsageDescription + This app requires the location to demonstrate arcgis location apis + NSLocationWhenInUseUsageDescription + This app requires the location to demonstrate arcgis location apis CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/example/lib/location_indicator_example_page.dart b/example/lib/location_indicator_example_page.dart new file mode 100644 index 000000000..c9a0df25e --- /dev/null +++ b/example/lib/location_indicator_example_page.dart @@ -0,0 +1,187 @@ +import 'dart:math'; + +import 'package:arcgis_example/main.dart'; +import 'package:arcgis_map_sdk/arcgis_map_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; + +class LocationIndicatorExamplePage extends StatefulWidget { + const LocationIndicatorExamplePage({super.key}); + + @override + State createState() => + _LocationIndicatorExamplePageState(); +} + +class _LocationIndicatorExamplePageState + extends State { + final _mockLocations = [ + LatLng(48.1234963, 11.5910182), + LatLng(48.1239241, 11.45897063), + LatLng(48.123876, 11.590120), + LatLng(48.123876, 11.590120), + LatLng(48.123740, 11.589015), + LatLng(48.123164, 11.588585), + LatLng(48.1234963, 11.5910182), + ]; + + final _snackBarKey = GlobalKey(); + ArcgisMapController? _controller; + bool _isStarted = false; + bool _useCourseSymbolForMovement = false; + bool _isManualLocationSource = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _snackBarKey, + appBar: AppBar(), + floatingActionButton: FloatingActionButton( + child: Icon(_isStarted ? Icons.stop : Icons.play_arrow), + onPressed: () async { + try { + _isStarted + ? await _controller!.locationDisplay.stopSource() + : await _controller!.locationDisplay.startSource(); + } catch (e, stack) { + if (!mounted) return; + ScaffoldMessenger.of(_snackBarKey.currentContext!) + .showSnackBar(SnackBar(content: Text("$e"))); + debugPrint("$e"); + debugPrintStack(stackTrace: stack); + } + + if (!mounted) return; + setState(() => _isStarted = !_isStarted); + }, + ), + body: Column( + children: [ + Expanded( + child: ArcgisMap( + apiKey: arcGisApiKey, + initialCenter: const LatLng(51.16, 10.45), + zoom: 13, + basemap: BaseMap.arcgisNavigationNight, + mapStyle: MapStyle.twoD, + onMapCreated: (controller) { + _controller = controller; + _requestLocationPermission(); + _configureLocationDisplay(Colors.blue); + }, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _switchLocationSource, + child: Text( + _isManualLocationSource + ? "Use auto location source" + : "Use manual location source", + ), + ), + if (_isManualLocationSource) ...[ + ElevatedButton( + onPressed: _simulateLocationChange, + child: Text("simulate location change"), + ), + ], + ElevatedButton( + onPressed: () => _configureLocationDisplay(Colors.green), + child: Text("tint indicator green"), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => _configureLocationDisplay(Colors.red), + child: Text("tint indicator red"), + ), + ElevatedButton( + onPressed: () { + setState( + () => + _useCourseSymbolForMovement = !_useCourseSymbolForMovement, + ); + _configureLocationDisplay(Colors.red); + }, + child: Text( + _useCourseSymbolForMovement + ? "Disable course indicator" + : "Enable course indicator", + ), + ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), + ], + ), + ); + } + + Future _requestLocationPermission() async { + await Geolocator.requestPermission(); + final location = await Geolocator.getLastKnownPosition(); + if (!mounted || location == null) return; + + await _controller!.moveCamera( + point: LatLng(location.latitude, location.longitude), + zoomLevel: 16, + ); + } + + Future _configureLocationDisplay(MaterialColor color) async { + await _controller!.locationDisplay.setUseCourseSymbolOnMovement( + _useCourseSymbolForMovement, + ); + await _controller!.locationDisplay.setDefaultSymbol( + SimpleMarkerSymbol( + color: color.shade100, + outlineColor: color.shade500, + radius: 24, + ), + ); + await _controller!.locationDisplay.setPingAnimationSymbol( + SimpleMarkerSymbol( + color: color.shade50, + outlineColor: color.shade900, + ), + ); + await _controller!.locationDisplay.setAccuracySymbol( + SimpleLineSymbol(color: color.shade800, width: 3), + ); + } + + Future _switchLocationSource() async { + await _controller!.locationDisplay.stopSource(); + await _controller!.setLocationDisplay( + _isManualLocationSource + ? ArcgisLocationDisplay() + : ArcgisManualLocationDisplay(), + ); + setState(() => _isManualLocationSource = !_isManualLocationSource); + + if (!_isManualLocationSource) { + final location = await Geolocator.getLastKnownPosition(); + if (location == null) return; + await _controller!.moveCamera( + point: LatLng(location.latitude, location.longitude), + ); + } + } + + Future _simulateLocationChange() async { + final display = _controller!.locationDisplay as ArcgisManualLocationDisplay; + + await _controller!.moveCamera(point: _mockLocations.first); + for (final latLng in _mockLocations) { + if (!mounted) break; + if (!_isManualLocationSource) break; + + await display.updateLocation( + UserPosition( + latLng: latLng, + accuracy: Random().nextInt(100).toDouble(), + velocity: Random().nextInt(100).toDouble(), + ), + ); + await Future.delayed(const Duration(milliseconds: 600)); + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index d8075deda..fda1bde80 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:core'; +import 'package:arcgis_example/location_indicator_example_page.dart'; import 'package:arcgis_example/map_elements.dart'; import 'package:arcgis_example/vector_layer_example_page.dart'; import 'package:arcgis_map_sdk/arcgis_map_sdk.dart'; @@ -485,11 +486,13 @@ class _ExampleMapState extends State { ), ), ElevatedButton( - onPressed: () { - _routeToVectorLayerMap(); - }, + onPressed: _routeToVectorLayerMap, child: const Text("Show Vector layer example"), ), + ElevatedButton( + onPressed: _routeToLocationIndicatorExample, + child: const Text("Location indicator example"), + ), ElevatedButton( onPressed: () { setState(() { @@ -731,4 +734,10 @@ class _ExampleMapState extends State { ), ); } + + void _routeToLocationIndicatorExample() { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const LocationIndicatorExamplePage()), + ); + } } diff --git a/example/pubspec.lock b/example/pubspec.lock index be84cb57b..0c7245f75 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -83,6 +83,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -114,6 +122,54 @@ packages: description: flutter source: sdk version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02 + url: "https://pub.dev" + source: hosted + version: "10.1.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "741579fa6c9e412984d2bdb2fbaa54e3c3f7587c60aeacfe6e058358a11f40f8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: ab90ae811c42ec2f6021e01eca71df00dee6ff1e69d2c2dafd4daeb0b793f73d + url: "https://pub.dev" + source: hosted + version: "2.3.2" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "6c8d494d6948757c56720b778af742f6973f31fca1f702a7539b8917e4a2468a" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: transitive description: @@ -199,6 +255,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -239,6 +303,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + url: "https://pub.dev" + source: hosted + version: "4.2.1" vector_math: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5b58833c3..44bf06e79 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: arcgis_map_sdk: ^0.8.0 cupertino_icons: ^1.0.0 + geolocator: ^10.1.0 flutter: sdk: flutter flutter_web_plugins: