Skip to content

Commit

Permalink
image-export (#77)
Browse files Browse the repository at this point in the history
* implement image-export

* add layer status observation

* remove map status observeration

* implement image export on android

* update readme

* update docs
  • Loading branch information
JulianBissekkou authored Aug 22, 2024
1 parent ae2a758 commit 1681884
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 4 deletions.
1 change: 1 addition & 0 deletions arcgis_map_sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Checkout the example app `example/lib/main.dart` for more details.
| toggleBaseMap ||||
| moveCamera ||||
| moveCameraToPoints | |||
| exportImage | |||
| zoomIn ||||
| zoomOut ||||
| getZoom ||||
Expand Down
6 changes: 6 additions & 0 deletions arcgis_map_sdk/lib/src/arcgis_map_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class ArcgisMapController {
);
}

/// Exports an image of the currently visible map view containing all
/// layers of that view.
Future<Uint8List> exportImage() {
return ArcgisMapPlatform.instance.exportImage(mapId);
}

Future<GraphicsLayer> addGraphicsLayer({
required String layerId,
required GraphicsLayerOptions options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.fluttercommunity.arcgis_map_sdk_android

import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import com.esri.arcgisruntime.ArcGISRuntimeEnvironment
Expand Down Expand Up @@ -39,6 +40,7 @@ import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.platform.PlatformView
import java.io.ByteArrayOutputStream
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.roundToInt
Expand Down Expand Up @@ -188,6 +190,8 @@ internal class ArcgisMapView(
result
)

"export_image" -> onExportImage(result)

else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -517,6 +521,20 @@ internal class ArcgisMapView(
}
}

private fun onExportImage(result: MethodChannel.Result) {
result.finishWithFuture(
mapResult = { bitmap ->
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
val byteArray = stream.toByteArray()
bitmap.recycle()
byteArray
},
getFuture = { mapView.exportImageAsync() }
)

}

/**
* Convert map scale to zoom level
* https://developers.arcgis.com/documentation/mapping-apis-and-services/reference/zoom-levels-and-scale/#conversion-tool
Expand Down Expand Up @@ -561,13 +579,23 @@ internal class ArcgisMapView(

// region helper methods

private fun MethodChannel.Result.finishWithFuture(function: () -> ListenableFuture<*>) {
/**
* Safely awaits the provide future and respond to the MethodChannel with the result
* or an error.
*
* @param mapResult optional transformation of the returned value of the future. If null will default to Boolean true.
* @param getFuture A callback that returns the future that will be awaited. This invocation is also caught.
*/
private fun <T> MethodChannel.Result.finishWithFuture(
mapResult: (T) -> Any = { _ -> true },
getFuture: () -> ListenableFuture<T>
) {
try {
val future = function()
val future = getFuture()
future.addDoneListener {
try {
future.get()
success(true)
val result = future.get()
success(mapResult(result))
} catch (e: Throwable) {
finishWithError(e)
}
Expand Down
17 changes: 17 additions & 0 deletions arcgis_map_sdk_ios/ios/Classes/ArcgisMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
}
map.basemap = AGSBasemap(baseLayers: layers, referenceLayers: nil)
}


map.minScale = convertZoomLevelToMapScale(mapOptions.minZoom)
map.maxScale = convertZoomLevelToMapScale(mapOptions.maxZoom)
Expand Down Expand Up @@ -158,6 +159,7 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
case "location_display_update_display_source_position_manually" : onUpdateLocationDisplaySourcePositionManually(call, result)
case "location_display_set_data_source_type" : onSetLocationDisplayDataSourceType(call, result)
case "update_is_attribution_text_visible": onUpdateIsAttributionTextVisible(call, result)
case "export_image" : onExportImage(result)
default:
result(FlutterError(code: "Unimplemented", message: "No method matching the name \(call.method)", details: nil))
}
Expand Down Expand Up @@ -516,6 +518,21 @@ class ArcgisMapView: NSObject, FlutterPlatformView {
mapView.isAttributionTextVisible = isVisible
result(true)
}

private func onExportImage(_ result: @escaping FlutterResult) {
mapView.exportImage { image, error in
if let error = error {
result(FlutterError(code: "export_error", message: error.localizedDescription, details: nil))
return
}

if let image = image, let imageData = image.pngData() {
result(FlutterStandardTypedData(bytes: imageData))
} else {
result(FlutterError(code: "conversion_error", message: "Failed to convert image to PNG data", details: nil))
}
}
}

private func operationWithSymbol(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, handler: (AGSSymbol) -> Void) {
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class MethodChannelArcgisMapPlugin extends ArcgisMapPlatform {
throw UnimplementedError('addFeatureLayer() has not been implemented.');
}

@override
Future<Uint8List> exportImage(int mapId) {
return _methodChannelBuilder(mapId)
.invokeMethod<Uint8List>("export_image")
.then((value) => value!);
}

@override
void setMouseCursor(SystemMouseCursor cursor, int mapId) {
throw UnimplementedError('setMouseCursor() has not been implemented');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class ArcgisMapPlatform extends PlatformInterface {
throw UnimplementedError('init() has not been implemented.');
}

Future<Uint8List> exportImage(int mapId) {
throw UnimplementedError('exportImage() has not been implemented.');
}

Future<FeatureLayer> addFeatureLayer(
FeatureLayerOptions options,
List<Graphic>? data,
Expand Down
128 changes: 128 additions & 0 deletions example/lib/export_image_example_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'dart:typed_data';

import 'package:arcgis_example/main.dart';
import 'package:arcgis_map_sdk/arcgis_map_sdk.dart';
import 'package:flutter/material.dart';

class ExportImageExamplePage extends StatefulWidget {
const ExportImageExamplePage({super.key});

@override
State<ExportImageExamplePage> createState() => _ExportImageExamplePageState();
}

class _ExportImageExamplePageState extends State<ExportImageExamplePage> {
final _snackBarKey = GlobalKey<ScaffoldState>();
ArcgisMapController? _controller;

Uint8List? _imageBytes;
final initialCenter = const LatLng(51.16, 10.45);
late final start = initialCenter;
final end = const LatLng(51.16551, 10.45221);

@override
Widget build(BuildContext context) {
return Scaffold(
key: _snackBarKey,
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: () async {
try {
final image = await _controller!.exportImage();
if (!mounted) return;
setState(() => _imageBytes = image);
} catch (e, stack) {
if (!mounted) return;
ScaffoldMessenger.of(_snackBarKey.currentContext!)
.showSnackBar(SnackBar(content: Text("$e")));
debugPrint("$e");
debugPrintStack(stackTrace: stack);
}
},
),
body: Column(
children: [
Expanded(
child: ArcgisMap(
apiKey: arcGisApiKey,
initialCenter: initialCenter,
zoom: 12,
basemap: BaseMap.arcgisNavigationNight,
mapStyle: MapStyle.twoD,
onMapCreated: (controller) {
_controller = controller;

controller.addGraphic(
layerId: "pin",
graphic: PointGraphic(
longitude: initialCenter.longitude,
latitude: initialCenter.latitude,
height: 20,
attributes: Attributes({
'id': "pin1",
'name': "pin1",
'family': 'Pins',
}),
symbol: PictureMarkerSymbol(
assetUri: 'assets/navPointer.png',
width: 56,
height: 56,
),
),
);

controller.addGraphic(
layerId: "line",
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': "line-1", 'name': "line-1"}),
),
);
},
),
),
const Divider(),
Text(
_imageBytes == null
? "Press the button to generate an image"
: "This image is a screenshot of the mapview! ⬇️",
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
Expanded(
child: _imageBytes == null
? SizedBox()
: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
child: Image.memory(_imageBytes!),
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
],
),
);
}
}
11 changes: 11 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:core';

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';
import 'package:arcgis_example/vector_layer_example_page.dart';
Expand Down Expand Up @@ -515,6 +516,10 @@ class _ExampleMapState extends State<ExampleMap> {
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"),
Expand Down Expand Up @@ -808,4 +813,10 @@ class _ExampleMapState extends State<ExampleMap> {
MaterialPageRoute(builder: (_) => const LocationIndicatorExamplePage()),
);
}

void _routeToExportImageExample() {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ExportImageExamplePage()),
);
}
}

0 comments on commit 1681884

Please sign in to comment.