Skip to content

Commit

Permalink
Create a static map snapshot (flutter-mapbox-gl#1076)
Browse files Browse the repository at this point in the history
* android impl

* take snapshot ios

* config for each platform interface

* take snapshot in ios

* take snap in android && example

* android hybrid composition

* ios test

* test with android hybrid composition

* Update README.md

* use JPG instead of PNG with writeToDisk option & remove unused funtions

* render example result with base64 option

* remove team ID in example

* iOS: use JPG instead of PNG

* rename funtion

* test flutter ci

* test ci

* ci: check swift formatting

* ci: test check java formatting

* ci: check java formatting

* ci: test check java formatting

* document for take snapshot feature

* revert ci config

* migration: jpeg with base64 option

* docs: web support

* feat: web support with base64 option

* ci: test github ci

* lint: ignore unnecessary_import

* ci: reverse config
  • Loading branch information
hungtrn75 authored Sep 28, 2022
1 parent 019ed84 commit 3e490a8
Show file tree
Hide file tree
Showing 30 changed files with 754 additions and 52 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [Map Styles](#map-styles)
- [Offline Sideloading](#offline-sideloading)
- [Downloading Offline Regions](#downloading-offline-regions)
- [Create a static map snapshot](#create-a-static-map-snapshot)
- [Location features](#location-features)
- [Android](#android)
- [iOS](#ios)
Expand Down Expand Up @@ -206,7 +207,25 @@ An offline region is a defined region of a map that is available for use in cond
downloadOfflineRegionStream(offlineRegion, onEvent);
```
## Create a static map snapshot

The snapshotManager generates static raster images of the map.
Each snapshot image depicts a portion of a map defined by an SnapshotOptions object you provide.

* Call `takeSnapshot` with predefined `SnapshotOptions`

```
final renderBox = mapKey.currentContext?.findRenderObject() as RenderBox;
final snapshotOptions = SnapshotOptions(
width: renderBox.size.width,
height: renderBox.size.height,
writeToDisk: true,
withLogo: false,
);
final uri = await mapController?.takeSnapshot(snapshotOptions);
```

## Location features
### Android
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ android {
implementation "com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v9:0.12.0"
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-offline-v9:0.7.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-turf:5.1.0'
}
compileOptions {
sourceCompatibility 1.8
Expand Down
57 changes: 57 additions & 0 deletions android/src/main/java/com/mapbox/mapboxgl/BitmapUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.mapbox.mapboxgl;

import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/** Created by nickitaliano on 10/9/17. */
public class BitmapUtils {
private static final String LOG_TAG = "BitmapUtils";

public static String createTempFile(Context context, Bitmap bitmap) {
File tempFile = null;
FileOutputStream outputStream = null;

try {
tempFile = File.createTempFile(LOG_TAG, ".jpeg", context.getCacheDir());
outputStream = new FileOutputStream(tempFile);
} catch (IOException e) {
Log.w(LOG_TAG, e.getLocalizedMessage());
}

if (tempFile == null) {
return null;
}

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
closeSnapshotOutputStream(outputStream);
return Uri.fromFile(tempFile).toString();
}

public static String createBase64(Bitmap bitmap) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
byte[] bitmapBytes = outputStream.toByteArray();
closeSnapshotOutputStream(outputStream);
String base64Prefix = "data:image/jpeg;base64,";
return base64Prefix + Base64.encodeToString(bitmapBytes, Base64.NO_WRAP);
}

private static void closeSnapshotOutputStream(OutputStream outputStream) {
if (outputStream == null) {
return;
}
try {
outputStream.close();
} catch (IOException e) {
Log.w(LOG_TAG, e.getLocalizedMessage());
}
}
}
38 changes: 38 additions & 0 deletions android/src/main/java/com/mapbox/mapboxgl/GeoJSONUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.mapbox.mapboxgl;

import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Geometry;
import com.mapbox.geojson.GeometryCollection;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.turf.TurfMeasurement;
import java.util.ArrayList;
import java.util.List;

public class GeoJSONUtils {
public static LatLng toLatLng(Point point) {
if (point == null) {
return null;
}
return new LatLng(point.latitude(), point.longitude());
}

private static GeometryCollection toGeometryCollection(List<Feature> features) {
ArrayList<Geometry> geometries = new ArrayList<>();
geometries.ensureCapacity(features.size());
for (Feature feature : features) {
geometries.add(feature.geometry());
}
return GeometryCollection.fromGeometries(geometries);
}

public static LatLngBounds toLatLngBounds(FeatureCollection featureCollection) {
List<Feature> features = featureCollection.features();

double[] bbox = TurfMeasurement.bbox(toGeometryCollection(features));

return LatLngBounds.from(bbox[3], bbox[2], bbox[1], bbox[0]);
}
}
89 changes: 83 additions & 6 deletions android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.mapbox.android.telemetry.TelemetryEnabler;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
Expand All @@ -58,6 +59,8 @@
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.offline.OfflineManager;
import com.mapbox.mapboxsdk.plugins.localization.LocalizationPlugin;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.storage.FileSource;
import com.mapbox.mapboxsdk.style.expressions.Expression;
import com.mapbox.mapboxsdk.style.layers.CircleLayer;
import com.mapbox.mapboxsdk.style.layers.FillExtrusionLayer;
Expand Down Expand Up @@ -85,6 +88,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/** Controller of a single MapboxMaps MapView instance. */
@SuppressLint("MissingPermission")
Expand All @@ -108,6 +112,9 @@ final class MapboxMapController
private final float density;
private final Context context;
private final String styleStringInitial;
private final Set<String> interactiveFeatureLayerIds;
private final Map<String, FeatureCollection> addedFeaturesByLayer;
private final Map<String, MapSnapshotter> mSnapshotterMap;
private MapView mapView;
private MapboxMap mapboxMap;
private boolean trackCameraPosition = false;
Expand All @@ -124,13 +131,8 @@ final class MapboxMapController
private Style style;
private Feature draggedFeature;
private AndroidGesturesManager androidGesturesManager;

private LatLng dragOrigin;
private LatLng dragPrevious;

private Set<String> interactiveFeatureLayerIds;
private Map<String, FeatureCollection> addedFeaturesByLayer;

private LatLngBounds bounds = null;
Style.OnStyleLoaded onStyleLoadedCallback =
new Style.OnStyleLoaded() {
Expand Down Expand Up @@ -174,7 +176,7 @@ public void onStyleLoaded(@NonNull Style style) {
if (dragEnabled) {
this.androidGesturesManager = new AndroidGesturesManager(this.mapView.getContext(), false);
}

this.mSnapshotterMap = new HashMap<>();
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/mapbox_maps_" + id);
methodChannel.setMethodCallHandler(this);
}
Expand Down Expand Up @@ -1185,6 +1187,81 @@ public void onFailure(@NonNull Exception exception) {
result.success(null);
break;
}
case "snapshot#takeSnapshot":
{
FileSource.getInstance(context).activate();
MapSnapshotter.Options snapShotOptions =
new MapSnapshotter.Options(
(int) call.argument("width"), (int) call.argument("height"));

snapShotOptions.withLogo((boolean) call.argument("withLogo"));
Style.Builder styleBuilder = new Style.Builder();
if (call.hasArgument("styleUri")) {
styleBuilder.fromUri((String) call.argument("styleUri"));
} else if (call.hasArgument("styleJson")) {
styleBuilder.fromJson((String) call.argument("styleJson"));
} else {
if (style == null) {
result.error(
"STYLE IS NULL",
"The style is null. Has onStyleLoaded() already been invoked?",
null);
}
styleBuilder.fromUri(style.getUri());
}
snapShotOptions.withStyleBuilder(styleBuilder);
if (call.hasArgument("bounds")) {
FeatureCollection bounds = FeatureCollection.fromJson((String) call.argument("bounds"));
snapShotOptions.withRegion(GeoJSONUtils.toLatLngBounds(bounds));
} else if (call.hasArgument("centerCoordinate")) {
Feature centerPoint = Feature.fromJson((String) call.argument("centerCoordinate"));
CameraPosition cameraPosition =
new CameraPosition.Builder()
.target(GeoJSONUtils.toLatLng((Point) centerPoint.geometry()))
.tilt((double) call.argument("pitch"))
.bearing((double) call.argument("heading"))
.zoom((double) call.argument("zoomLevel"))
.build();
snapShotOptions.withCameraPosition(cameraPosition);
} else {
snapShotOptions.withRegion(mapboxMap.getProjection().getVisibleRegion().latLngBounds);
}

final MapSnapshotter snapshotter = new MapSnapshotter(context, snapShotOptions);
final String snapshotterID = UUID.randomUUID().toString();
mSnapshotterMap.put(snapshotterID, snapshotter);

snapshotter.start(
snapshot -> {
Bitmap bitmap = snapshot.getBitmap();

String result1;
if ((boolean) call.argument("writeToDisk")) {
result1 = BitmapUtils.createTempFile(context, bitmap);
} else {
result1 = BitmapUtils.createBase64(bitmap);
}

if (result1 == null) {
result.error(
"NO_RESULT",
"Could not generate snapshot, please check Android logs for more info.",
null);
return;
}

result.success(result1);
mSnapshotterMap.remove(snapshotterID);
},
new MapSnapshotter.ErrorHandler() {
@Override
public void onError(String error) {
result.error("SNAPSHOT_ERROR", error, null);
mSnapshotterMap.remove(snapshotterID);
}
});
break;
}
default:
result.notImplemented();
}
Expand Down
1 change: 1 addition & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.buildlog/
.history
.svn/
.fvm/

# IntelliJ related
*.iml
Expand Down
1 change: 1 addition & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

<application
android:name="${applicationName}"
Expand Down
3 changes: 3 additions & 0 deletions example/android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
15 changes: 3 additions & 12 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
Expand Down Expand Up @@ -512,10 +509,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
Expand All @@ -539,10 +533,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
Expand Down
16 changes: 8 additions & 8 deletions example/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import UIKit
import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@
<string>Shows your location on the map and helps improve the map</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Shows your location on the map and helps improve the map</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
1 change: 1 addition & 0 deletions example/lib/generated_plugin_registrant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

// ignore_for_file: directives_ordering
// ignore_for_file: lines_longer_than_80_chars
// ignore_for_file: depend_on_referenced_packages

import 'package:device_info_plus_web/device_info_plus_web.dart';
import 'package:location_web/location_web.dart';
Expand Down
1 change: 1 addition & 0 deletions example/lib/line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
// ignore: unnecessary_import
import 'dart:typed_data';

import 'package:flutter/material.dart';
Expand Down
Loading

0 comments on commit 3e490a8

Please sign in to comment.