diff --git a/example/lib/basemap_style_example_page.dart b/example/lib/basemap_style_example_page.dart index 3f9e0859..2c4836bc 100644 --- a/example/lib/basemap_style_example_page.dart +++ b/example/lib/basemap_style_example_page.dart @@ -21,7 +21,6 @@ class _BasemapStyleExamplePageState extends State { BaseMap selectedBasemap = BaseMap.values.first; var _isSwitchingAllStyles = false; bool show3dMap = false; - bool isBasemapMenuOpened = false; final initialCenter = const LatLng(51.16, 10.45); final tappedHQ = const LatLng(48.1234963, 11.5910182); @@ -65,201 +64,193 @@ class _BasemapStyleExamplePageState extends State { Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, - body: GestureDetector( - onTap: () { - setState(() { - isBasemapMenuOpened = false; - }); - }, - child: Stack( - children: [ - ArcgisMap( - mapStyle: show3dMap ? MapStyle.threeD : MapStyle.twoD, - apiKey: arcGisApiKey, - basemap: BaseMap.osmDarkGray, - ground: show3dMap ? Ground.worldElevation : null, - showLabelsBeneathGraphics: true, - initialCenter: initialCenter, - zoom: 8, - rotationEnabled: true, - onMapCreated: _onMapCreated, - defaultUiList: [ - DefaultWidget( - viewType: DefaultWidgetType.compass, - position: WidgetPosition.topRight, - ), - ], - ), - Positioned( - top: MediaQuery.paddingOf(context).top + 8, - left: 8, - child: BackButton( - color: Colors.black, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Colors.grey)), + body: Stack( + children: [ + ArcgisMap( + mapStyle: show3dMap ? MapStyle.threeD : MapStyle.twoD, + apiKey: arcGisApiKey, + basemap: BaseMap.osmDarkGray, + ground: show3dMap ? Ground.worldElevation : null, + showLabelsBeneathGraphics: true, + initialCenter: initialCenter, + zoom: 8, + rotationEnabled: true, + onMapCreated: _onMapCreated, + defaultUiList: [ + DefaultWidget( + viewType: DefaultWidgetType.compass, + position: WidgetPosition.topRight, ), + ], + ), + Positioned( + top: MediaQuery.paddingOf(context).top + 8, + left: 8, + child: BackButton( + color: Colors.black, + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.grey)), ), - Positioned( - bottom: 40, - right: 0, - left: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FloatingActionButton( - heroTag: "zoom-in-button", - onPressed: () { - _controller?.zoomIn( - lodFactor: 1, - animationOptions: AnimationOptions( - duration: 1000, - animationCurve: AnimationCurve.easeIn, - ), - ); - }, - backgroundColor: Colors.grey, - child: const Icon(Icons.add), - ), - FloatingActionButton( - heroTag: "zoom-out-button", - onPressed: () { - _controller?.zoomOut( - lodFactor: 1, - animationOptions: AnimationOptions( - duration: 1000, - animationCurve: AnimationCurve.easeIn, - ), - ); - }, - backgroundColor: Colors.grey, - child: const Icon(Icons.remove), - ), + ), + Positioned( + bottom: 40, + right: 0, + left: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FloatingActionButton( + heroTag: "zoom-in-button", + onPressed: () { + _controller?.zoomIn( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.add), + ), + FloatingActionButton( + heroTag: "zoom-out-button", + onPressed: () { + _controller?.zoomOut( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.remove), + ), + FloatingActionButton( + heroTag: "move-camera-button", + backgroundColor: Colors.red, + child: const Icon(Icons.place_outlined), + onPressed: () { + _controller?.moveCamera( + point: tappedHQ, + zoomLevel: 8.0, + threeDHeading: 30, + threeDTilt: 60, + animationOptions: AnimationOptions( + duration: 1500, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + ), + if (kIsWeb) FloatingActionButton( - heroTag: "move-camera-button", - backgroundColor: Colors.red, - child: const Icon(Icons.place_outlined), + heroTag: "3d-map-button", onPressed: () { - _controller?.moveCamera( - point: tappedHQ, - zoomLevel: 8.0, - threeDHeading: 30, - threeDTilt: 60, - animationOptions: AnimationOptions( - duration: 1500, - animationCurve: AnimationCurve.easeIn, - ), - ); + 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'), - ), - ], - ), - SizedBox(height: 8), - Center( - child: ElevatedButton( - onPressed: - _isSwitchingAllStyles ? null : _switchAllStyles, - child: Text(_isSwitchingAllStyles - ? "Switching... ${BaseMap.values.indexOf(selectedBasemap) + 1}/${BaseMap.values.length}" - : "Switch through all styles once")), - ), - Center( - child: ElevatedButton( - style: ButtonStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - ), + ], + ), + SizedBox(height: 8), + Center( + child: ElevatedButton( + onPressed: + _isSwitchingAllStyles ? null : _switchAllStyles, + child: Text(_isSwitchingAllStyles + ? "Switching... ${BaseMap.values.indexOf(selectedBasemap) + 1}/${BaseMap.values.length}" + : "Switch through all styles once")), + ), + Center( + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), ), - onPressed: _isSwitchingAllStyles - ? null - : () { - showModalBottomSheet( - context: context, - builder: (context) { - return PointerInterceptorWeb( - child: ListView.separated( - padding: EdgeInsets.only( - top: 8, - bottom: - MediaQuery.paddingOf(context) - .bottom + - 8), - itemCount: BaseMap.values.length, - separatorBuilder: (_, __) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20), - child: Divider( - height: 1, - ), + ), + onPressed: _isSwitchingAllStyles + ? null + : () { + showModalBottomSheet( + context: context, + builder: (context) { + return PointerInterceptorWeb( + child: ListView.separated( + padding: EdgeInsets.only( + top: 8, + bottom: MediaQuery.paddingOf(context) + .bottom + + 8), + itemCount: BaseMap.values.length, + separatorBuilder: (_, __) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20), + child: Divider( + height: 1, ), - itemBuilder: (context, int i) { - final basemap = BaseMap.values[i]; - return ListTile( - dense: true, - onTap: () { - Navigator.pop(context); - setState(() { - selectedBasemap = basemap; - }); + ), + itemBuilder: (context, int i) { + final basemap = BaseMap.values[i]; + return ListTile( + dense: true, + onTap: () { + Navigator.pop(context); + setState(() { + selectedBasemap = basemap; + }); - _controller!.toggleBaseMap( - baseMap: basemap); - }, - title: Text( - basemap.name, - style: TextStyle( - color: Theme.of(context) - .buttonTheme - .colorScheme! - .onPrimaryContainer, - ), + _controller!.toggleBaseMap( + baseMap: basemap); + }, + title: Text( + basemap.name, + style: TextStyle( + color: Theme.of(context) + .buttonTheme + .colorScheme! + .onPrimaryContainer, ), - ); - }, - ), - ); - }); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - selectedBasemap.name, - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: 15, - ), + ), + ); + }, + ), + ); + }); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + selectedBasemap.name, + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: 15, ), - SizedBox(width: 8), - Icon( - Icons.keyboard_arrow_down, - size: 22, - ) - ], - ), + ), + SizedBox(width: 8), + Icon( + Icons.keyboard_arrow_down, + size: 22, + ) + ], ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/example/lib/draw_on_map_example_page.dart b/example/lib/draw_on_map_example_page.dart new file mode 100644 index 00000000..b04f9b93 --- /dev/null +++ b/example/lib/draw_on_map_example_page.dart @@ -0,0 +1,595 @@ +import 'dart:async'; + +import 'package:arcgis_example/main.dart'; +import 'package:arcgis_example/map_elements.dart'; +import 'package:arcgis_example/pointer_interceptor_web.dart'; +import 'package:arcgis_map_sdk/arcgis_map_sdk.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class DrawOnMapExamplePage extends StatefulWidget { + const DrawOnMapExamplePage({super.key}); + + @override + State createState() => _DrawOnMapExamplePageState(); +} + +class _DrawOnMapExamplePageState extends State { + static const String _pinId1 = '123'; + final LatLng _firstPinCoordinates = const LatLng(52.9, 13.2); + static const String _pinId2 = '456'; + final LatLng _secondPinCoordinates = const LatLng(51, 11); + static const String _pinFeatureId = '789'; + final LatLng _pinFeatureCoordinates = const LatLng(50.945, 6.965); + static const String _pinLayerId = 'PinLayer'; + static const String _featureLayerId = 'FeatureLayer'; + static const String _polygon1 = 'polygon1'; + static const String _polygon2 = 'polygon2'; + static const String _polyLayerId = 'PolygonLayer'; + static const String _lineLayerId = 'LinesGraphics'; + + /// null when executed on a platform that's not supported yet + ArcgisMapController? _controller; + bool show3dMap = false; + final initialCenter = const LatLng(51.16, 10.45); + final tappedHQ = const LatLng(48.1234963, 11.5910182); + + VoidCallback? _removeStatusListener; + + final _scaffoldKey = GlobalKey(); + + bool _isFirstPinInView = false; + bool _isSecondPinInView = false; + bool _isFeatureInView = false; + + bool _subscribedToGraphicsInView = false; + + final Map _hoveredPolygons = {}; + StreamSubscription? _isGraphicHoveredSubscription; + StreamSubscription>? _graphicsInViewSubscription; + + final _markerSymbol = PictureMarkerSymbol( + assetUri: 'assets/navPointer.png', + width: 56, + height: 56, + ); + + @override + void dispose() { + _isGraphicHoveredSubscription?.cancel(); + _graphicsInViewSubscription?.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) { + if (attributes == null || !mounted) return; + final snackBar = SnackBar( + content: + Text('Attributes Id after on Click: ${attributes.data['name']}'), + ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(snackBar); + }); + + // Create GraphicsLayer with Polygons + await _createGraphicLayer( + layerId: _polyLayerId, + elevationMode: ElevationMode.onTheGround, + ); + + await _createFeatureLayer( + layerId: _featureLayerId, + ); + + // Create GraphicsLayer with Lines + await _createGraphicLayer( + layerId: _lineLayerId, + elevationMode: ElevationMode.onTheGround, + ); + + // Create GraphicsLayer with 3D Pins + await _createGraphicLayer(layerId: _pinLayerId); + + // Subscribe to hover events to change the mouse cursor + _isGraphicHoveredSubscription = + _controller?.isGraphicHoveredStream().listen((bool isHovered) { + _setMouseCursor(isHovered); + }); + } + + _connectTwoPinsWithPolyline( + id: 'connecting-polyline-01', + name: 'Connecting polyline', + start: _firstPinCoordinates, + end: _secondPinCoordinates, + ); + + // Add Polygons to the PolyLayer + _addPolygon( + layerId: _polyLayerId, + graphic: PolygonGraphic( + rings: firstPolygon, + symbol: orangeFillSymbol, + attributes: Attributes({'id': _polygon1, 'name': 'First Polygon'}), + onHover: (isHovered) { + isHovered + ? _updateGraphicSymbol( + layerId: _polyLayerId, + graphicId: _polygon1, + symbol: highlightedOrangeFillSymbol, + ) + : _updateGraphicSymbol( + layerId: _polyLayerId, + graphicId: _polygon1, + symbol: orangeFillSymbol, + ); + if (_hoveredPolygons[_polygon1] == isHovered) return; + _hoveredPolygons[_polygon1] = isHovered; + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + floatingActionButton: FloatingActionButton( + onPressed: _showBottomSheet, + child: Icon(Icons.draw), + ), + body: Stack( + children: [ + ArcgisMap( + mapStyle: show3dMap ? MapStyle.threeD : MapStyle.twoD, + apiKey: arcGisApiKey, + basemap: BaseMap.osmDarkGray, + ground: show3dMap ? Ground.worldElevation : null, + showLabelsBeneathGraphics: true, + initialCenter: initialCenter, + zoom: 8, + rotationEnabled: true, + onMapCreated: _onMapCreated, + defaultUiList: [ + DefaultWidget( + viewType: DefaultWidgetType.compass, + position: WidgetPosition.topRight, + ), + ], + ), + Positioned( + top: MediaQuery.paddingOf(context).top + 8, + left: 8, + child: BackButton( + color: Colors.black, + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.grey)), + ), + ), + Positioned( + bottom: 40, + right: 0, + left: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FloatingActionButton( + heroTag: "zoom-in-button", + onPressed: () { + _controller?.zoomIn( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.add), + ), + FloatingActionButton( + heroTag: "zoom-out-button", + onPressed: () { + _controller?.zoomOut( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.remove), + ), + FloatingActionButton( + heroTag: "move-camera-button", + backgroundColor: Colors.red, + child: const Icon(Icons.place_outlined), + onPressed: () { + _controller?.moveCamera( + point: tappedHQ, + zoomLevel: 8.0, + threeDHeading: 30, + threeDTilt: 60, + animationOptions: AnimationOptions( + duration: 1500, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + ), + 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'), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + 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), + ), + ); + } + + void _setMouseCursor(bool isHovered) { + _controller?.setMouseCursor( + isHovered ? SystemMouseCursors.click : SystemMouseCursors.basic, + ); + } + + void _addPin({ + required String layerId, + required String objectId, + required LatLng location, + }) { + _controller?.addGraphic( + layerId: layerId, + graphic: PointGraphic( + longitude: location.longitude, + latitude: location.latitude, + height: 20, + attributes: Attributes({ + 'id': objectId, + 'name': objectId, + 'family': 'Pins', + }), + symbol: _markerSymbol, + ), + ); + } + + void _updateGraphicSymbol({ + required String layerId, + required String graphicId, + required Symbol symbol, + }) { + _controller?.updateGraphicSymbol( + layerId: layerId, + symbol: symbol, + graphicId: graphicId, + ); + } + + void _subscribeToGraphicsInView() { + _graphicsInViewSubscription?.cancel(); + _graphicsInViewSubscription = _controller + ?.visibleGraphics() + .listen((List visibleGraphicsIds) { + visibleGraphicsIds.forEach(debugPrint); + }); + setState(() { + _subscribedToGraphicsInView = true; + }); + } + + void _unSubscribeToGraphicsInView() { + _graphicsInViewSubscription?.cancel(); + setState(() { + _subscribedToGraphicsInView = false; + }); + } + + Future _createGraphicLayer({ + required String layerId, + ElevationMode elevationMode = ElevationMode.relativeToScene, + }) async { + final layer = await _controller?.addGraphicsLayer( + layerId: layerId, + options: GraphicsLayerOptions( + fields: [ + Field(name: 'oid', type: 'oid'), + Field(name: 'id', type: 'string'), + Field(name: 'family', type: 'string'), + Field(name: 'name', type: 'string'), + ], + featureReduction: FeatureReductionCluster().toJson(), + elevationMode: elevationMode, + ), + ); + return layer; + } + + void _removeGraphic({ + required String layerId, + required String objectId, + }) { + _controller?.removeGraphic(layerId: layerId, objectId: objectId); + } + + Future _createFeatureLayer({ + required String layerId, + }) async { + final layer = await _controller?.addFeatureLayer( + layerId: layerId, + options: FeatureLayerOptions( + fields: [ + Field(name: 'oid', type: 'oid'), + Field(name: 'id', type: 'string'), + Field(name: 'family', type: 'string'), + Field(name: 'name', type: 'string'), + ], + symbol: _markerSymbol, + ), + ); + return layer; + } + + void _makePolylineVisible({required List points}) { + _controller?.moveCameraToPoints( + points: points, + padding: 30, + ); + } + + void _addPolygon({ + required String layerId, + required PolygonGraphic graphic, + }) { + _controller?.addGraphic( + layerId: layerId, + graphic: graphic, + ); + } + + void _connectTwoPinsWithPolyline({ + required String id, + required String name, + required LatLng start, + required LatLng end, + }) { + _controller?.addGraphic( + layerId: _lineLayerId, + graphic: PolylineGraphic( + paths: [ + [ + [start.longitude, start.latitude, 10.0], + [end.longitude, end.latitude, 10.0], + ] + ], + symbol: const SimpleLineSymbol( + color: Colors.purple, + style: PolylineStyle.shortDashDotDot, + width: 3, + marker: LineSymbolMarker( + color: Colors.green, + colorOpacity: 1, + style: MarkerStyle.circle, + ), + ), + attributes: Attributes({'id': id, 'name': name}), + ), + ); + } + + void _showBottomSheet() { + showModalBottomSheet( + context: context, + builder: (context) { + return PointerInterceptorWeb( + child: ListView( + padding: EdgeInsets.only( + top: 16, + bottom: MediaQuery.paddingOf(context).bottom + 8, + right: 16, + left: 16), + children: [ + _buildAction( + onPressed: () { + if (_isFirstPinInView) { + _removeGraphic( + layerId: _pinLayerId, + objectId: _pinId1, + ); + setState(() { + _isFirstPinInView = false; + }); + } else { + _addPin( + layerId: _pinLayerId, + objectId: _pinId1, + location: _firstPinCoordinates, + ); + setState(() { + _isFirstPinInView = true; + }); + } + }, + text: + _isFirstPinInView ? 'Remove first Pin' : 'Add first Pin', + ), + _buildAction( + onPressed: () { + if (_isSecondPinInView) { + _removeGraphic( + layerId: _pinLayerId, + objectId: _pinId2, + ); + setState(() { + _isSecondPinInView = false; + }); + } else { + _addPin( + layerId: _pinLayerId, + objectId: _pinId2, + location: _secondPinCoordinates, + ); + setState(() { + _isSecondPinInView = true; + }); + } + }, + text: _isSecondPinInView + ? 'Remove second Pin' + : 'Add second Pin', + ), + _buildAction( + onPressed: () { + if (_isFeatureInView) { + _removeGraphic( + layerId: _featureLayerId, + objectId: _pinFeatureId, + ); + setState(() { + _isFeatureInView = false; + }); + } else { + _addPin( + layerId: _featureLayerId, + objectId: _pinFeatureId, + location: _pinFeatureCoordinates, + ); + _controller?.moveCamera( + point: _pinFeatureCoordinates, + zoomLevel: 15, + ); + setState(() { + _isFeatureInView = true; + }); + } + }, + text: _isFeatureInView + ? 'Remove FeatureLayer Pin' + : 'Add FeatureLayer Pin', + ), + if (kIsWeb) + _buildAction( + onPressed: () { + if (_subscribedToGraphicsInView) { + _unSubscribeToGraphicsInView(); + } else { + _subscribeToGraphicsInView(); + } + }, + text: _subscribedToGraphicsInView + ? "Stop printing Graphics" + : "Start printing Graphics", + ), + if (kIsWeb) + _buildAction( + onPressed: () { + final graphicIdsInView = + _controller?.getVisibleGraphicIds(); + graphicIdsInView?.forEach(debugPrint); + }, + text: "Print visible Graphics", + ), + _buildAction( + onPressed: () { + _addPolygon( + layerId: _polyLayerId, + graphic: PolygonGraphic( + rings: secondPolygon, + symbol: redFillSymbol, + attributes: Attributes({ + 'id': _polygon2, + 'name': 'Second Polygon', + }), + onHover: (isHovered) { + isHovered + ? _updateGraphicSymbol( + layerId: _polyLayerId, + graphicId: _polygon2, + symbol: highlightedRedFillSymbol, + ) + : _updateGraphicSymbol( + layerId: _polyLayerId, + graphicId: _polygon2, + symbol: redFillSymbol, + ); + if (_hoveredPolygons[_polygon2] == isHovered) { + return; + } + _hoveredPolygons[_polygon2] = isHovered; + }, + ), + ); + }, + text: 'Add red polygon', + ), + _buildAction( + onPressed: () => _removeGraphic( + layerId: _polyLayerId, + objectId: _polygon2, + ), + text: 'Remove red polygon', + ), + _buildAction( + onPressed: () => _makePolylineVisible( + points: [_firstPinCoordinates, _secondPinCoordinates], + ), + text: 'Zoom to polyline', + ), + ], + ), + ); + }); + } + + Widget _buildAction({required VoidCallback onPressed, required String text}) { + return ListTile( + onTap: () { + Navigator.pop(context); + onPressed(); + }, + title: Text( + text, + style: TextStyle(color: Colors.deepPurple), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 9276959e..19d6fe53 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:core'; import 'package:arcgis_example/basemap_style_example_page.dart'; +import 'package:arcgis_example/draw_on_map_example_page.dart'; import 'package:arcgis_example/export_image_example_page.dart'; import 'package:arcgis_example/location_indicator_example_page.dart'; import 'package:arcgis_example/map_elements.dart'; @@ -40,37 +41,18 @@ class ExampleMap extends StatefulWidget { } class _ExampleMapState extends State { - static const String _pinId1 = '123'; - final LatLng _firstPinCoordinates = const LatLng(52.9, 13.2); - static const String _pinId2 = '456'; - final LatLng _secondPinCoordinates = const LatLng(51, 11); - static const String _pinFeatureId = '789'; - final LatLng _pinFeatureCoordinates = const LatLng(50.945, 6.965); - static const String _pinLayerId = 'PinLayer'; - static const String _featureLayerId = 'FeatureLayer'; - static const String _polygon1 = 'polygon1'; - static const String _polygon2 = 'polygon2'; - static const String _polyLayerId = 'PolygonLayer'; - static const String _lineLayerId = 'LinesGraphics'; - /// null when executed on a platform that's not supported yet ArcgisMapController? _controller; StreamSubscription? _boundingBoxSubscription; StreamSubscription? _centerPositionSubscription; - StreamSubscription>? _graphicsInViewSubscription; StreamSubscription? _attributionTextSubscription; StreamSubscription? _zoomSubscription; - StreamSubscription? _isGraphicHoveredSubscription; String _attributionText = ''; bool _subscribedToBounds = false; - bool _isFirstPinInView = false; - bool _isSecondPinInView = false; - bool _isFeatureInView = false; + bool _subscribedToCenterPosition = false; - bool _subscribedToGraphicsInView = false; bool _subscribedToZoom = false; - final Map _hoveredPolygons = {}; bool show3dMap = false; final initialCenter = const LatLng(51.16, 10.45); @@ -85,10 +67,8 @@ class _ExampleMapState extends State { void dispose() { _boundingBoxSubscription?.cancel(); _centerPositionSubscription?.cancel(); - _graphicsInViewSubscription?.cancel(); _attributionTextSubscription?.cancel(); _zoomSubscription?.cancel(); - _isGraphicHoveredSubscription?.cancel(); _removeStatusListener?.call(); super.dispose(); @@ -122,82 +102,7 @@ class _ExampleMapState extends State { // Create basic 3D Layer with elevation and 3D buildings await _createSceneLayer(); - - // Create GraphicsLayer with Polygons - await _createGraphicLayer( - layerId: _polyLayerId, - elevationMode: ElevationMode.onTheGround, - ); - - await _createFeatureLayer( - layerId: _featureLayerId, - ); - - // Create GraphicsLayer with Lines - await _createGraphicLayer( - layerId: _lineLayerId, - elevationMode: ElevationMode.onTheGround, - ); - - // Create GraphicsLayer with 3D Pins - await _createGraphicLayer(layerId: _pinLayerId); - - // Subscribe to hover events to change the mouse cursor - _isGraphicHoveredSubscription = - _controller?.isGraphicHoveredStream().listen((bool isHovered) { - _setMouseCursor(isHovered); - }); } - - _connectTwoPinsWithPolyline( - id: 'connecting-polyline-01', - name: 'Connecting polyline', - start: _firstPinCoordinates, - end: _secondPinCoordinates, - ); - - // Add Polygons to the PolyLayer - _addPolygon( - layerId: _polyLayerId, - graphic: PolygonGraphic( - rings: firstPolygon, - symbol: orangeFillSymbol, - attributes: Attributes({'id': _polygon1, 'name': 'First Polygon'}), - onHover: (isHovered) { - isHovered - ? _updateGraphicSymbol( - layerId: _polyLayerId, - graphicId: _polygon1, - symbol: highlightedOrangeFillSymbol, - ) - : _updateGraphicSymbol( - layerId: _polyLayerId, - graphicId: _polygon1, - symbol: orangeFillSymbol, - ); - if (_hoveredPolygons[_polygon1] == isHovered) return; - _hoveredPolygons[_polygon1] = isHovered; - }, - ), - ); - } - - void _updateGraphicSymbol({ - required String layerId, - required String graphicId, - required Symbol symbol, - }) { - _controller?.updateGraphicSymbol( - layerId: layerId, - symbol: symbol, - graphicId: graphicId, - ); - } - - void _setMouseCursor(bool isHovered) { - _controller?.setMouseCursor( - isHovered ? SystemMouseCursors.click : SystemMouseCursors.basic, - ); } void _subscribeToBounds() { @@ -222,25 +127,6 @@ class _ExampleMapState extends State { }); } - void _subscribeToGraphicsInView() { - _graphicsInViewSubscription?.cancel(); - _graphicsInViewSubscription = _controller - ?.visibleGraphics() - .listen((List visibleGraphicsIds) { - visibleGraphicsIds.forEach(debugPrint); - }); - setState(() { - _subscribedToGraphicsInView = true; - }); - } - - void _unSubscribeToGraphicsInView() { - _graphicsInViewSubscription?.cancel(); - setState(() { - _subscribedToGraphicsInView = false; - }); - } - void _subscribeToPos() { _centerPositionSubscription?.cancel(); _centerPositionSubscription = @@ -276,27 +162,6 @@ class _ExampleMapState extends State { }); } - void _addPin({ - required String layerId, - required String objectId, - required LatLng location, - }) { - _controller?.addGraphic( - layerId: layerId, - graphic: PointGraphic( - longitude: location.longitude, - latitude: location.latitude, - height: 20, - attributes: Attributes({ - 'id': objectId, - 'name': objectId, - 'family': 'Pins', - }), - symbol: _markerSymbol, - ), - ); - } - Future _createSceneLayer() async { final layer = await _controller?.addSceneLayer( layerId: '3D Buildings', @@ -309,444 +174,222 @@ class _ExampleMapState extends State { return layer; } - Future _createGraphicLayer({ - required String layerId, - ElevationMode elevationMode = ElevationMode.relativeToScene, - }) async { - final layer = await _controller?.addGraphicsLayer( - layerId: layerId, - options: GraphicsLayerOptions( - fields: [ - Field(name: 'oid', type: 'oid'), - Field(name: 'id', type: 'string'), - Field(name: 'family', type: 'string'), - Field(name: 'name', type: 'string'), - ], - featureReduction: FeatureReductionCluster().toJson(), - elevationMode: elevationMode, - ), - ); - return layer; - } - - void _removeGraphic({ - required String layerId, - required String objectId, - }) { - _controller?.removeGraphic(layerId: layerId, objectId: objectId); - } - - Future _createFeatureLayer({ - required String layerId, - }) async { - final layer = await _controller?.addFeatureLayer( - layerId: layerId, - options: FeatureLayerOptions( - fields: [ - Field(name: 'oid', type: 'oid'), - Field(name: 'id', type: 'string'), - Field(name: 'family', type: 'string'), - Field(name: 'name', type: 'string'), - ], - symbol: _markerSymbol, - ), - ); - return layer; - } - - void _makePolylineVisible({required List points}) { - _controller?.moveCameraToPoints( - points: points, - padding: 30, - ); - } - - void _addPolygon({ - required String layerId, - required PolygonGraphic graphic, - }) { - _controller?.addGraphic( - layerId: layerId, - graphic: graphic, - ); - } - - void _connectTwoPinsWithPolyline({ - required String id, - required String name, - required LatLng start, - required LatLng end, - }) { - _controller?.addGraphic( - layerId: _lineLayerId, - graphic: PolylineGraphic( - paths: [ - [ - [start.longitude, start.latitude, 10.0], - [end.longitude, end.latitude, 10.0], - ] - ], - symbol: const SimpleLineSymbol( - color: Colors.purple, - style: PolylineStyle.shortDashDotDot, - width: 3, - marker: LineSymbolMarker( - color: Colors.green, - colorOpacity: 1, - style: MarkerStyle.circle, - ), - ), - attributes: Attributes({'id': id, 'name': name}), - ), - ); - } - @override Widget build(BuildContext context) { + final isLargeScreen = MediaQuery.sizeOf(context).width > 800; return Scaffold( key: _scaffoldKey, - body: Stack( + drawer: isLargeScreen + ? null + : NavigationDrawer(children: _buildTiles(context)), + body: Row( children: [ - ArcgisMap( - mapStyle: show3dMap ? MapStyle.threeD : MapStyle.twoD, - apiKey: arcGisApiKey, - basemap: BaseMap.osmDarkGray, - ground: show3dMap ? Ground.worldElevation : null, - showLabelsBeneathGraphics: true, - initialCenter: initialCenter, - zoom: 8, - rotationEnabled: true, - onMapCreated: _onMapCreated, - defaultUiList: [ - DefaultWidget( - viewType: DefaultWidgetType.compass, - position: WidgetPosition.topRight, + if (isLargeScreen) + SizedBox( + width: 250, + child: ListView( + children: _buildTiles(context), ), - ], - ), - Positioned( - bottom: 40, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + Expanded( + child: Stack( children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, + ArcgisMap( + mapStyle: show3dMap ? MapStyle.threeD : MapStyle.twoD, + apiKey: arcGisApiKey, + basemap: BaseMap.osmDarkGray, + ground: show3dMap ? Ground.worldElevation : null, + showLabelsBeneathGraphics: true, + initialCenter: initialCenter, + zoom: 8, + rotationEnabled: true, + onMapCreated: _onMapCreated, + defaultUiList: [ + DefaultWidget( + viewType: DefaultWidgetType.compass, + position: WidgetPosition.topRight, + ), + ], + ), + Positioned( + bottom: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - FloatingActionButton( - heroTag: "zoom-in-button", - onPressed: () { - _controller?.zoomIn( - lodFactor: 1, - animationOptions: AnimationOptions( - duration: 1000, - animationCurve: AnimationCurve.easeIn, + SizedBox( + width: MediaQuery.of(context).size.width, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Row( + children: [ + FloatingActionButton( + onPressed: () { + _controller?.zoomIn( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.add), ), - ); - }, - backgroundColor: Colors.grey, - child: const Icon(Icons.add), - ), - FloatingActionButton( - heroTag: "zoom-out-button", - onPressed: () { - _controller?.zoomOut( - lodFactor: 1, - animationOptions: AnimationOptions( - duration: 1000, - animationCurve: AnimationCurve.easeIn, + FloatingActionButton( + onPressed: () { + _controller?.zoomOut( + lodFactor: 1, + animationOptions: AnimationOptions( + duration: 1000, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, + backgroundColor: Colors.grey, + child: const Icon(Icons.remove), ), - ); - }, - backgroundColor: Colors.grey, - child: const Icon(Icons.remove), - ), - FloatingActionButton( - heroTag: "move-camera-button", - backgroundColor: Colors.red, - child: const Icon(Icons.place_outlined), - onPressed: () { - _controller?.moveCamera( - point: tappedHQ, - zoomLevel: 8.0, - threeDHeading: 30, - threeDTilt: 60, - animationOptions: AnimationOptions( - duration: 1500, - animationCurve: AnimationCurve.easeIn, + FloatingActionButton( + backgroundColor: Colors.red, + child: const Icon(Icons.place_outlined), + onPressed: () { + _controller?.moveCamera( + point: tappedHQ, + zoomLevel: 8.0, + threeDHeading: 30, + threeDTilt: 60, + animationOptions: AnimationOptions( + duration: 1500, + animationCurve: AnimationCurve.easeIn, + ), + ); + }, ), - ); - }, - ), - if (kIsWeb) - FloatingActionButton( - heroTag: "3d-map-button", + 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( onPressed: () { + _controller?.setInteraction( + isEnabled: !_isInteractionEnabled, + ); + setState(() { - show3dMap = !show3dMap; - _controller?.switchMapStyle( - show3dMap ? MapStyle.threeD : MapStyle.twoD, - ); + _isInteractionEnabled = + !_isInteractionEnabled; }); }, - backgroundColor: - show3dMap ? Colors.red : Colors.blue, - child: Text(show3dMap ? '3D' : '2D'), + child: Text( + "${_isInteractionEnabled ? "Disable" : "Enable"} Interaction", + ), ), - ], - ), - ElevatedButton( - onPressed: () { - _controller?.setInteraction( - isEnabled: !_isInteractionEnabled, - ); - - setState(() { - _isInteractionEnabled = !_isInteractionEnabled; - }); - }, - child: Text( - "${_isInteractionEnabled ? "Disable" : "Enable"} Interaction", - ), - ), - ElevatedButton( - onPressed: _routeToVectorLayerMap, - child: const Text("Show Vector layer example"), - ), - ElevatedButton( - onPressed: _routeToExportImageExample, - child: const Text("Show export image example"), - ), - ElevatedButton( - onPressed: _routeToLocationIndicatorExample, - child: const Text("Location indicator example"), - ), - ElevatedButton( - onPressed: _routeToBasemapStyleExamplePage, - child: const Text("Basemap example"), - ), - ElevatedButton( - onPressed: () { - _controller?.addViewPadding( - padding: const ViewPadding(right: 300), - ); - }, - child: const Text("Add 300 right"), - ), - ElevatedButton( - onPressed: () { - _controller?.addViewPadding( - padding: const ViewPadding(left: 300), - ); - }, - child: const Text("Add 300 left"), - ), - ElevatedButton( - onPressed: () { - if (_isFirstPinInView) { - _removeGraphic( - layerId: _pinLayerId, - objectId: _pinId1, - ); - setState(() { - _isFirstPinInView = false; - }); - } else { - _addPin( - layerId: _pinLayerId, - objectId: _pinId1, - location: _firstPinCoordinates, - ); - setState(() { - _isFirstPinInView = true; - }); - } - }, - child: _isFirstPinInView - ? const Text('Remove first Pin') - : const Text('Add first Pin'), - ), - ElevatedButton( - onPressed: () { - if (_isSecondPinInView) { - _removeGraphic( - layerId: _pinLayerId, - objectId: _pinId2, - ); - setState(() { - _isSecondPinInView = false; - }); - } else { - _addPin( - layerId: _pinLayerId, - objectId: _pinId2, - location: _secondPinCoordinates, - ); - setState(() { - _isSecondPinInView = true; - }); - } - }, - child: _isSecondPinInView - ? const Text('Remove second Pin') - : const Text('Add second Pin'), - ), - ElevatedButton( - onPressed: () { - if (_isFeatureInView) { - _removeGraphic( - layerId: _featureLayerId, - objectId: _pinFeatureId, - ); - setState(() { - _isFeatureInView = false; - }); - } else { - _addPin( - layerId: _featureLayerId, - objectId: _pinFeatureId, - location: _pinFeatureCoordinates, - ); - _controller?.moveCamera( - point: _pinFeatureCoordinates, - zoomLevel: 15, - ); - setState(() { - _isFeatureInView = true; - }); - } - }, - child: _isFeatureInView - ? const Text('Remove FeatureLayer Pin') - : const Text('Add FeatureLayer Pin'), - ), - ElevatedButton( - onPressed: () { - if (_subscribedToZoom) { - _unsubscribeFromZoom(); - } else { - _subscribeToZoom(); - } - }, - child: _subscribedToZoom - ? const Text('Stop zoom') - : const Text('Sub to zoom'), - ), - ElevatedButton( - onPressed: () { - if (_subscribedToCenterPosition) { - _unsubscribeFromPos(); - } else { - _subscribeToPos(); - } - }, - child: _subscribedToCenterPosition - ? const Text("Stop pos") - : const Text("Sub to pos"), - ), - 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( - layerId: _polyLayerId, - graphic: PolygonGraphic( - rings: secondPolygon, - symbol: redFillSymbol, - attributes: Attributes({ - 'id': _polygon2, - 'name': 'Second Polygon', - }), - onHover: (isHovered) { - isHovered - ? _updateGraphicSymbol( - layerId: _polyLayerId, - graphicId: _polygon2, - symbol: highlightedRedFillSymbol, - ) - : _updateGraphicSymbol( - layerId: _polyLayerId, - graphicId: _polygon2, - symbol: redFillSymbol, - ); - if (_hoveredPolygons[_polygon2] == isHovered) { - return; + ElevatedButton( + onPressed: () { + _controller?.addViewPadding( + padding: const ViewPadding(right: 300), + ); + }, + child: const Text("Add 300 right"), + ), + ElevatedButton( + onPressed: () { + _controller?.addViewPadding( + padding: const ViewPadding(left: 300), + ); + }, + child: const Text("Add 300 left"), + ), + ElevatedButton( + onPressed: () { + if (_subscribedToZoom) { + _unsubscribeFromZoom(); + } else { + _subscribeToZoom(); } - _hoveredPolygons[_polygon2] = isHovered; }, + child: _subscribedToZoom + ? const Text('Stop zoom') + : const Text('Sub to zoom'), ), - ); - }, - child: const Text('Add red polygon'), - ), - ElevatedButton( - onPressed: () => _removeGraphic( - layerId: _polyLayerId, - objectId: _polygon2, + ElevatedButton( + onPressed: () { + if (_subscribedToCenterPosition) { + _unsubscribeFromPos(); + } else { + _subscribeToPos(); + } + }, + child: _subscribedToCenterPosition + ? const Text("Stop pos") + : const Text("Sub to pos"), + ), + if (kIsWeb) + ElevatedButton( + onPressed: () { + if (_subscribedToBounds) { + _unsubscribeFromBounds(); + } else { + _subscribeToBounds(); + } + }, + child: _subscribedToBounds + ? const Text("Stop bounds") + : const Text("Sub to bounds"), + ), + ElevatedButton( + onPressed: () => _controller?.retryLoad(), + child: const Text('Reload map'), + ), + ], ), - child: const Text('Remove red polygon'), - ), - ElevatedButton( - onPressed: () => _controller?.retryLoad(), - child: const Text('Reload map'), ), - ElevatedButton( - onPressed: () => _makePolylineVisible( - points: [_firstPinCoordinates, _secondPinCoordinates], - ), - child: const Text('Zoom to polyline'), + Row( + children: [ + const Text( + 'Powered by Esri', + style: TextStyle(color: Colors.white), + ), + Text( + _attributionText, + style: const TextStyle(color: Colors.white), + ), + ], ), ], ), ), - Row( - children: [ - const Text( - 'Powered by Esri', - style: TextStyle(color: Colors.white), - ), - Text( - _attributionText, - style: const TextStyle(color: Colors.white), + if (!isLargeScreen) + Positioned( + top: 8 + MediaQuery.paddingOf(context).top, + left: 8, + child: IconButton( + onPressed: () { + _scaffoldKey.currentState!.openDrawer(); + }, + color: Colors.black, + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + Colors.grey.withValues(alpha: 0.5)), + ), + icon: Icon(Icons.menu), ), - ], - ), + ) ], ), ), @@ -764,24 +407,6 @@ class _ExampleMapState extends State { // height: 56, // ); - final _markerSymbol = PictureMarkerSymbol( - assetUri: 'assets/navPointer.png', - width: 56, - height: 56, - ); - - void _routeToVectorLayerMap() { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const VectorLayerExamplePage()), - ); - } - - void _routeToBasemapStyleExamplePage() { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => BasemapStyleExamplePage()), - ); - } - void _onMapStatusChanged(MapStatus status) { final scaffoldContext = _scaffoldKey.currentContext; if (scaffoldContext == null) return; @@ -794,15 +419,64 @@ class _ExampleMapState extends State { ); } - void _routeToLocationIndicatorExample() { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const LocationIndicatorExamplePage()), + List _buildTiles(BuildContext context) { + return [ + _buildTile( + title: "Vector Layer Example", + onTap: () { + _navigateTo(context, VectorLayerExamplePage()); + }, + ), + if (!kIsWeb) + _buildTile( + title: "Export Image Example", + onTap: () { + _navigateTo(context, ExportImageExamplePage()); + }, + ), + if (!kIsWeb) + _buildTile( + title: "Location Indicator Example", + onTap: () { + _navigateTo(context, LocationIndicatorExamplePage()); + }, + ), + _buildTile( + title: "Basemap Style Example", + onTap: () { + _navigateTo(context, BasemapStyleExamplePage()); + }, + ), + _buildTile( + title: "Draw on map Example", + onTap: () { + _navigateTo(context, DrawOnMapExamplePage()); + }, + ), + ]; + } + + Widget _buildTile({ + required String title, + required VoidCallback onTap, + }) { + return Column( + children: [ + ListTile( + onTap: onTap, + title: Text( + title, + style: TextStyle(fontSize: 14, color: Colors.deepPurple), + ), + ), + Divider(height: 1), + ], ); } - void _routeToExportImageExample() { + void _navigateTo(BuildContext context, Widget page) { Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const ExportImageExamplePage()), + MaterialPageRoute(builder: (_) => page), ); } }