Skip to content

Commit

Permalink
custom symbols (#79)
Browse files Browse the repository at this point in the history
* Passing map style to iOS map (MGLMapView).

* Moved map style lookup table to a separate file.

* Renamed style parameter to styleString.

* Moved the lookup behind a fromUrl function.

* Added checks for empty styleString and JSON styleStrings.

* Added a sample icon at different resolutions.

* Added custom icon as asset.

* Added sink object for symbols.

* Initial Symbol class with a mapping between some of the setters form the sink.

* Updated Convert to interpret symbol options.

* Added symbol#add to (iOS) mapboxcontroller.

* Passing the registrar instead of the binary messenger to the controller. We need it to lead the images.

* Load image for symbol (from registrar). The extension method for loading the image takes scaling into account.

* Added a button for adding the custom icon symbol.

* Wired annotation selection to symbol#onTap.

* Typo "line#onTap" should be "symbol#onTap"

* Remove unused camera symbol.

* Generate uuid for symbol id.

* Implemented updating a symbol (only updating position works).

* Implemented remove annotation.

* Refactor symbol lookup.

* Added gradle wrapper.

* Added Android custom symbols.

* Cleaned up code.

* Removed need for custom image parameter.

* Fixed memory issue.

* Android icon image scaling works.

* Bug fix (on custom_symbol branch).

* Refactored the getImage method.

* Fixed typo.

* Removed hardcoded assets path (path is now derived from the fullpath in symbol.iconImage property).

* Renamed getImage to getScaledImage

* Add symbol image using the async getStyle method.

* Adding an image based on the addOnStyleImageMissing callback for more efficiency.
  • Loading branch information
TimothySealy authored and tobrun committed Nov 17, 2019
1 parent bf73492 commit 2c7ff8d
Show file tree
Hide file tree
Showing 17 changed files with 479 additions and 48 deletions.
Binary file added android/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
3 changes: 0 additions & 3 deletions android/src/main/java/com/mapbox/mapboxgl/Convert.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@

import android.graphics.Point;

import android.util.Log;

import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.log.Logger;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.geojson.LineString;

import java.util.ArrayList;
import java.util.Arrays;
Expand Down
78 changes: 73 additions & 5 deletions android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Bundle;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import android.util.Log;
import android.view.View;
Expand Down Expand Up @@ -47,11 +52,9 @@
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.platform.PlatformView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static com.mapbox.mapboxgl.MapboxMapsPlugin.CREATED;
Expand Down Expand Up @@ -265,6 +268,15 @@ public void onMapReady(MapboxMap mapboxMap) {
mapboxMap.addOnCameraMoveStartedListener(this);
mapboxMap.addOnCameraMoveListener(this);
mapboxMap.addOnCameraIdleListener(this);

mapView.addOnStyleImageMissingListener((id) -> {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
final Bitmap bitmap = getScaledImage(id, displayMetrics.density);
if (bitmap != null) {
mapboxMap.getStyle().addImage(id, bitmap);
}
});

setStyleString(styleStringInitial);
// updateMyLocationEnabled();
}
Expand Down Expand Up @@ -741,4 +753,60 @@ private int checkSelfPermission(String permission) {
permission, android.os.Process.myPid(), android.os.Process.myUid());
}

/**
* Tries to find highest scale image for display type
* @param imageId
* @param density
* @return
*/
private Bitmap getScaledImage(String imageId, float density) {
AssetManager assetManager = registrar.context().getAssets();
AssetFileDescriptor assetFileDescriptor = null;

// Split image path into parts.
List<String> imagePathList = Arrays.asList(imageId.split("/"));
List<String> assetPathList = new ArrayList<>();

// "On devices with a device pixel ratio of 1.8, the asset .../2.0x/my_icon.png would be chosen.
// For a device pixel ratio of 2.7, the asset .../3.0x/my_icon.png would be chosen."
// Source: https://flutter.dev/docs/development/ui/assets-and-images#resolution-aware
for (int i = (int) Math.ceil(density); i > 0; i--) {
String assetPath;
if (i == 1) {
// If density is 1.0x then simply take the default asset path
assetPath = registrar.lookupKeyForAsset(imageId);
} else {
// Build a resolution aware asset path as follows:
// <directory asset>/<ratio>/<image name>
// where ratio is 1.0x, 2.0x or 3.0x.
StringBuilder stringBuilder = new StringBuilder();
for (int j = 0; j < imagePathList.size() - 1; j++) {
stringBuilder.append(imagePathList.get(j));
stringBuilder.append("/");
}
stringBuilder.append(((float) i) + "x");
stringBuilder.append("/");
stringBuilder.append(imagePathList.get(imagePathList.size()-1));
assetPath = registrar.lookupKeyForAsset(stringBuilder.toString());
}
// Build up a list of resolution aware asset paths.
assetPathList.add(assetPath);
}

// Iterate over asset paths and get the highest scaled asset (as a bitmap).
Bitmap bitmap = null;
for (String assetPath : assetPathList) {
try {
// Read path (throws exception if doesn't exist).
assetFileDescriptor = assetManager.openFd(assetPath);
InputStream assetStream = assetFileDescriptor.createInputStream();
bitmap = BitmapFactory.decodeStream(assetStream);
assetFileDescriptor.close(); // Close for memory
break; // If exists, break
} catch (IOException e) {
// Skip
}
}
return bitmap;
}
}
5 changes: 5 additions & 0 deletions android/src/main/java/com/mapbox/mapboxgl/SymbolBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
class SymbolBuilder implements SymbolOptionsSink {
private final SymbolManager symbolManager;
private final SymbolOptions symbolOptions;
private static boolean customImage;

SymbolBuilder(SymbolManager symbolManager) {
this.symbolManager = symbolManager;
Expand Down Expand Up @@ -159,4 +160,8 @@ public void setSymbolSortKey(float symbolSortKey) {
public void setDraggable(boolean draggable) {
symbolOptions.withDraggable(draggable);
}

public boolean getCustomImage() {
return this.customImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ interface SymbolOptionsSink {
void setSymbolSortKey(float symbolSortKey);

void setDraggable(boolean draggable);

}
Binary file added example/assets/symbols/2.0x/custom-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/symbols/3.0x/custom-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/symbols/custom-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 16 additions & 9 deletions example/lib/place_symbol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ class PlaceSymbolBodyState extends State<PlaceSymbolBody> {
controller.updateSymbol(_selectedSymbol, changes);
}

void _add() {
void _add(String iconImage) {
controller.addSymbol(
SymbolOptions(
geometry: LatLng(
center.latitude + sin(_symbolCount * pi / 6.0) / 20.0,
center.longitude + cos(_symbolCount * pi / 6.0) / 20.0,
),
iconImage: "airport-15"),
geometry: LatLng(
center.latitude + sin(_symbolCount * pi / 6.0) / 20.0,
center.longitude + cos(_symbolCount * pi / 6.0) / 20.0,
),
iconImage: iconImage,
),
);
setState(() {
_symbolCount += 1;
Expand Down Expand Up @@ -156,8 +157,7 @@ class PlaceSymbolBodyState extends State<PlaceSymbolBody> {
}

_updateSelectedSymbol(
SymbolOptions(
iconOpacity: current == 0.0 ? 1.0 : 0.0),
SymbolOptions(iconOpacity: current == 0.0 ? 1.0 : 0.0),
);
}

Expand Down Expand Up @@ -202,7 +202,14 @@ class PlaceSymbolBodyState extends State<PlaceSymbolBody> {
children: <Widget>[
FlatButton(
child: const Text('add'),
onPressed: (_symbolCount == 12) ? null : _add,
onPressed: () =>
(_symbolCount == 12) ? null : _add("airport-15"),
),
FlatButton(
child: const Text('add (custom icon)'),
onPressed: () => (_symbolCount == 12)
? null
: _add("assets/symbols/custom-icon.png"),
),
FlatButton(
child: const Text('remove'),
Expand Down
5 changes: 5 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ flutter:
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.io/assets-and-images/#resolution-aware.

assets:
- assets/symbols/custom-icon.png
- assets/symbols/2.0x/custom-icon.png
- assets/symbols/3.0x/custom-icon.png

# For details regarding adding assets from package dependencies, see
# https://flutter.io/assets-and-images/#from-packages

Expand Down
82 changes: 82 additions & 0 deletions ios/Classes/Convert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,86 @@ class Convert {
class func getAltitude(zoom: Double, mapView: MGLMapView) -> Double {
return MGLAltitudeForZoomLevel(zoom, mapView.camera.pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size)
}

class func interpretSymbolOptions(options: Any?, delegate: SymbolOptionsSink) {
guard let options = options as? [String: Any] else { return }

if let iconSize = options["iconSize"] as? Double {
delegate.setIconSize(iconSize: iconSize)
}
if let iconImage = options["iconImage"] as? String {
delegate.setIconImage(iconImage: iconImage)
}
if let iconRotate = options["iconRotate"] as? Double {
delegate.setIconRotate(iconRotate: iconRotate)
}
//TODO: iconOffset
if let iconAnchor = options["iconAnchor"] as? String {
delegate.setIconAnchor(iconAnchor: iconAnchor)
}
if let textField = options["textField"] as? String {
delegate.setTextField(textField: textField)
}
if let textSize = options["textSize"] as? Double {
delegate.setTextSize(textSize: textSize)
}
if let textMaxWidth = options["textMaxWidth"] as? Double {
delegate.setTextMaxWidth(textMaxWidth: textMaxWidth)
}
if let textLetterSpacing = options["textLetterSpacing"] as? Double {
delegate.setTextLetterSpacing(textLetterSpacing: textLetterSpacing)
}
if let textJustify = options["textJustify"] as? String {
delegate.setTextJustify(textJustify: textJustify)
}
if let textAnchor = options["textAnchor"] as? String {
delegate.setTextAnchor(textAnchor: textAnchor)
}
if let textRotate = options["textRotate"] as? Double {
delegate.setTextRotate(textRotate: textRotate)
}
if let textTransform = options["textTransform"] as? String {
delegate.setTextTransform(textTransform: textTransform)
}
//TODO: textOffset
if let iconOpacity = options["iconOpacity"] as? Double {
delegate.setIconOpacity(iconOpacity: iconOpacity)
}
if let iconColor = options["iconColor"] as? String {
delegate.setIconColor(iconColor: iconColor)
}
if let iconHaloColor = options["iconHaloColor"] as? String {
delegate.setIconHaloColor(iconHaloColor: iconHaloColor)
}
if let iconHaloWidth = options["iconHaloWidth"] as? Double {
delegate.setIconHaloWidth(iconHaloWidth: iconHaloWidth)
}
if let iconHaloBlur = options["iconHaloBlur"] as? Double {
delegate.setIconHaloBlur(iconHaloBlur: iconHaloBlur)
}
if let textOpacity = options["textOpacity"] as? Double {
delegate.setTextOpacity(textOpacity: textOpacity)
}
if let textColor = options["textColor"] as? String {
delegate.setTextColor(textColor: textColor)
}
if let textHaloColor = options["textHaloColor"] as? String {
delegate.setTextHaloColor(textHaloColor: textHaloColor)
}
if let textHaloWidth = options["textHaloWidth"] as? Double {
delegate.setTextHaloWidth(textHaloWidth: textHaloWidth)
}
if let textHaloBlur = options["textHaloBlur"] as? Double {
delegate.setTextHaloBlur(textHaloBlur: textHaloBlur)
}
if let geometry = options["geometry"] as? [Double] {
delegate.setGeometry(geometry: geometry)
}
if let zIndex = options["zIndex"] as? Int {
delegate.setZIndex(zIndex: zIndex)
}
if let draggable = options["draggable"] as? Bool {
delegate.setDraggable(draggable: draggable)
}
}
}
23 changes: 23 additions & 0 deletions ios/Classes/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,26 @@ extension MGLCoordinateBounds {
return MGLCoordinateBounds(sw: southwest, ne: northeast)
}
}

extension UIImage {
static func loadFromFile(imagePath: String, imageName: String) -> UIImage? {
// Add the trailing slash in path if missing.
let path = imagePath.hasSuffix("/") ? imagePath : "\(imagePath)/"
// Build scale dependant image path.
let scale = UIScreen.main.scale
var absolutePath = "\(path)\(scale)x/\(imageName)"
// Check if the image exists, if not try a an unscaled path.
if Bundle.main.path(forResource: absolutePath, ofType: nil) == nil {
absolutePath = "\(path)\(imageName)"
}
// Load image if it exists.
if let path = Bundle.main.path(forResource: absolutePath, ofType: nil) {
let imageUrl: URL = URL(fileURLWithPath: path)
if let imageData: Data = try? Data(contentsOf: imageUrl),
let image: UIImage = UIImage(data: imageData, scale: UIScreen.main.scale) {
return image
}
}
return nil
}
}
Loading

0 comments on commit 2c7ff8d

Please sign in to comment.