Skip to content

Commit 1681884

Browse files
image-export (#77)
* implement image-export * add layer status observation * remove map status observeration * implement image export on android * update readme * update docs
1 parent ae2a758 commit 1681884

File tree

8 files changed

+206
-4
lines changed

8 files changed

+206
-4
lines changed

arcgis_map_sdk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Checkout the example app `example/lib/main.dart` for more details.
109109
| toggleBaseMap ||||
110110
| moveCamera ||||
111111
| moveCameraToPoints | |||
112+
| exportImage | |||
112113
| zoomIn ||||
113114
| zoomOut ||||
114115
| getZoom ||||

arcgis_map_sdk/lib/src/arcgis_map_controller.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ class ArcgisMapController {
5252
);
5353
}
5454

55+
/// Exports an image of the currently visible map view containing all
56+
/// layers of that view.
57+
Future<Uint8List> exportImage() {
58+
return ArcgisMapPlatform.instance.exportImage(mapId);
59+
}
60+
5561
Future<GraphicsLayer> addGraphicsLayer({
5662
required String layerId,
5763
required GraphicsLayerOptions options,

arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.fluttercommunity.arcgis_map_sdk_android
22

33
import android.content.Context
4+
import android.graphics.Bitmap
45
import android.view.LayoutInflater
56
import android.view.View
67
import com.esri.arcgisruntime.ArcGISRuntimeEnvironment
@@ -39,6 +40,7 @@ import io.flutter.plugin.common.EventChannel
3940
import io.flutter.plugin.common.MethodCall
4041
import io.flutter.plugin.common.MethodChannel
4142
import io.flutter.plugin.platform.PlatformView
43+
import java.io.ByteArrayOutputStream
4244
import kotlin.math.exp
4345
import kotlin.math.ln
4446
import kotlin.math.roundToInt
@@ -188,6 +190,8 @@ internal class ArcgisMapView(
188190
result
189191
)
190192

193+
"export_image" -> onExportImage(result)
194+
191195
else -> result.notImplemented()
192196
}
193197
}
@@ -517,6 +521,20 @@ internal class ArcgisMapView(
517521
}
518522
}
519523

524+
private fun onExportImage(result: MethodChannel.Result) {
525+
result.finishWithFuture(
526+
mapResult = { bitmap ->
527+
val stream = ByteArrayOutputStream()
528+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
529+
val byteArray = stream.toByteArray()
530+
bitmap.recycle()
531+
byteArray
532+
},
533+
getFuture = { mapView.exportImageAsync() }
534+
)
535+
536+
}
537+
520538
/**
521539
* Convert map scale to zoom level
522540
* https://developers.arcgis.com/documentation/mapping-apis-and-services/reference/zoom-levels-and-scale/#conversion-tool
@@ -561,13 +579,23 @@ internal class ArcgisMapView(
561579

562580
// region helper methods
563581

564-
private fun MethodChannel.Result.finishWithFuture(function: () -> ListenableFuture<*>) {
582+
/**
583+
* Safely awaits the provide future and respond to the MethodChannel with the result
584+
* or an error.
585+
*
586+
* @param mapResult optional transformation of the returned value of the future. If null will default to Boolean true.
587+
* @param getFuture A callback that returns the future that will be awaited. This invocation is also caught.
588+
*/
589+
private fun <T> MethodChannel.Result.finishWithFuture(
590+
mapResult: (T) -> Any = { _ -> true },
591+
getFuture: () -> ListenableFuture<T>
592+
) {
565593
try {
566-
val future = function()
594+
val future = getFuture()
567595
future.addDoneListener {
568596
try {
569-
future.get()
570-
success(true)
597+
val result = future.get()
598+
success(mapResult(result))
571599
} catch (e: Throwable) {
572600
finishWithError(e)
573601
}

arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
8888
}
8989
map.basemap = AGSBasemap(baseLayers: layers, referenceLayers: nil)
9090
}
91+
9192

9293
map.minScale = convertZoomLevelToMapScale(mapOptions.minZoom)
9394
map.maxScale = convertZoomLevelToMapScale(mapOptions.maxZoom)
@@ -158,6 +159,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
158159
case "location_display_update_display_source_position_manually" : onUpdateLocationDisplaySourcePositionManually(call, result)
159160
case "location_display_set_data_source_type" : onSetLocationDisplayDataSourceType(call, result)
160161
case "update_is_attribution_text_visible": onUpdateIsAttributionTextVisible(call, result)
162+
case "export_image" : onExportImage(result)
161163
default:
162164
result(FlutterError(code: "Unimplemented", message: "No method matching the name \(call.method)", details: nil))
163165
}
@@ -516,6 +518,21 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
516518
mapView.isAttributionTextVisible = isVisible
517519
result(true)
518520
}
521+
522+
private func onExportImage(_ result: @escaping FlutterResult) {
523+
mapView.exportImage { image, error in
524+
if let error = error {
525+
result(FlutterError(code: "export_error", message: error.localizedDescription, details: nil))
526+
return
527+
}
528+
529+
if let image = image, let imageData = image.pngData() {
530+
result(FlutterStandardTypedData(bytes: imageData))
531+
} else {
532+
result(FlutterError(code: "conversion_error", message: "Failed to convert image to PNG data", details: nil))
533+
}
534+
}
535+
}
519536

520537
private func operationWithSymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, handler: (AGSSymbol) -> Void) {
521538
do {

arcgis_map_sdk_method_channel/lib/src/method_channel_arcgis_map_plugin.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class MethodChannelArcgisMapPlugin extends ArcgisMapPlatform {
2929
throw UnimplementedError('addFeatureLayer() has not been implemented.');
3030
}
3131

32+
@override
33+
Future<Uint8List> exportImage(int mapId) {
34+
return _methodChannelBuilder(mapId)
35+
.invokeMethod<Uint8List>("export_image")
36+
.then((value) => value!);
37+
}
38+
3239
@override
3340
void setMouseCursor(SystemMouseCursor cursor, int mapId) {
3441
throw UnimplementedError('setMouseCursor() has not been implemented');

arcgis_map_sdk_platform_interface/lib/src/arcgis_map_sdk_platform_interface.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class ArcgisMapPlatform extends PlatformInterface {
2222
throw UnimplementedError('init() has not been implemented.');
2323
}
2424

25+
Future<Uint8List> exportImage(int mapId) {
26+
throw UnimplementedError('exportImage() has not been implemented.');
27+
}
28+
2529
Future<FeatureLayer> addFeatureLayer(
2630
FeatureLayerOptions options,
2731
List<Graphic>? data,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:arcgis_example/main.dart';
4+
import 'package:arcgis_map_sdk/arcgis_map_sdk.dart';
5+
import 'package:flutter/material.dart';
6+
7+
class ExportImageExamplePage extends StatefulWidget {
8+
const ExportImageExamplePage({super.key});
9+
10+
@override
11+
State<ExportImageExamplePage> createState() => _ExportImageExamplePageState();
12+
}
13+
14+
class _ExportImageExamplePageState extends State<ExportImageExamplePage> {
15+
final _snackBarKey = GlobalKey<ScaffoldState>();
16+
ArcgisMapController? _controller;
17+
18+
Uint8List? _imageBytes;
19+
final initialCenter = const LatLng(51.16, 10.45);
20+
late final start = initialCenter;
21+
final end = const LatLng(51.16551, 10.45221);
22+
23+
@override
24+
Widget build(BuildContext context) {
25+
return Scaffold(
26+
key: _snackBarKey,
27+
appBar: AppBar(),
28+
floatingActionButton: FloatingActionButton(
29+
child: Icon(Icons.refresh),
30+
onPressed: () async {
31+
try {
32+
final image = await _controller!.exportImage();
33+
if (!mounted) return;
34+
setState(() => _imageBytes = image);
35+
} catch (e, stack) {
36+
if (!mounted) return;
37+
ScaffoldMessenger.of(_snackBarKey.currentContext!)
38+
.showSnackBar(SnackBar(content: Text("$e")));
39+
debugPrint("$e");
40+
debugPrintStack(stackTrace: stack);
41+
}
42+
},
43+
),
44+
body: Column(
45+
children: [
46+
Expanded(
47+
child: ArcgisMap(
48+
apiKey: arcGisApiKey,
49+
initialCenter: initialCenter,
50+
zoom: 12,
51+
basemap: BaseMap.arcgisNavigationNight,
52+
mapStyle: MapStyle.twoD,
53+
onMapCreated: (controller) {
54+
_controller = controller;
55+
56+
controller.addGraphic(
57+
layerId: "pin",
58+
graphic: PointGraphic(
59+
longitude: initialCenter.longitude,
60+
latitude: initialCenter.latitude,
61+
height: 20,
62+
attributes: Attributes({
63+
'id': "pin1",
64+
'name': "pin1",
65+
'family': 'Pins',
66+
}),
67+
symbol: PictureMarkerSymbol(
68+
assetUri: 'assets/navPointer.png',
69+
width: 56,
70+
height: 56,
71+
),
72+
),
73+
);
74+
75+
controller.addGraphic(
76+
layerId: "line",
77+
graphic: PolylineGraphic(
78+
paths: [
79+
[
80+
[start.longitude, start.latitude, 10.0],
81+
[end.longitude, end.latitude, 10.0],
82+
]
83+
],
84+
symbol: const SimpleLineSymbol(
85+
color: Colors.purple,
86+
style: PolylineStyle.shortDashDotDot,
87+
width: 3,
88+
marker: LineSymbolMarker(
89+
color: Colors.green,
90+
colorOpacity: 1,
91+
style: MarkerStyle.circle,
92+
),
93+
),
94+
attributes: Attributes({'id': "line-1", 'name': "line-1"}),
95+
),
96+
);
97+
},
98+
),
99+
),
100+
const Divider(),
101+
Text(
102+
_imageBytes == null
103+
? "Press the button to generate an image"
104+
: "This image is a screenshot of the mapview! ⬇️",
105+
style: Theme.of(context).textTheme.bodyLarge,
106+
),
107+
const SizedBox(height: 8),
108+
Expanded(
109+
child: _imageBytes == null
110+
? SizedBox()
111+
: Container(
112+
padding: EdgeInsets.all(16),
113+
decoration: BoxDecoration(
114+
color: Theme.of(context).primaryColor,
115+
borderRadius: BorderRadius.circular(12),
116+
),
117+
child: ClipRRect(
118+
child: Image.memory(_imageBytes!),
119+
borderRadius: BorderRadius.circular(12),
120+
),
121+
),
122+
),
123+
SizedBox(height: MediaQuery.paddingOf(context).bottom),
124+
],
125+
),
126+
);
127+
}
128+
}

example/lib/main.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:core';
33

4+
import 'package:arcgis_example/export_image_example_page.dart';
45
import 'package:arcgis_example/location_indicator_example_page.dart';
56
import 'package:arcgis_example/map_elements.dart';
67
import 'package:arcgis_example/vector_layer_example_page.dart';
@@ -515,6 +516,10 @@ class _ExampleMapState extends State<ExampleMap> {
515516
onPressed: _routeToVectorLayerMap,
516517
child: const Text("Show Vector layer example"),
517518
),
519+
ElevatedButton(
520+
onPressed: _routeToExportImageExample,
521+
child: const Text("Show export image example"),
522+
),
518523
ElevatedButton(
519524
onPressed: _routeToLocationIndicatorExample,
520525
child: const Text("Location indicator example"),
@@ -808,4 +813,10 @@ class _ExampleMapState extends State<ExampleMap> {
808813
MaterialPageRoute(builder: (_) => const LocationIndicatorExamplePage()),
809814
);
810815
}
816+
817+
void _routeToExportImageExample() {
818+
Navigator.of(context).push(
819+
MaterialPageRoute(builder: (_) => const ExportImageExamplePage()),
820+
);
821+
}
811822
}

0 commit comments

Comments
 (0)