diff --git a/arcgis_map_sdk/lib/arcgis_map_sdk.dart b/arcgis_map_sdk/lib/arcgis_map_sdk.dart index c1b43b7d..5f4dabec 100644 --- a/arcgis_map_sdk/lib/arcgis_map_sdk.dart +++ b/arcgis_map_sdk/lib/arcgis_map_sdk.dart @@ -3,4 +3,5 @@ library arcgis_map; 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'; export 'package:arcgis_map_sdk_platform_interface/arcgis_map_sdk_platform_interface.dart'; diff --git a/arcgis_map_sdk/lib/src/arcgis_map_controller.dart b/arcgis_map_sdk/lib/src/arcgis_map_controller.dart index 28893f99..7fce8c26 100644 --- a/arcgis_map_sdk/lib/src/arcgis_map_controller.dart +++ b/arcgis_map_sdk/lib/src/arcgis_map_controller.dart @@ -1,13 +1,26 @@ +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'; +typedef MapStatusListener = void Function(MapStatus status); + class ArcgisMapController { ArcgisMapController._({ required this.mapId, - }); + }) { + ArcgisMapPlatform.instance.setMethodCallHandler( + mapId: mapId, + onCall: _onCall, + ); + } final int mapId; + final _listeners = []; + MapStatus _mapStatus = MapStatus.unknown; + + MapStatus get mapStatus => _mapStatus; + static Future init( int id, ) async { @@ -60,6 +73,16 @@ class ArcgisMapController { ); } + Future _onCall(MethodCall call) async { + final method = call.method; + switch (method) { + case "onStatusChanged": + return _notifyStatusChanged(call); + default: + throw UnimplementedError('Method "$method" not implemented'); + } + } + Stream getZoom() { return ArcgisMapPlatform.instance.getZoom(mapId); } @@ -160,6 +183,26 @@ class ArcgisMapController { ); } + /// Adds a listener that gets notified if the map status changes. + VoidCallback addStatusChangeListener(MapStatusListener listener) { + _listeners.add(listener); + return () => _listeners.removeWhere((l) => l == listener); + } + + /// Calling native `retryLoadAsync` (android) and `retryLoad` (swift) + /// https://developers.arcgis.com/kotlin/api-reference/arcgis-maps-kotlin/com.arcgismaps/-loadable/retry-load.html + /// This does not trigger `onMapCreated` since it will only try, if there is an error + Future retryLoad() { + return ArcgisMapPlatform.instance.retryLoad(mapId); + } + + void _notifyStatusChanged(MethodCall call) { + _mapStatus = MapStatus.values.byName(call.arguments as String); + for (final listener in _listeners) { + listener(_mapStatus); + } + } + Future setInteraction({required bool isEnabled}) { return ArcgisMapPlatform.instance .setInteraction(mapId, isEnabled: isEnabled); diff --git a/arcgis_map_sdk/lib/src/model/map_status.dart b/arcgis_map_sdk/lib/src/model/map_status.dart new file mode 100644 index 00000000..b9755be7 --- /dev/null +++ b/arcgis_map_sdk/lib/src/model/map_status.dart @@ -0,0 +1 @@ +enum MapStatus { loaded, loading, failedToLoad, notLoaded, unknown } 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 ba469b28..06bce86b 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 @@ -12,6 +12,11 @@ import com.esri.arcgisruntime.geometry.PointCollection import com.esri.arcgisruntime.geometry.Polyline import com.esri.arcgisruntime.geometry.SpatialReferences import com.esri.arcgisruntime.layers.ArcGISVectorTiledLayer +import com.esri.arcgisruntime.loadable.LoadStatus.FAILED_TO_LOAD +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.mapping.ArcGISMap import com.esri.arcgisruntime.mapping.Basemap import com.esri.arcgisruntime.mapping.BasemapStyle @@ -66,15 +71,20 @@ internal class ArcgisMapView( mapView = view.findViewById(R.id.mapView) - if (mapOptions.basemap != null) { - map.basemap = Basemap(mapOptions.basemap) - } else { - val layers = mapOptions.vectorTilesUrls.map { url -> ArcGISVectorTiledLayer(url) } - map.basemap = Basemap(layers, null) + map.apply { + + basemap = if (mapOptions.basemap != null) { + Basemap(mapOptions.basemap) + } else { + val layers = mapOptions.vectorTilesUrls.map { url -> ArcGISVectorTiledLayer(url) } + Basemap(layers, null) + } + + minScale = getMapScale(mapOptions.minZoom) + maxScale = getMapScale(mapOptions.maxZoom) + addLoadStatusChangedListener(::onLoadStatusChanged) } - map.minScale = getMapScale(mapOptions.minZoom) - map.maxScale = getMapScale(mapOptions.maxZoom) mapView.map = map mapView.graphicsOverlays.add(defaultGraphicsOverlay) @@ -107,7 +117,15 @@ internal class ArcgisMapView( setupEventChannel() } - override fun dispose() {} + private fun onLoadStatusChanged(event: LoadStatusChangedEvent?) { + if (event == null) return + methodChannel.invokeMethod("onStatusChanged", event.jsonValue()) + } + + override fun dispose() { + map.removeLoadStatusChangedListener(::onLoadStatusChanged) + mapView.dispose() + } // region helper @@ -123,6 +141,7 @@ internal class ArcgisMapView( "add_graphic" -> onAddGraphic(call = call, result = result) "remove_graphic" -> onRemoveGraphic(call = call, result = result) "toggle_base_map" -> onToggleBaseMap(call = call, result = result) + "retryLoad" -> onRetryLoad(result = result) else -> result.notImplemented() } } @@ -260,7 +279,7 @@ internal class ArcgisMapView( val animationOptionMap = (arguments["animationOptions"] as Map?) val animationOptions = - if (animationOptionMap == null || animationOptionMap.isEmpty()) null + if (animationOptionMap.isNullOrEmpty()) null else animationOptionMap.parseToClass() val scale = if (zoomLevel != null) { @@ -318,6 +337,11 @@ internal class ArcgisMapView( result.success(true) } + private fun onRetryLoad(result: MethodChannel.Result) { + mapView.map?.retryLoadAsync() + result.success(true) + } + /** * Convert map scale to zoom level * https://developers.arcgis.com/documentation/mapping-apis-and-services/reference/zoom-levels-and-scale/#conversion-tool @@ -363,4 +387,11 @@ internal class ArcgisMapView( // endregion } +private fun LoadStatusChangedEvent.jsonValue() = when (newLoadStatus) { + LOADED -> "loaded" + LOADING -> "loading" + FAILED_TO_LOAD -> "failedToLoad" + NOT_LOADED -> "notLoaded" + else -> "unknown" +} diff --git a/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift b/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift index ca1b2bd2..13831095 100644 --- a/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift +++ b/arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift @@ -11,6 +11,8 @@ class ArcgisMapView: NSObject, FlutterPlatformView { private let centerPositionEventChannel: FlutterEventChannel private let centerPositionStreamHandler = CenterPositionStreamHandler() + private var mapLoadStatusObservation: NSKeyValueObservation? + private var mapScaleObservation: NSKeyValueObservation? private var mapVisibleAreaObservation: NSKeyValueObservation? @@ -115,6 +117,13 @@ class ArcgisMapView: NSObject, FlutterPlatformView { setMapInteractive(mapOptions.isInteractive) setupMethodChannel() + + mapLoadStatusObservation = map.observe(\.loadStatus, options: .initial) { [weak self] (map, notifier) in + DispatchQueue.main.async { + let status = map.loadStatus + self?.notifyStatus(status) + } + } } private func setupMethodChannel() { @@ -129,6 +138,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView { case "add_graphic": onAddGraphic(call, result) case "remove_graphic": onRemoveGraphic(call, result) case "toggle_base_map" : onToggleBaseMap(call, result) + case "retryLoad" : onRetryLoad(call, result) default: result(FlutterError(code: "Unimplemented", message: "No method matching the name \(call.method)", details: nil)) } @@ -203,10 +213,10 @@ class ArcgisMapView: NSObject, FlutterPlatformView { result(success) } } - + private func onMoveCameraToPoints(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { let dict = call.arguments as! Dictionary - + let payload: MoveToPointsPayload = try! JsonUtil.objectOfJson(dict) let polyline = AGSPolyline(points: payload.points.map { latLng in AGSPoint(x: latLng.longitude, y:latLng.latitude, spatialReference: .wgs84()) }) @@ -280,7 +290,16 @@ class ArcgisMapView: NSObject, FlutterPlatformView { result(true) } - + + private func onRetryLoad(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + mapView.map!.retryLoad() + result(true) + } + + private func notifyStatus(_ status: AGSLoadStatus) { + methodChannel.invokeMethod("onStatusChanged", arguments: status.jsonValue()) + } + private func onSetInteraction(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { let enabled = (call.arguments! as! Dictionary)["enabled"]! as! Bool @@ -479,6 +498,25 @@ extension AGSBasemapStyle { } } +extension AGSLoadStatus { + func jsonValue() -> String { + switch self { + case .loaded: + return "loaded" + case .loading: + return "loading" + case .failedToLoad: + return "failedToLoad" + case .notLoaded: + return "notLoaded" + case .unknown: + return "unknown" + @unknown default: + return "unknown" + } + } +} + struct MoveToPointsPayload : Codable { let points : [LatLng] let padding : 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 52a30b75..508cb1f7 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 @@ -176,6 +176,19 @@ class MethodChannelArcgisMapPlugin extends ArcgisMapPlatform { ).then((value) => value!); } + @override + Future retryLoad(int mapId) async { + return _methodChannelBuilder(mapId).invokeMethod("retryLoad"); + } + + @override + Future setMethodCallHandler({ + required int mapId, + required Future Function(MethodCall) onCall, + }) async { + return _methodChannelBuilder(mapId).setMethodCallHandler(onCall); + } + @override Stream getZoom(int mapId) { _zoomEventStream ??= 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 eeee8903..2347b935 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 @@ -162,6 +162,19 @@ class ArcgisMapPlatform extends PlatformInterface { throw UnimplementedError('zoomOut() has not been implemented.'); } + Future retryLoad(int mapId) { + throw UnimplementedError('reload() has not been implemented.'); + } + + Future setMethodCallHandler({ + required int mapId, + required Future Function(MethodCall) onCall, + }) { + throw UnimplementedError( + 'setMethodCallHandler() has not been implemented.', + ); + } + Future setInteraction(int mapId, {required bool isEnabled}) { throw UnimplementedError('setInteraction() has not been implemented.'); } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 449844cb..daec7463 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -30,4 +30,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.0 diff --git a/example/lib/main.dart b/example/lib/main.dart index cd16d248..d8075ded 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,6 +71,10 @@ class _ExampleMapState extends State { final tappedHQ = const LatLng(48.1234963, 11.5910182); var _isInteractionEnabled = true; + VoidCallback? _removeStatusListener; + + final _scaffoldKey = GlobalKey(); + @override void dispose() { _boundingBoxSubscription?.cancel(); @@ -79,12 +83,17 @@ class _ExampleMapState extends State { _attributionTextSubscription?.cancel(); _zoomSubscription?.cancel(); _isGraphicHoveredSubscription?.cancel(); + _removeStatusListener?.call(); + super.dispose(); } Future _onMapCreated(ArcgisMapController controller) async { _controller = controller; + _removeStatusListener = + _controller!.addStatusChangeListener(_onMapStatusChanged); + // TODO: Remove when mobile implementation is complete if (kIsWeb) { _controller?.onClickListener().listen((Attributes? attributes) { @@ -367,6 +376,7 @@ class _ExampleMapState extends State { @override Widget build(BuildContext context) { return Scaffold( + key: _scaffoldKey, body: Stack( children: [ ArcgisMap( @@ -443,20 +453,21 @@ class _ExampleMapState extends State { ); }, ), - FloatingActionButton( - heroTag: "3d-map-button", - onPressed: () { - setState(() { - show3dMap = !show3dMap; - _controller?.switchMapStyle( - show3dMap ? MapStyle.threeD : MapStyle.twoD, - ); - }); - }, - backgroundColor: - show3dMap ? Colors.red : Colors.blue, - child: Text(show3dMap ? '3D' : '2D'), - ), + if (kIsWeb) + FloatingActionButton( + heroTag: "3d-map-button", + onPressed: () { + setState(() { + show3dMap = !show3dMap; + _controller?.switchMapStyle( + show3dMap ? MapStyle.threeD : MapStyle.twoD, + ); + }); + }, + backgroundColor: + show3dMap ? Colors.red : Colors.blue, + child: Text(show3dMap ? '3D' : '2D'), + ), ], ), ElevatedButton( @@ -582,38 +593,41 @@ class _ExampleMapState extends State { ? const Text("Stop pos") : const Text("Sub to pos"), ), - ElevatedButton( - onPressed: () { - if (_subscribedToBounds) { - _unsubscribeFromBounds(); - } else { - _subscribeToBounds(); - } - }, - child: _subscribedToBounds - ? const Text("Stop bounds") - : const Text("Sub to bounds"), - ), - ElevatedButton( - onPressed: () { - if (_subscribedToGraphicsInView) { - _unSubscribeToGraphicsInView(); - } else { - _subscribeToGraphicsInView(); - } - }, - child: _subscribedToGraphicsInView - ? const Text("Stop printing Graphics") - : const Text("Start printing Graphics"), - ), - ElevatedButton( - onPressed: () { - final graphicIdsInView = - _controller?.getVisibleGraphicIds(); - graphicIdsInView?.forEach(debugPrint); - }, - child: const Text("Print visible Graphics"), - ), + if (kIsWeb) + ElevatedButton( + onPressed: () { + if (_subscribedToBounds) { + _unsubscribeFromBounds(); + } else { + _subscribeToBounds(); + } + }, + child: _subscribedToBounds + ? const Text("Stop bounds") + : const Text("Sub to bounds"), + ), + if (kIsWeb) + ElevatedButton( + onPressed: () { + if (_subscribedToGraphicsInView) { + _unSubscribeToGraphicsInView(); + } else { + _subscribeToGraphicsInView(); + } + }, + child: _subscribedToGraphicsInView + ? const Text("Stop printing Graphics") + : const Text("Start printing Graphics"), + ), + if (kIsWeb) + ElevatedButton( + onPressed: () { + final graphicIdsInView = + _controller?.getVisibleGraphicIds(); + graphicIdsInView?.forEach(debugPrint); + }, + child: const Text("Print visible Graphics"), + ), ElevatedButton( onPressed: () { _addPolygon( @@ -654,16 +668,23 @@ class _ExampleMapState extends State { ), child: const Text('Remove red polygon'), ), + ElevatedButton( + onPressed: () => _controller?.retryLoad(), + child: const Text('Reload map'), + ), + if (!kIsWeb) + ElevatedButton( + onPressed: () => _makePolylineVisible( + points: [ + _firstPinCoordinates, + _secondPinCoordinates + ], + ), + child: const Text('Zoom to polyline'), + ), ], ), ), - if (!kIsWeb) - ElevatedButton( - onPressed: () => _makePolylineVisible( - points: [_firstPinCoordinates, _secondPinCoordinates], - ), - child: const Text('Zoom to polyline'), - ), Row( children: [ const Text( @@ -698,4 +719,16 @@ class _ExampleMapState extends State { MaterialPageRoute(builder: (_) => const VectorLayerExamplePage()), ); } + + void _onMapStatusChanged(MapStatus status) { + final scaffoldContext = _scaffoldKey.currentContext; + if (scaffoldContext == null) return; + + ScaffoldMessenger.of(scaffoldContext).showSnackBar( + SnackBar( + content: Text('Map status changed to: $status'), + duration: const Duration(seconds: 1), + ), + ); + } } diff --git a/example/lib/vector_layer_example_page.dart b/example/lib/vector_layer_example_page.dart index 8358eec3..07276bc8 100644 --- a/example/lib/vector_layer_example_page.dart +++ b/example/lib/vector_layer_example_page.dart @@ -14,18 +14,14 @@ class _VectorLayerExamplePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), - body: Stack( - children: [ - ArcgisMap( - apiKey: arcGisApiKey, - initialCenter: const LatLng(51.16, 10.45), - zoom: 13, - vectorTileLayerUrls: const [ - "https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer", - ], - mapStyle: MapStyle.twoD, - ), + body: ArcgisMap( + apiKey: arcGisApiKey, + initialCenter: const LatLng(51.16, 10.45), + zoom: 13, + vectorTileLayerUrls: const [ + "https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer", ], + mapStyle: MapStyle.twoD, ), ); }