header : headers.entrySet()) {
+ if (header.getKey() == null || header.getKey().trim().isEmpty()) {
+ continue;
+ }
+ if (header.getValue() == null || header.getValue().trim().isEmpty()) {
+ builder.removeHeader(header.getKey());
+ } else {
+ builder.header(header.getKey(), header.getValue());
+ }
+ }
+ return chain.proceed(builder.build());
+ });
+ } catch (Exception e) {
+ result.error(
+ "OK_HTTP_CLIENT_ERROR",
+ "An unexcepted error happened during creating http " + "client" + e.getMessage(),
+ null);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/android/src/main/java/ai/nextbillion/maps_flutter/NbMapUtils.java b/android/src/main/java/ai/nextbillion/maps_flutter/NbMapUtils.java
new file mode 100644
index 0000000..d815f08
--- /dev/null
+++ b/android/src/main/java/ai/nextbillion/maps_flutter/NbMapUtils.java
@@ -0,0 +1,77 @@
+package ai.nextbillion.maps_flutter;
+
+import android.content.Context;
+
+import ai.nextbillion.maps.Nextbillion;
+import ai.nextbillion.maps.exceptions.NbmapConfigurationException;
+
+abstract class NbMapUtils {
+ private static final String TAG = "NbMapController";
+
+ static Nextbillion getNextbillion(Context context, String accessToken) {
+ if (accessToken == null || accessToken.isEmpty()) {
+ throw new NbmapConfigurationException("\nUsing MapView requires calling Nextbillion.initNextbillion(String accessKey) before inflating or creating NBMap Widget. The accessKey parameter is required when using a NBMap Widget.");
+ }
+ return Nextbillion.getInstance(context.getApplicationContext(), accessToken);
+ }
+
+ static String getAccessKey() {
+ String accessToken = Nextbillion.getAccessKey();
+ if (accessToken == null || accessToken.isEmpty()) {
+ throw new NbmapConfigurationException("\n Access Key is not set or Access Key is empty");
+ }
+ return accessToken;
+ }
+
+ static void setAccessKey(String accessToken) {
+ if (accessToken == null || accessToken.isEmpty()) {
+ throw new NbmapConfigurationException("\n Access Key should not be empty");
+ }
+ Nextbillion.setAccessKey(accessToken);
+ }
+
+ static String getBaseUri() {
+ String baseUri = Nextbillion.getBaseUri();
+ if (baseUri == null || baseUri.isEmpty()) {
+ throw new NbmapConfigurationException("\n BaseUri is not set or BaseUri is empty");
+ }
+ return baseUri;
+ }
+
+ static void setBaseUri(String baseUri) {
+ if (baseUri == null || baseUri.isEmpty()) {
+ throw new NbmapConfigurationException("\n BaseUri should not be empty");
+ }
+ Nextbillion.setBaseUri(baseUri);
+ }
+
+ static void setApiKeyHeaderName(String apiKeyHeaderName) {
+ if (apiKeyHeaderName == null || apiKeyHeaderName.isEmpty()) {
+ throw new NbmapConfigurationException("\n Api Key Header Name should not be empty");
+ }
+ Nextbillion.setApiKeyHeaderName(apiKeyHeaderName);
+ }
+
+ static String getApiKeyHeaderName() {
+ return Nextbillion.getApiKeyHeaderName();
+ }
+
+ static String getUserid() {
+ return Nextbillion.getUserId();
+ }
+
+ static void setUserid(String userid) {
+ Nextbillion.setUserId(userid);
+ }
+
+ static String getNbId() {
+ return Nextbillion.getNBId();
+ }
+
+ static void setCrossPlatformInfo() {
+ String crossPlatformName = String.format("Flutter-%s-%s", BuildConfig.NBMAP_FLUTTER_VERSION, BuildConfig.GIT_REVISION_SHORT);
+ Nextbillion.setCrossPlatformInfo(crossPlatformName);
+ }
+
+
+}
diff --git a/android/src/main/java/ai/nextbillion/maps_flutter/NbMapsPlugin.java b/android/src/main/java/ai/nextbillion/maps_flutter/NbMapsPlugin.java
new file mode 100644
index 0000000..7d70659
--- /dev/null
+++ b/android/src/main/java/ai/nextbillion/maps_flutter/NbMapsPlugin.java
@@ -0,0 +1,219 @@
+
+package ai.nextbillion.maps_flutter;
+
+import android.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
+
+/**
+ * Plugin for controlling a set of NBMap views to be shown as overlays on top of the Flutter
+ * view. The overlay should be hidden during transformations or while Flutter is rendering on top of
+ * the map. A Texture drawn using NBMap bitmap snapshots can then be shown instead of the
+ * overlay.
+ */
+public class NbMapsPlugin implements FlutterPlugin, ActivityAware {
+
+ private static final String VIEW_TYPE = "plugins.flutter.io/nb_maps_flutter";
+
+ static FlutterAssets flutterAssets;
+ private Lifecycle lifecycle;
+
+ public NbMapsPlugin() {
+ // no-op
+ }
+
+ // New Plugin APIs
+
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
+ flutterAssets = binding.getFlutterAssets();
+
+ MethodChannel methodChannel =
+ new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/nb_maps_flutter");
+ methodChannel.setMethodCallHandler(new GlobalMethodHandler(binding));
+
+ MethodChannel nextBillionChannel =
+ new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/nextbillion_init");
+ nextBillionChannel.setMethodCallHandler(new NextBillionMethodHandler(binding));
+
+ binding
+ .getPlatformViewRegistry()
+ .registerViewFactory(
+ "plugins.flutter.io/nb_maps_flutter",
+ new NbMapFactory(
+ binding.getBinaryMessenger(),
+ new LifecycleProvider() {
+ @Nullable
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycle;
+ }
+ }));
+
+ }
+
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ // no-op
+ }
+
+ @Override
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+ lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
+ }
+
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
+ }
+
+ @Override
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
+ onAttachedToActivity(binding);
+ }
+
+ @Override
+ public void onDetachedFromActivity() {
+ lifecycle = null;
+ }
+
+ // Old Plugin APIs
+
+ public static void registerWith(Registrar registrar) {
+ final Activity activity = registrar.activity();
+ if (activity == null) {
+ // When a background flutter view tries to register the plugin, the registrar has no activity.
+ // We stop the registration process as this plugin is foreground only.
+ return;
+ }
+ if (activity instanceof LifecycleOwner) {
+ registrar
+ .platformViewRegistry()
+ .registerViewFactory(
+ VIEW_TYPE,
+ new NbMapFactory(
+ registrar.messenger(),
+ new LifecycleProvider() {
+ @Override
+ public Lifecycle getLifecycle() {
+ return ((LifecycleOwner) activity).getLifecycle();
+ }
+ }));
+ } else {
+ registrar
+ .platformViewRegistry()
+ .registerViewFactory(
+ VIEW_TYPE,
+ new NbMapFactory(registrar.messenger(), new ProxyLifecycleProvider(activity)));
+ }
+
+ MethodChannel methodChannel =
+ new MethodChannel(registrar.messenger(), "plugins.flutter.io/nb_maps_flutter");
+ methodChannel.setMethodCallHandler(new GlobalMethodHandler(registrar));
+ }
+
+ private static final class ProxyLifecycleProvider
+ implements ActivityLifecycleCallbacks, LifecycleOwner, LifecycleProvider {
+
+ private final LifecycleRegistry lifecycle = new LifecycleRegistry(this);
+ private final int registrarActivityHashCode;
+
+ private ProxyLifecycleProvider(Activity activity) {
+ this.registrarActivityHashCode = activity.hashCode();
+ activity.getApplication().registerActivityLifecycleCallbacks(this);
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ activity.getApplication().unregisterActivityLifecycleCallbacks(this);
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycle;
+ }
+ }
+
+ interface LifecycleProvider {
+ @Nullable
+ Lifecycle getLifecycle();
+ }
+
+ /** Provides a static method for extracting lifecycle objects from Flutter plugin bindings. */
+ public static class FlutterLifecycleAdapter {
+
+ /**
+ * Returns the lifecycle object for the activity a plugin is bound to.
+ *
+ * Returns null if the Flutter engine version does not include the lifecycle extraction code.
+ * (this probably means the Flutter engine version is too old).
+ */
+ @NonNull
+ public static Lifecycle getActivityLifecycle(
+ @NonNull ActivityPluginBinding activityPluginBinding) {
+ HiddenLifecycleReference reference =
+ (HiddenLifecycleReference) activityPluginBinding.getLifecycle();
+ return reference.getLifecycle();
+ }
+ }
+}
diff --git a/android/src/main/java/ai/nextbillion/maps_flutter/NextBillionMethodHandler.java b/android/src/main/java/ai/nextbillion/maps_flutter/NextBillionMethodHandler.java
new file mode 100644
index 0000000..40f7c6b
--- /dev/null
+++ b/android/src/main/java/ai/nextbillion/maps_flutter/NextBillionMethodHandler.java
@@ -0,0 +1,80 @@
+package ai.nextbillion.maps_flutter;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import ai.nextbillion.maps.Nextbillion;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+/**
+ * @author qiuyu
+ * @Date 2023/7/13
+ **/
+public class NextBillionMethodHandler implements MethodChannel.MethodCallHandler {
+
+ @NonNull
+ private final Context context;
+
+ NextBillionMethodHandler(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
+ this.context = binding.getApplicationContext();
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ switch (call.method) {
+ case "nextbillion/init_nextbillion":
+ String accessToken = call.argument("accessKey");
+ NbMapUtils.getNextbillion(context, accessToken);
+ NbMapUtils.setCrossPlatformInfo();
+ result.success(null);
+ break;
+ case "nextbillion/get_access_key":
+ String accessTokenResult = NbMapUtils.getAccessKey();
+ result.success(accessTokenResult);
+ break;
+ case "nextbillion/set_access_key":
+ String accessTokenToSet = call.argument("accessKey");
+ NbMapUtils.setAccessKey(accessTokenToSet);
+ result.success(null);
+ break;
+ case "nextbillion/get_base_uri":
+ String baseUri = NbMapUtils.getBaseUri();
+ result.success(baseUri);
+ break;
+ case "nextbillion/set_base_uri":
+ String baseUriToSet = call.argument("baseUri");
+ NbMapUtils.setBaseUri(baseUriToSet);
+ result.success(null);
+ break;
+ case "nextbillion/set_key_header_name":
+ String apiKeyHeaderName = call.argument("apiKeyHeaderName");
+ NbMapUtils.setApiKeyHeaderName(apiKeyHeaderName);
+ result.success(null);
+ break;
+ case "nextbillion/get_key_header_name":
+ String keyHeaderName = NbMapUtils.getApiKeyHeaderName();
+ result.success(keyHeaderName);
+ break;
+ case "nextbillion/get_nb_id":
+ String nbId = NbMapUtils.getNbId();
+ result.success(nbId);
+ break;
+ case "nextbillion/set_user_id":
+ String id = call.argument("userId");
+ NbMapUtils.setUserid(id);
+ result.success(null);
+ break;
+ case "nextbillion/get_user_id":
+ String userId = NbMapUtils.getUserid();
+ result.success(userId);
+ break;
+
+ default:
+ result.notImplemented();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/ai/nextbillion/maps_flutter/NextbillionMapController.java b/android/src/main/java/ai/nextbillion/maps_flutter/NextbillionMapController.java
new file mode 100644
index 0000000..f316c7d
--- /dev/null
+++ b/android/src/main/java/ai/nextbillion/maps_flutter/NextbillionMapController.java
@@ -0,0 +1,2093 @@
+
+package ai.nextbillion.maps_flutter;
+
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.location.Location;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import ai.nextbillion.gestures.AndroidGesturesManager;
+import ai.nextbillion.gestures.MoveGestureDetector;
+import ai.nextbillion.kits.geojson.Feature;
+import ai.nextbillion.kits.geojson.FeatureCollection;
+import ai.nextbillion.kits.geojson.Point;
+import ai.nextbillion.maps.camera.CameraPosition;
+import ai.nextbillion.maps.camera.CameraUpdate;
+import ai.nextbillion.maps.camera.CameraUpdateFactory;
+import ai.nextbillion.maps.core.MapView;
+import ai.nextbillion.maps.core.NextbillionMap;
+import ai.nextbillion.maps.core.NextbillionMapOptions;
+import ai.nextbillion.maps.core.OnMapReadyCallback;
+import ai.nextbillion.maps.core.Style;
+import ai.nextbillion.maps.geometry.LatLng;
+import ai.nextbillion.maps.geometry.LatLngBounds;
+import ai.nextbillion.maps.geometry.LatLngQuad;
+import ai.nextbillion.maps.geometry.VisibleRegion;
+import ai.nextbillion.maps.location.LocationComponent;
+import ai.nextbillion.maps.location.LocationComponentOptions;
+import ai.nextbillion.maps.location.OnCameraTrackingChangedListener;
+import ai.nextbillion.maps.location.engine.LocationEngine;
+import ai.nextbillion.maps.location.engine.LocationEngineCallback;
+import ai.nextbillion.maps.location.engine.LocationEngineProvider;
+import ai.nextbillion.maps.location.engine.LocationEngineResult;
+import ai.nextbillion.maps.location.modes.CameraMode;
+import ai.nextbillion.maps.location.modes.RenderMode;
+import ai.nextbillion.maps.offline.OfflineManager;
+import ai.nextbillion.maps.snapshotter.MapSnapshotter;
+import ai.nextbillion.maps.storage.FileSource;
+import ai.nextbillion.maps.style.expressions.Expression;
+import ai.nextbillion.maps.style.layers.CircleLayer;
+import ai.nextbillion.maps.style.layers.FillExtrusionLayer;
+import ai.nextbillion.maps.style.layers.FillLayer;
+import ai.nextbillion.maps.style.layers.HeatmapLayer;
+import ai.nextbillion.maps.style.layers.HillshadeLayer;
+import ai.nextbillion.maps.style.layers.Layer;
+import ai.nextbillion.maps.style.layers.LineLayer;
+import ai.nextbillion.maps.style.layers.PropertyValue;
+import ai.nextbillion.maps.style.layers.RasterLayer;
+import ai.nextbillion.maps.style.layers.SymbolLayer;
+import ai.nextbillion.maps.style.sources.GeoJsonSource;
+import ai.nextbillion.maps.style.sources.ImageSource;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.platform.PlatformView;
+
+import static ai.nextbillion.maps.style.layers.Property.NONE;
+import static ai.nextbillion.maps.style.layers.Property.VISIBLE;
+import static ai.nextbillion.maps.style.layers.PropertyFactory.visibility;
+import static ai.nextbillion.maps_flutter.Convert.interpretNextbillionMapOptions;
+
+/** Controller of a single NbMaps MapView instance. */
+@SuppressLint("MissingPermission")
+final class NextbillionMapController
+ implements DefaultLifecycleObserver,
+ NextbillionMap.OnCameraIdleListener,
+ NextbillionMap.OnCameraMoveListener,
+ NextbillionMap.OnCameraMoveStartedListener,
+ MapView.OnDidBecomeIdleListener,
+ NextbillionMap.OnMapClickListener,
+ NextbillionMap.OnMapLongClickListener,
+ NextbillionMapOptionsSink,
+ MethodChannel.MethodCallHandler,
+ OnMapReadyCallback,
+ OnCameraTrackingChangedListener,
+ PlatformView {
+
+ private static final String TAG = "NbMapController";
+ private final int id;
+ private final MethodChannel methodChannel;
+ private final NbMapsPlugin.LifecycleProvider lifecycleProvider;
+ private final float density;
+ private final Context context;
+ private final String styleStringInitial;
+ private final Set interactiveFeatureLayerIds;
+ private final Map addedFeaturesByLayer;
+ private final Map mSnapshotterMap;
+ private MapView mapView;
+ private NextbillionMap nextbillionMap;
+ private boolean trackCameraPosition = false;
+ private boolean myLocationEnabled = false;
+ private int myLocationTrackingMode = 0;
+ private int myLocationRenderMode = 0;
+ private boolean disposed = false;
+ private boolean dragEnabled = true;
+ private MethodChannel.Result mapReadyResult;
+ private LocationComponent locationComponent = null;
+ private LocationEngine locationEngine = null;
+ private LocationEngineCallback locationEngineCallback = null;
+// private LocalizationPlugin localizationPlugin;
+ private Style style;
+ private Feature draggedFeature;
+ private AndroidGesturesManager androidGesturesManager;
+ private LatLng dragOrigin;
+ private LatLng dragPrevious;
+ private LatLngBounds bounds = null;
+ Style.OnStyleLoaded onStyleLoadedCallback =
+ new Style.OnStyleLoaded() {
+ @Override
+ public void onStyleLoaded(@NonNull Style style) {
+ NextbillionMapController.this.style = style;
+
+ updateMyLocationEnabled();
+
+ if (null != bounds) {
+ nextbillionMap.setLatLngBoundsForCameraTarget(bounds);
+ }
+
+ nextbillionMap.addOnMapClickListener(NextbillionMapController.this);
+ nextbillionMap.addOnMapLongClickListener(NextbillionMapController.this);
+// localizationPlugin = new LocalizationPlugin(mapView, nextbillionMap, style);
+
+ methodChannel.invokeMethod("map#onStyleLoaded", null);
+ }
+ };
+
+ NextbillionMapController(
+ int id,
+ Context context,
+ BinaryMessenger messenger,
+ NbMapsPlugin.LifecycleProvider lifecycleProvider,
+ NextbillionMapOptions options,
+ String styleStringInitial,
+ boolean dragEnabled) {
+ this.id = id;
+ this.context = context;
+ this.dragEnabled = dragEnabled;
+ this.styleStringInitial = styleStringInitial;
+ this.mapView = new MapView(context, options);
+ this.interactiveFeatureLayerIds = new HashSet<>();
+ this.addedFeaturesByLayer = new HashMap();
+ this.density = context.getResources().getDisplayMetrics().density;
+ this.lifecycleProvider = lifecycleProvider;
+ if (dragEnabled) {
+ this.androidGesturesManager = new AndroidGesturesManager(this.mapView.getContext(), false);
+ }
+ this.mSnapshotterMap = new HashMap<>();
+ methodChannel = new MethodChannel(messenger, "plugins.flutter.io/nbmaps_maps_" + id);
+ methodChannel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public View getView() {
+ return mapView;
+ }
+
+ void init() {
+ lifecycleProvider.getLifecycle().addObserver(this);
+ mapView.getMapAsync(this);
+ }
+
+ private void moveCamera(CameraUpdate cameraUpdate) {
+ nextbillionMap.moveCamera(cameraUpdate);
+ }
+
+ private void animateCamera(CameraUpdate cameraUpdate) {
+ nextbillionMap.animateCamera(cameraUpdate);
+ }
+
+ private CameraPosition getCameraPosition() {
+ return trackCameraPosition ? nextbillionMap.getCameraPosition() : null;
+ }
+
+ @Override
+ public void onMapReady(NextbillionMap nextbillionMap) {
+ this.nextbillionMap = nextbillionMap;
+ if (mapReadyResult != null) {
+ mapReadyResult.success(null);
+ mapReadyResult = null;
+ }
+ nextbillionMap.addOnCameraMoveStartedListener(this);
+ nextbillionMap.addOnCameraMoveListener(this);
+ nextbillionMap.addOnCameraIdleListener(this);
+
+ if (androidGesturesManager != null) {
+ androidGesturesManager.setMoveGestureListener(new MoveGestureListener());
+ mapView.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ androidGesturesManager.onTouchEvent(event);
+
+ return draggedFeature != null;
+ }
+ });
+ }
+
+ mapView.addOnStyleImageMissingListener(
+ (id) -> {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ final Bitmap bitmap = getScaledImage(id, displayMetrics.density);
+ if (bitmap != null) {
+ nextbillionMap.getStyle().addImage(id, bitmap);
+ }
+ });
+
+ mapView.addOnDidBecomeIdleListener(this);
+
+ setStyleString(styleStringInitial);
+ }
+
+ @Override
+ public void setStyleString(String styleString) {
+ // clear old layer id from the location Component
+ clearLocationComponentLayer();
+
+ // Check if json, url, absolute path or asset path:
+ if (styleString == null || styleString.isEmpty()) {
+ Log.e(TAG, "setStyleString - string empty or null");
+ } else if (styleString.startsWith("{") || styleString.startsWith("[")) {
+ nextbillionMap.setStyle(new Style.Builder().fromJson(styleString), onStyleLoadedCallback);
+ } else if (styleString.startsWith("/")) {
+ // Absolute path
+ nextbillionMap.setStyle(
+ new Style.Builder().fromUri("file://" + styleString), onStyleLoadedCallback);
+ } else if (!styleString.startsWith("http://")
+ && !styleString.startsWith("https://")) {
+ // We are assuming that the style will be loaded from an asset here.
+ String key = NbMapsPlugin.flutterAssets.getAssetFilePathByName(styleString);
+ nextbillionMap.setStyle(new Style.Builder().fromUri("asset://" + key), onStyleLoadedCallback);
+ } else {
+ nextbillionMap.setStyle(new Style.Builder().fromUri(styleString), onStyleLoadedCallback);
+ }
+ }
+
+ @SuppressWarnings({"MissingPermission"})
+ private void enableLocationComponent(@NonNull Style style) {
+ if (hasLocationPermission()) {
+ locationEngine = LocationEngineProvider.getBestLocationEngine(context);
+ locationComponent = nextbillionMap.getLocationComponent();
+ locationComponent.activateLocationComponent(
+ context, style, buildLocationComponentOptions(style));
+ locationComponent.setLocationComponentEnabled(true);
+ // locationComponent.setRenderMode(RenderMode.COMPASS); // remove or keep default?
+ locationComponent.setLocationEngine(locationEngine);
+ locationComponent.setMaxAnimationFps(30);
+ updateMyLocationTrackingMode();
+ updateMyLocationRenderMode();
+ locationComponent.addOnCameraTrackingChangedListener(this);
+ } else {
+ Log.e(TAG, "missing location permissions");
+ }
+ }
+
+ private void updateLocationComponentLayer() {
+ if (locationComponent != null && locationComponentRequiresUpdate()) {
+ locationComponent.applyStyle(buildLocationComponentOptions(style));
+ }
+ }
+
+ private void clearLocationComponentLayer() {
+ if (locationComponent != null) {
+ locationComponent.applyStyle(buildLocationComponentOptions(null));
+ }
+ }
+
+ String getLastLayerOnStyle(Style style) {
+ if (style != null) {
+ final List layers = style.getLayers();
+
+ if (layers.size() > 0) {
+ return layers.get(layers.size() - 1).getId();
+ }
+ }
+ return null;
+ }
+
+ boolean locationComponentRequiresUpdate() {
+ final String lastLayerId = getLastLayerOnStyle(style);
+ return lastLayerId != null && !lastLayerId.equals("nbmap-location-bearing-layer");
+ }
+
+ private LocationComponentOptions buildLocationComponentOptions(Style style) {
+ final LocationComponentOptions.Builder optionsBuilder =
+ LocationComponentOptions.builder(context);
+ optionsBuilder.trackingGesturesManagement(true);
+
+ final String lastLayerId = getLastLayerOnStyle(style);
+ if (lastLayerId != null) {
+ optionsBuilder.layerAbove(lastLayerId);
+ }
+ return optionsBuilder.build();
+ }
+
+ private void onUserLocationUpdate(Location location) {
+ if (location == null) {
+ return;
+ }
+
+ final Map userLocation = new HashMap<>(6);
+ userLocation.put("position", new double[] {location.getLatitude(), location.getLongitude()});
+ userLocation.put("speed", location.getSpeed());
+ userLocation.put("altitude", location.getAltitude());
+ userLocation.put("bearing", location.getBearing());
+ userLocation.put("horizontalAccuracy", location.getAccuracy());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ userLocation.put(
+ "verticalAccuracy",
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ ? location.getVerticalAccuracyMeters()
+ : null);
+ }
+ userLocation.put("timestamp", location.getTime());
+
+ final Map arguments = new HashMap<>(1);
+ arguments.put("userLocation", userLocation);
+ methodChannel.invokeMethod("map#onUserLocationUpdated", arguments);
+ }
+
+ private void addGeoJsonSource(String sourceName, String source) {
+ FeatureCollection featureCollection = FeatureCollection.fromJson(source);
+ GeoJsonSource geoJsonSource = new GeoJsonSource(sourceName, featureCollection);
+ addedFeaturesByLayer.put(sourceName, featureCollection);
+
+ style.addSource(geoJsonSource);
+ }
+
+ private void setGeoJsonSource(String sourceName, String geojson) {
+ FeatureCollection featureCollection = FeatureCollection.fromJson(geojson);
+ GeoJsonSource geoJsonSource = style.getSourceAs(sourceName);
+ addedFeaturesByLayer.put(sourceName, featureCollection);
+
+ geoJsonSource.setGeoJson(featureCollection);
+ }
+
+ private void setGeoJsonFeature(String sourceName, String geojsonFeature) {
+ Feature feature = Feature.fromJson(geojsonFeature);
+ FeatureCollection featureCollection = addedFeaturesByLayer.get(sourceName);
+ GeoJsonSource geoJsonSource = style.getSourceAs(sourceName);
+ if (featureCollection != null && geoJsonSource != null) {
+ final List features = featureCollection.features();
+ for (int i = 0; i < features.size(); i++) {
+ final String id = features.get(i).id();
+ if (id.equals(feature.id())) {
+ features.set(i, feature);
+ break;
+ }
+ }
+
+ geoJsonSource.setGeoJson(featureCollection);
+ }
+ }
+
+ private void addSymbolLayer(
+ String layerName,
+ String sourceName,
+ String belowLayerId,
+ String sourceLayer,
+ Float minZoom,
+ Float maxZoom,
+ PropertyValue[] properties,
+ boolean enableInteraction,
+ Expression filter) {
+
+ SymbolLayer symbolLayer = new SymbolLayer(layerName, sourceName);
+ symbolLayer.setProperties(properties);
+ if (sourceLayer != null) {
+ symbolLayer.setSourceLayer(sourceLayer);
+ }
+ if (minZoom != null) {
+ symbolLayer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ symbolLayer.setMaxZoom(maxZoom);
+ }
+ if (filter != null) {
+ symbolLayer.setFilter(filter);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(symbolLayer, belowLayerId);
+ } else {
+ style.addLayer(symbolLayer);
+ }
+ if (enableInteraction) {
+ interactiveFeatureLayerIds.add(layerName);
+ }
+ }
+
+ private void addLineLayer(
+ String layerName,
+ String sourceName,
+ String belowLayerId,
+ String sourceLayer,
+ Float minZoom,
+ Float maxZoom,
+ PropertyValue[] properties,
+ boolean enableInteraction,
+ Expression filter) {
+ LineLayer lineLayer = new LineLayer(layerName, sourceName);
+ lineLayer.setProperties(properties);
+ if (sourceLayer != null) {
+ lineLayer.setSourceLayer(sourceLayer);
+ }
+ if (minZoom != null) {
+ lineLayer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ lineLayer.setMaxZoom(maxZoom);
+ }
+ if (filter != null) {
+ lineLayer.setFilter(filter);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(lineLayer, belowLayerId);
+ } else {
+ style.addLayer(lineLayer);
+ }
+ if (enableInteraction) {
+ interactiveFeatureLayerIds.add(layerName);
+ }
+ }
+
+ private void addFillLayer(
+ String layerName,
+ String sourceName,
+ String belowLayerId,
+ String sourceLayer,
+ Float minZoom,
+ Float maxZoom,
+ PropertyValue[] properties,
+ boolean enableInteraction,
+ Expression filter) {
+ FillLayer fillLayer = new FillLayer(layerName, sourceName);
+ fillLayer.setProperties(properties);
+ if (sourceLayer != null) {
+ fillLayer.setSourceLayer(sourceLayer);
+ }
+ if (minZoom != null) {
+ fillLayer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ fillLayer.setMaxZoom(maxZoom);
+ }
+ if (filter != null) {
+ fillLayer.setFilter(filter);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(fillLayer, belowLayerId);
+ } else {
+ style.addLayer(fillLayer);
+ }
+ if (enableInteraction) {
+ interactiveFeatureLayerIds.add(layerName);
+ }
+ }
+
+ private void addFillExtrusionLayer(
+ String layerName,
+ String sourceName,
+ String belowLayerId,
+ String sourceLayer,
+ Float minZoom,
+ Float maxZoom,
+ PropertyValue[] properties,
+ boolean enableInteraction,
+ Expression filter) {
+ FillExtrusionLayer fillLayer = new FillExtrusionLayer(layerName, sourceName);
+ fillLayer.setProperties(properties);
+ if (sourceLayer != null) {
+ fillLayer.setSourceLayer(sourceLayer);
+ }
+ if (minZoom != null) {
+ fillLayer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ fillLayer.setMaxZoom(maxZoom);
+ }
+ if (filter != null) {
+ fillLayer.setFilter(filter);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(fillLayer, belowLayerId);
+ } else {
+ style.addLayer(fillLayer);
+ }
+ if (enableInteraction) {
+ interactiveFeatureLayerIds.add(layerName);
+ }
+ }
+
+ private void addCircleLayer(
+ String layerName,
+ String sourceName,
+ String belowLayerId,
+ String sourceLayer,
+ Float minZoom,
+ Float maxZoom,
+ PropertyValue[] properties,
+ boolean enableInteraction,
+ Expression filter) {
+ CircleLayer circleLayer = new CircleLayer(layerName, sourceName);
+ circleLayer.setProperties(properties);
+ if (sourceLayer != null) {
+ circleLayer.setSourceLayer(sourceLayer);
+ }
+ if (minZoom != null) {
+ circleLayer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ circleLayer.setMaxZoom(maxZoom);
+ }
+ if (filter != null) {
+ circleLayer.setFilter(filter);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(circleLayer, belowLayerId);
+ } else {
+ style.addLayer(circleLayer);
+ }
+ if (enableInteraction) {
+ interactiveFeatureLayerIds.add(layerName);
+ }
+ }
+
+ private Expression parseFilter(String filter) {
+ JsonParser parser = new JsonParser();
+ JsonElement filterJsonElement = parser.parse(filter);
+ return filterJsonElement.isJsonNull() ? null : Expression.Converter.convert(filterJsonElement);
+ }
+
+ private void addRasterLayer(
+ String layerName,
+ String sourceName,
+ Float minZoom,
+ Float maxZoom,
+ String belowLayerId,
+ PropertyValue[] properties,
+ Expression filter) {
+ RasterLayer layer = new RasterLayer(layerName, sourceName);
+ layer.setProperties(properties);
+ if (minZoom != null) {
+ layer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ layer.setMaxZoom(maxZoom);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(layer, belowLayerId);
+ } else {
+ style.addLayer(layer);
+ }
+ }
+
+ private void addHillshadeLayer(
+ String layerName,
+ String sourceName,
+ Float minZoom,
+ Float maxZoom,
+ String belowLayerId,
+ PropertyValue[] properties,
+ Expression filter) {
+ HillshadeLayer layer = new HillshadeLayer(layerName, sourceName);
+ layer.setProperties(properties);
+ if (minZoom != null) {
+ layer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ layer.setMaxZoom(maxZoom);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(layer, belowLayerId);
+ } else {
+ style.addLayer(layer);
+ }
+ }
+
+ private void addHeatmapLayer(
+ String layerName,
+ String sourceName,
+ Float minZoom,
+ Float maxZoom,
+ String belowLayerId,
+ PropertyValue[] properties,
+ Expression filter) {
+ HeatmapLayer layer = new HeatmapLayer(layerName, sourceName);
+ layer.setProperties(properties);
+ if (minZoom != null) {
+ layer.setMinZoom(minZoom);
+ }
+ if (maxZoom != null) {
+ layer.setMaxZoom(maxZoom);
+ }
+ if (belowLayerId != null) {
+ style.addLayerBelow(layer, belowLayerId);
+ } else {
+ style.addLayer(layer);
+ }
+ }
+
+ private Feature firstFeatureOnLayers(RectF in) {
+ if (style != null) {
+ final List layers = style.getLayers();
+ final List layersInOrder = new ArrayList();
+ for (Layer layer : layers) {
+ String id = layer.getId();
+ if (interactiveFeatureLayerIds.contains(id)) {
+ layersInOrder.add(id);
+ }
+ }
+ Collections.reverse(layersInOrder);
+
+ for (String id : layersInOrder) {
+ List features = nextbillionMap.queryRenderedFeatures(in, id);
+ if (!features.isEmpty()) {
+ return features.get(0);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
+
+ switch (call.method) {
+ case "map#waitForMap":
+ if (nextbillionMap != null) {
+ result.success(null);
+ return;
+ }
+ mapReadyResult = result;
+ break;
+ case "map#update":
+ {
+ interpretNextbillionMapOptions(call.argument("options"), this, context);
+ result.success(Convert.toJson(getCameraPosition()));
+ break;
+ }
+ case "map#updateMyLocationTrackingMode":
+ {
+ int myLocationTrackingMode = call.argument("mode");
+ setMyLocationTrackingMode(myLocationTrackingMode);
+ result.success(null);
+ break;
+ }
+ case "map#matchMapLanguageWithDeviceDefault":
+ {
+ try {
+// localizationPlugin.matchMapLanguageWithDeviceDefault();
+ result.success(null);
+ } catch (RuntimeException exception) {
+ Log.d(TAG, exception.toString());
+ result.error("NbMaps LOCALIZATION PLUGIN ERROR", exception.toString(), null);
+ }
+ break;
+ }
+ case "map#updateContentInsets":
+ {
+ HashMap insets = call.argument("bounds");
+ final CameraUpdate cameraUpdate =
+ CameraUpdateFactory.paddingTo(
+ Convert.toPixels(insets.get("left"), density),
+ Convert.toPixels(insets.get("top"), density),
+ Convert.toPixels(insets.get("right"), density),
+ Convert.toPixels(insets.get("bottom"), density));
+
+ if (call.argument("animated")) {
+ animateCamera(cameraUpdate, null, result);
+ } else {
+ moveCamera(cameraUpdate, result);
+ }
+ break;
+ }
+ case "map#setMapLanguage":
+ {
+ final String language = call.argument("language");
+ try {
+// localizationPlugin.setMapLanguage(language);
+ result.success(null);
+ } catch (RuntimeException exception) {
+ Log.d(TAG, exception.toString());
+ result.error("NbMaps LOCALIZATION PLUGIN ERROR", exception.toString(), null);
+ }
+ break;
+ }
+ case "map#getVisibleRegion":
+ {
+ Map reply = new HashMap<>();
+ VisibleRegion visibleRegion = nextbillionMap.getProjection().getVisibleRegion();
+ reply.put(
+ "sw",
+ Arrays.asList(
+ visibleRegion.nearLeft.getLatitude(), visibleRegion.nearLeft.getLongitude()));
+ reply.put(
+ "ne",
+ Arrays.asList(
+ visibleRegion.farRight.getLatitude(), visibleRegion.farRight.getLongitude()));
+ result.success(reply);
+ break;
+ }
+ case "map#toScreenLocation":
+ {
+ Map reply = new HashMap<>();
+ PointF pointf =
+ nextbillionMap
+ .getProjection()
+ .toScreenLocation(
+ new LatLng(call.argument("latitude"), call.argument("longitude")));
+ reply.put("x", pointf.x);
+ reply.put("y", pointf.y);
+ result.success(reply);
+ break;
+ }
+ case "map#toScreenLocationBatch":
+ {
+ double[] param = (double[]) call.argument("coordinates");
+ double[] reply = new double[param.length];
+
+ for (int i = 0; i < param.length; i += 2) {
+ PointF pointf =
+ nextbillionMap.getProjection().toScreenLocation(new LatLng(param[i], param[i + 1]));
+ reply[i] = pointf.x;
+ reply[i + 1] = pointf.y;
+ }
+
+ result.success(reply);
+ break;
+ }
+ case "map#toLatLng":
+ {
+ Map reply = new HashMap<>();
+ LatLng latlng =
+ nextbillionMap
+ .getProjection()
+ .fromScreenLocation(
+ new PointF(
+ ((Double) call.argument("x")).floatValue(),
+ ((Double) call.argument("y")).floatValue()));
+ reply.put("latitude", latlng.getLatitude());
+ reply.put("longitude", latlng.getLongitude());
+ result.success(reply);
+ break;
+ }
+ case "map#getMetersPerPixelAtLatitude":
+ {
+ Map reply = new HashMap<>();
+ Double retVal =
+ nextbillionMap
+ .getProjection()
+ .getMetersPerPixelAtLatitude((Double) call.argument("latitude"));
+ reply.put("metersperpixel", retVal);
+ result.success(reply);
+ break;
+ }
+ case "camera#move":
+ {
+ final CameraUpdate cameraUpdate =
+ Convert.toCameraUpdate(call.argument("cameraUpdate"), nextbillionMap, density);
+ moveCamera(cameraUpdate, result);
+ break;
+ }
+ case "camera#animate":
+ {
+ final CameraUpdate cameraUpdate =
+ Convert.toCameraUpdate(call.argument("cameraUpdate"), nextbillionMap, density);
+ final Integer duration = call.argument("duration");
+
+ animateCamera(cameraUpdate, duration, result);
+ break;
+ }
+ case "map#queryRenderedFeatures":
+ {
+ Map reply = new HashMap<>();
+ List features;
+
+ String[] layerIds = ((List) call.argument("layerIds")).toArray(new String[0]);
+
+ List