Rename Gradle targets Android (#2226)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
import org.maplibre.android.log.Logger;
|
||||
|
||||
/**
|
||||
* Loads the mapbox-gl shared library
|
||||
* <p>
|
||||
* By default uses System.loadLibrary
|
||||
* use {@link #setLibraryLoader(LibraryLoader)} to provide an alternative library loading hook.
|
||||
* </p>
|
||||
*/
|
||||
public abstract class LibraryLoader {
|
||||
|
||||
private static final String TAG = "Mbgl-LibraryLoader";
|
||||
|
||||
private static final LibraryLoader DEFAULT = MapLibre.getModuleProvider()
|
||||
.createLibraryLoaderProvider()
|
||||
.getDefaultLibraryLoader();
|
||||
|
||||
private static volatile LibraryLoader loader = DEFAULT;
|
||||
|
||||
private static boolean loaded;
|
||||
|
||||
/**
|
||||
* Set the library loader that loads the shared library.
|
||||
*
|
||||
* @param libraryLoader the library loader
|
||||
*/
|
||||
public static void setLibraryLoader(LibraryLoader libraryLoader) {
|
||||
loader = libraryLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads "libmaplibre.so" native shared library.
|
||||
* <p>
|
||||
* Catches UnsatisfiedLinkErrors and prints a warning to logcat.
|
||||
* </p>
|
||||
*/
|
||||
public static synchronized void load() {
|
||||
try {
|
||||
if (!loaded) {
|
||||
loaded = true;
|
||||
loader.load("maplibre");
|
||||
}
|
||||
} catch (UnsatisfiedLinkError error) {
|
||||
loaded = false;
|
||||
String message = "Failed to load native shared library.";
|
||||
Logger.e(TAG, message, error);
|
||||
MapStrictMode.strictModeViolation(message, error);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void load(String name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
/**
|
||||
* Injects the default library loader.
|
||||
*/
|
||||
public interface LibraryLoaderProvider {
|
||||
|
||||
/**
|
||||
* Creates and returns a the default Library Loader.
|
||||
*
|
||||
* @return the default library loader
|
||||
*/
|
||||
LibraryLoader getDefaultLibraryLoader();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import timber.log.Timber;
|
||||
|
||||
import org.maplibre.android.constants.MapLibreConstants;
|
||||
import org.maplibre.android.exceptions.MapLibreConfigurationException;
|
||||
import org.maplibre.android.net.ConnectivityReceiver;
|
||||
import org.maplibre.android.storage.FileSource;
|
||||
import org.maplibre.android.util.DefaultStyle;
|
||||
import org.maplibre.android.util.TileServerOptions;
|
||||
import org.maplibre.android.utils.ThreadUtils;
|
||||
|
||||
/**
|
||||
* The entry point to initialize the MapLibre Android SDK.
|
||||
* <p>
|
||||
* Obtain a reference by calling {@link #getInstance(Context, String, WellKnownTileServer)}.
|
||||
* Usually this class is configured in Application#onCreate() and is responsible for the
|
||||
* active API key, application context, and connectivity state.
|
||||
* </p>
|
||||
*/
|
||||
@UiThread
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Keep
|
||||
public final class MapLibre {
|
||||
|
||||
private static final String TAG = "Mbgl-MapLibre";
|
||||
private static ModuleProvider moduleProvider;
|
||||
private static MapLibre INSTANCE;
|
||||
|
||||
private Context context;
|
||||
@Nullable
|
||||
private String apiKey;
|
||||
@Nullable
|
||||
private TileServerOptions tileServerOptions;
|
||||
|
||||
/**
|
||||
* Get an instance of MapLibre.
|
||||
* <p>
|
||||
* This class manages the API key, application context, and connectivity state.
|
||||
* </p>
|
||||
*
|
||||
* @param context Android context which holds or is an application context
|
||||
* @return the single instance of MapLibre
|
||||
*/
|
||||
@UiThread
|
||||
@NonNull
|
||||
public static synchronized MapLibre getInstance(@NonNull Context context) {
|
||||
ThreadUtils.init(context);
|
||||
ThreadUtils.checkThread(TAG);
|
||||
if (INSTANCE == null) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
FileSource.initializeFileDirsPaths(appContext);
|
||||
INSTANCE = new MapLibre(appContext, null);
|
||||
ConnectivityReceiver.instance(appContext);
|
||||
}
|
||||
|
||||
TileServerOptions tileServerOptions = TileServerOptions.get(WellKnownTileServer.MapLibre);
|
||||
INSTANCE.tileServerOptions = tileServerOptions;
|
||||
INSTANCE.apiKey = null;
|
||||
FileSource fileSource = FileSource.getInstance(context);
|
||||
fileSource.setTileServerOptions(tileServerOptions);
|
||||
fileSource.setApiKey(null);
|
||||
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of MapLibre.
|
||||
* <p>
|
||||
* This class manages the API key, application context, and connectivity state.
|
||||
* </p>
|
||||
*
|
||||
* @param context Android context which holds or is an application context
|
||||
* @param apiKey api key
|
||||
* @param tileServer the tile server whose predefined configuration will be used to
|
||||
* bootstrap the SDK. The predefined configuration includes
|
||||
* rules for converting resource URLs between normal and canonical forms
|
||||
* and set of predefined styles available on the server.
|
||||
* @return the single instance of MapLibre
|
||||
*/
|
||||
@UiThread
|
||||
@NonNull
|
||||
public static synchronized MapLibre getInstance(@NonNull Context context, @Nullable String apiKey,
|
||||
WellKnownTileServer tileServer) {
|
||||
ThreadUtils.init(context);
|
||||
ThreadUtils.checkThread(TAG);
|
||||
if (INSTANCE == null) {
|
||||
Timber.plant();
|
||||
Context appContext = context.getApplicationContext();
|
||||
FileSource.initializeFileDirsPaths(appContext);
|
||||
INSTANCE = new MapLibre(appContext, apiKey);
|
||||
ConnectivityReceiver.instance(appContext);
|
||||
} else {
|
||||
INSTANCE.apiKey = apiKey;
|
||||
}
|
||||
|
||||
TileServerOptions tileServerOptions = TileServerOptions.get(tileServer);
|
||||
INSTANCE.tileServerOptions = tileServerOptions;
|
||||
FileSource fileSource = FileSource.getInstance(context);
|
||||
fileSource.setTileServerOptions(tileServerOptions);
|
||||
fileSource.setApiKey(apiKey);
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
MapLibre(@NonNull Context context, @Nullable String apiKey) {
|
||||
this.context = context;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
MapLibre(@NonNull Context context, @Nullable String apiKey, @NonNull TileServerOptions options) {
|
||||
this.context = context;
|
||||
this.apiKey = apiKey;
|
||||
this.tileServerOptions = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current active API key for this application.
|
||||
*
|
||||
* @return API key
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApiKey() {
|
||||
validateMapbox();
|
||||
return INSTANCE.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current active apiKey.
|
||||
*/
|
||||
public static void setApiKey(String apiKey) {
|
||||
validateMapbox();
|
||||
throwIfApiKeyInvalid(apiKey);
|
||||
INSTANCE.apiKey = apiKey;
|
||||
FileSource.getInstance(getApplicationContext()).setApiKey(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tile server configuration.
|
||||
*/
|
||||
@Nullable
|
||||
public static TileServerOptions getTileServerOptions() {
|
||||
validateMapbox();
|
||||
return INSTANCE.tileServerOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pre-defined styles
|
||||
*
|
||||
* @return List of predefined styles
|
||||
*/
|
||||
@Nullable
|
||||
public static DefaultStyle[] getPredefinedStyles() {
|
||||
validateMapbox();
|
||||
if (INSTANCE.tileServerOptions != null) {
|
||||
return INSTANCE.tileServerOptions.getDefaultStyles();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get predefined style by name
|
||||
*
|
||||
* @return Predefined style if found
|
||||
*/
|
||||
@Nullable
|
||||
public static DefaultStyle getPredefinedStyle(String name) {
|
||||
validateMapbox();
|
||||
if (INSTANCE.tileServerOptions != null) {
|
||||
DefaultStyle[] styles = INSTANCE.tileServerOptions.getDefaultStyles();
|
||||
for (DefaultStyle style : styles) {
|
||||
if (style.getName().equalsIgnoreCase(name)) {
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application context
|
||||
*
|
||||
* @return the application context
|
||||
*/
|
||||
@NonNull
|
||||
public static Context getApplicationContext() {
|
||||
validateMapbox();
|
||||
return INSTANCE.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually sets the connectivity state of the app. This is useful for apps which control their
|
||||
* own connectivity state and want to bypass any checks to the ConnectivityManager.
|
||||
*
|
||||
* @param connected flag to determine the connectivity state, true for connected, false for
|
||||
* disconnected, and null for ConnectivityManager to determine.
|
||||
*/
|
||||
public static synchronized void setConnected(Boolean connected) {
|
||||
validateMapbox();
|
||||
ConnectivityReceiver.instance(INSTANCE.context).setConnected(connected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether we have an internet connection available. Please do not rely on this
|
||||
* method in your apps. This method is used internally by the SDK.
|
||||
*
|
||||
* @return true if there is an internet connection, false otherwise
|
||||
*/
|
||||
public static synchronized Boolean isConnected() {
|
||||
validateMapbox();
|
||||
return ConnectivityReceiver.instance(INSTANCE.context).isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module provider
|
||||
*
|
||||
* @return moduleProvider
|
||||
*/
|
||||
@NonNull
|
||||
public static ModuleProvider getModuleProvider() {
|
||||
if (moduleProvider == null) {
|
||||
moduleProvider = new ModuleProviderImpl();
|
||||
}
|
||||
return moduleProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime validation of MapLibre creation.
|
||||
*/
|
||||
private static void validateMapbox() {
|
||||
if (INSTANCE == null) {
|
||||
throw new MapLibreConfigurationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime validation of MapLibre access token
|
||||
*
|
||||
* @param apiKey the access token to validate
|
||||
* @return true is valid, false otherwise
|
||||
*/
|
||||
static boolean isApiKeyValid(@Nullable String apiKey) {
|
||||
if (apiKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
apiKey = apiKey.trim().toLowerCase(MapLibreConstants.MAPLIBRE_LOCALE);
|
||||
return apiKey.length() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws exception when access token is invalid
|
||||
*/
|
||||
public static void throwIfApiKeyInvalid(@Nullable String apiKey) {
|
||||
if (!isApiKeyValid(apiKey)) {
|
||||
throw new MapLibreConfigurationException(
|
||||
"A valid API key is required, currently provided key is: " + apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal use. Check if the {@link MapLibre#INSTANCE} is present.
|
||||
*/
|
||||
public static boolean hasInstance() {
|
||||
return INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal use. Returns AssetManager.
|
||||
*
|
||||
* @return the asset manager
|
||||
*/
|
||||
private static AssetManager getAssetManager() {
|
||||
return getApplicationContext().getResources().getAssets();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
/**
|
||||
* Using this class you can enable a strict mode that will throw the {@link MapStrictModeException}
|
||||
* whenever the map would fail silently otherwise.
|
||||
*/
|
||||
public class MapStrictMode {
|
||||
private static volatile boolean strictModeEnabled;
|
||||
|
||||
/**
|
||||
* Set the strict mode that will throw the {@link MapStrictModeException}
|
||||
* whenever the map would fail silently otherwise.
|
||||
*
|
||||
* @param strictModeEnabled true to enable the strict mode, false otherwise
|
||||
*/
|
||||
public static synchronized void setStrictModeEnabled(boolean strictModeEnabled) {
|
||||
MapStrictMode.strictModeEnabled = strictModeEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal use. Called whenever the strict mode violation occurs.
|
||||
*/
|
||||
public static void strictModeViolation(String message) {
|
||||
if (strictModeEnabled) {
|
||||
throw new MapStrictModeException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal use. Called whenever the strict mode violation occurs.
|
||||
*/
|
||||
public static void strictModeViolation(String message, Throwable throwable) {
|
||||
if (strictModeEnabled) {
|
||||
throw new MapStrictModeException(String.format("%s - %s", message, throwable));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal use. Called whenever the strict mode violation occurs.
|
||||
*/
|
||||
public static void strictModeViolation(Throwable throwable) {
|
||||
if (strictModeEnabled) {
|
||||
throw new MapStrictModeException(String.format("%s", throwable));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
public class MapStrictModeException extends RuntimeException {
|
||||
MapStrictModeException(String message) {
|
||||
super(String.format("Map detected an error that would fail silently otherwise: %s", message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.http.HttpRequest;
|
||||
|
||||
/**
|
||||
* Injects concrete instances of configurable abstractions
|
||||
*/
|
||||
public interface ModuleProvider {
|
||||
|
||||
/**
|
||||
* Create a new concrete implementation of HttpRequest.
|
||||
*
|
||||
* @return a new instance of an HttpRequest
|
||||
*/
|
||||
@NonNull
|
||||
HttpRequest createHttpRequest();
|
||||
|
||||
/**
|
||||
* Get the concrete implementation of LibraryLoaderProvider
|
||||
*
|
||||
* @return a new instance of LibraryLoaderProvider
|
||||
*/
|
||||
@NonNull
|
||||
LibraryLoaderProvider createLibraryLoaderProvider();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.http.HttpRequest;
|
||||
import org.maplibre.android.module.http.HttpRequestImpl;
|
||||
import org.maplibre.android.module.loader.LibraryLoaderProviderImpl;
|
||||
|
||||
public class ModuleProviderImpl implements ModuleProvider {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public HttpRequest createHttpRequest() {
|
||||
return new HttpRequestImpl();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LibraryLoaderProvider createLibraryLoaderProvider() {
|
||||
return new LibraryLoaderProviderImpl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.maplibre.android;
|
||||
|
||||
public enum WellKnownTileServer {
|
||||
Mapbox,
|
||||
MapTiler,
|
||||
MapLibre;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.maps.MapView;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
/**
|
||||
* Annotation is an overlay on top of a Map.
|
||||
* <p>
|
||||
* Known subclasses are {@link Polygon}, {@link Polyline} and {@link Marker}.
|
||||
* </p>
|
||||
* <p>
|
||||
* This class manages attachment to a map and identification, but does not require
|
||||
* content to be placed at a geographical point.
|
||||
* </p>
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class Annotation implements Comparable<Annotation> {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* The annotation id
|
||||
* </p>
|
||||
* Internal C++ id is stored as unsigned int.
|
||||
*/
|
||||
private long id = -1; // -1 unless added to a MapView
|
||||
protected MapLibreMap maplibreMap;
|
||||
protected MapView mapView;
|
||||
|
||||
protected Annotation() {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Gets the annotation's unique ID.
|
||||
* </p>
|
||||
* This ID is unique for a MapView instance and is suitable for associating your own extra
|
||||
* data with.
|
||||
*
|
||||
* @return the assigned id.
|
||||
*/
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*/
|
||||
public void remove() {
|
||||
if (maplibreMap == null) {
|
||||
return;
|
||||
}
|
||||
maplibreMap.removeAnnotation(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @param id the assigned id
|
||||
*/
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @param maplibreMap the hosting mapbox map
|
||||
*/
|
||||
public void setMapboxMap(MapLibreMap maplibreMap) {
|
||||
this.maplibreMap = maplibreMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hosting mapbox map.
|
||||
*
|
||||
* @return the MapboxMap
|
||||
*/
|
||||
protected MapLibreMap getMapboxMap() {
|
||||
return maplibreMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @param mapView the hosting map view
|
||||
*/
|
||||
public void setMapView(MapView mapView) {
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hosting map view.
|
||||
*
|
||||
* @return The MapView
|
||||
*/
|
||||
protected MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this Annotation object with another Annotation.
|
||||
*
|
||||
* @param annotation Another Annotation to compare with this object.
|
||||
* @return returns 0 if id's match, 1 if id is lower, -1 if id is higher of another Annotation
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NonNull Annotation annotation) {
|
||||
if (id < annotation.getId()) {
|
||||
return 1;
|
||||
} else if (id > annotation.getId()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this Annotation object is equal to another Annotation.
|
||||
*
|
||||
* @param object Another Annotation to check equality with this object.
|
||||
* @return returns true both id's match else returns false.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (object == null || !(object instanceof Annotation)) {
|
||||
return false;
|
||||
}
|
||||
Annotation that = (Annotation) object;
|
||||
return id == that.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int) (getId() ^ (getId() >>> 32));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Deprecated
|
||||
class ArrowDirection {
|
||||
@IntDef( {LEFT, RIGHT, TOP, BOTTOM})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Value {
|
||||
}
|
||||
|
||||
static final int LEFT = 0;
|
||||
static final int RIGHT = 1;
|
||||
static final int TOP = 2;
|
||||
static final int BOTTOM = 3;
|
||||
|
||||
@Value
|
||||
private final int value;
|
||||
|
||||
ArrowDirection(@Value int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
/**
|
||||
* Abstract builder class for composing custom Marker objects.
|
||||
* <p>
|
||||
* Extending this class requires implementing Parceable interface.
|
||||
*
|
||||
* @param <U> Type of the marker to be composed
|
||||
* @param <T> Type of the builder to be used for composing a custom Marker
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class BaseMarkerOptions<U extends Marker, T extends BaseMarkerOptions<U, T>> implements Parcelable {
|
||||
|
||||
protected LatLng position;
|
||||
protected String snippet;
|
||||
protected String title;
|
||||
protected Icon icon;
|
||||
|
||||
/**
|
||||
* Set the geographical location of the Marker.
|
||||
*
|
||||
* @param position the location to position the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T position(LatLng position) {
|
||||
this.position = position;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the snippet of the Marker.
|
||||
*
|
||||
* @param snippet the snippet of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T snippet(String snippet) {
|
||||
this.snippet = snippet;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the Marker.
|
||||
*
|
||||
* @param title the title of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T title(String title) {
|
||||
this.title = title;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icon of the Marker.
|
||||
*
|
||||
* @param icon the icon of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T icon(Icon icon) {
|
||||
this.icon = icon;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icon of the Marker.
|
||||
*
|
||||
* @param icon the icon of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T setIcon(Icon icon) {
|
||||
return icon(icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the geographical location of the Marker.
|
||||
*
|
||||
* @param position the location to position the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T setPosition(LatLng position) {
|
||||
return position(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the snippet of the Marker.
|
||||
*
|
||||
* @param snippet the snippet of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T setSnippet(String snippet) {
|
||||
return snippet(snippet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the Marker.
|
||||
*
|
||||
* @param title the title of the {@link Marker}.
|
||||
* @return the object for which the method was called.
|
||||
*/
|
||||
public T setTitle(String title) {
|
||||
return title(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of the object for which this method was called.
|
||||
*
|
||||
* @return the object for which the this method was called.
|
||||
*/
|
||||
public abstract T getThis();
|
||||
|
||||
/**
|
||||
* Get the Marker.
|
||||
*
|
||||
* @return the Marker created from this builder.
|
||||
*/
|
||||
public abstract U getMarker();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Multipoint is an abstract annotation for combining geographical locations.
|
||||
*
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class BasePointCollection extends Annotation {
|
||||
|
||||
@Keep
|
||||
private List<LatLng> points;
|
||||
@Keep
|
||||
private float alpha = 1.0f;
|
||||
|
||||
protected BasePointCollection() {
|
||||
super();
|
||||
points = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the points.
|
||||
*
|
||||
* @return A {@link List} of points.
|
||||
*/
|
||||
@NonNull
|
||||
public List<LatLng> getPoints() {
|
||||
return new ArrayList<>(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the points of this polyline. This method will take a copy of the points, so further
|
||||
* mutations to points will have no effect on this polyline.
|
||||
*
|
||||
* @param points A {@link List} of {@link LatLng} points making up the polyline.
|
||||
*/
|
||||
public void setPoints(@NonNull List<LatLng> points) {
|
||||
this.points = new ArrayList<>(points);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a point to the polyline.
|
||||
*
|
||||
* @param point A {@link LatLng} point to be added.
|
||||
*/
|
||||
public void addPoint(LatLng point) {
|
||||
points.add(point);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Value between 0 and 1 defining the polyline alpha.
|
||||
*
|
||||
* @return float value between 0 and 1.
|
||||
*/
|
||||
public float getAlpha() {
|
||||
return alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this {@link BasePointCollection}s alpha.
|
||||
*
|
||||
* @param alpha float value between 0 and 1.
|
||||
*/
|
||||
public void setAlpha(float alpha) {
|
||||
this.alpha = alpha;
|
||||
update();
|
||||
}
|
||||
|
||||
abstract void update();
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@Deprecated
|
||||
class Bubble extends Drawable {
|
||||
|
||||
private RectF rect;
|
||||
private float arrowWidth;
|
||||
private float arrowHeight;
|
||||
private float arrowPosition;
|
||||
private float cornersRadius;
|
||||
@NonNull
|
||||
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private float strokeWidth;
|
||||
private Paint strokePaint;
|
||||
private Path strokePath;
|
||||
@NonNull
|
||||
private Path path = new Path();
|
||||
|
||||
Bubble(@NonNull RectF rect, @NonNull ArrowDirection arrowDirection, float arrowWidth, float arrowHeight,
|
||||
float arrowPosition, float cornersRadius, int bubbleColor, float strokeWidth, int strokeColor) {
|
||||
this.rect = rect;
|
||||
this.arrowWidth = arrowWidth;
|
||||
this.arrowHeight = arrowHeight;
|
||||
this.arrowPosition = arrowPosition;
|
||||
this.cornersRadius = cornersRadius;
|
||||
paint.setColor(bubbleColor);
|
||||
this.strokeWidth = strokeWidth;
|
||||
|
||||
if (strokeWidth > 0) {
|
||||
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
strokePaint.setColor(strokeColor);
|
||||
strokePath = new Path();
|
||||
initPath(arrowDirection, path, strokeWidth);
|
||||
initPath(arrowDirection, strokePath, 0);
|
||||
} else {
|
||||
initPath(arrowDirection, path, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (strokeWidth > 0) {
|
||||
canvas.drawPath(strokePath, strokePaint);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
paint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return (int) rect.width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return (int) rect.height();
|
||||
}
|
||||
|
||||
private void initPath(@NonNull ArrowDirection arrowDirection, @NonNull Path path, float strokeWidth) {
|
||||
switch (arrowDirection.getValue()) {
|
||||
case ArrowDirection.LEFT:
|
||||
if (cornersRadius <= 0) {
|
||||
initLeftSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
if (strokeWidth > 0 && strokeWidth > cornersRadius) {
|
||||
initLeftSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
initLeftRoundedPath(rect, path, strokeWidth);
|
||||
break;
|
||||
case ArrowDirection.TOP:
|
||||
if (cornersRadius <= 0) {
|
||||
initTopSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
if (strokeWidth > 0 && strokeWidth > cornersRadius) {
|
||||
initTopSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
initTopRoundedPath(rect, path, strokeWidth);
|
||||
break;
|
||||
case ArrowDirection.RIGHT:
|
||||
if (cornersRadius <= 0) {
|
||||
initRightSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
if (strokeWidth > 0 && strokeWidth > cornersRadius) {
|
||||
initRightSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
initRightRoundedPath(rect, path, strokeWidth);
|
||||
break;
|
||||
case ArrowDirection.BOTTOM:
|
||||
if (cornersRadius <= 0) {
|
||||
initBottomSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
if (strokeWidth > 0 && strokeWidth > cornersRadius) {
|
||||
initBottomSquarePath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
initBottomRoundedPath(rect, path, strokeWidth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void initLeftSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(arrowWidth + rect.left + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.width() - strokeWidth, rect.top + strokeWidth);
|
||||
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, arrowHeight + arrowPosition - (strokeWidth / 2));
|
||||
path.lineTo(rect.left + strokeWidth + strokeWidth, arrowPosition + arrowHeight / 2);
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, arrowPosition + (strokeWidth / 2));
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, rect.top + strokeWidth);
|
||||
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initLeftRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(arrowWidth + rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.width() - cornersRadius - strokeWidth, rect.top + strokeWidth);
|
||||
path.arcTo(new RectF(rect.right - cornersRadius, rect.top + strokeWidth, rect.right - strokeWidth,
|
||||
cornersRadius + rect.top), 270, 90);
|
||||
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - cornersRadius - strokeWidth);
|
||||
path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius,
|
||||
rect.right - strokeWidth, rect.bottom - strokeWidth), 0, 90);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + cornersRadius + strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + arrowWidth + strokeWidth, rect.bottom - cornersRadius,
|
||||
cornersRadius + rect.left + arrowWidth, rect.bottom - strokeWidth), 90, 90);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, arrowHeight + arrowPosition - (strokeWidth / 2));
|
||||
|
||||
path.lineTo(rect.left + strokeWidth + strokeWidth, arrowPosition + arrowHeight / 2);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, arrowPosition + (strokeWidth / 2));
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, rect.top + cornersRadius + strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + arrowWidth + strokeWidth, rect.top + strokeWidth, cornersRadius
|
||||
+ rect.left + arrowWidth, cornersRadius + rect.top), 180, 90);
|
||||
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initTopSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + arrowPosition + strokeWidth, rect.top + arrowHeight + strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.top + arrowHeight + strokeWidth);
|
||||
path.lineTo(rect.left + arrowWidth / 2 + arrowPosition, rect.top + strokeWidth + strokeWidth);
|
||||
path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.top + arrowHeight + strokeWidth);
|
||||
path.lineTo(rect.right - strokeWidth, rect.top + arrowHeight + strokeWidth);
|
||||
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + strokeWidth, rect.top + arrowHeight + strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + arrowPosition + strokeWidth, rect.top + arrowHeight + strokeWidth);
|
||||
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initTopRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + Math.min(arrowPosition, cornersRadius) + strokeWidth, rect.top + arrowHeight
|
||||
+ strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.top + arrowHeight + strokeWidth);
|
||||
path.lineTo(rect.left + arrowWidth / 2 + arrowPosition, rect.top + strokeWidth + strokeWidth);
|
||||
path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.top + arrowHeight + strokeWidth);
|
||||
path.lineTo(rect.right - cornersRadius - strokeWidth, rect.top + arrowHeight + strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.right - cornersRadius,
|
||||
rect.top + arrowHeight + strokeWidth, rect.right - strokeWidth, cornersRadius + rect.top + arrowHeight),
|
||||
270, 90);
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - cornersRadius - strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius,
|
||||
rect.right - strokeWidth, rect.bottom - strokeWidth), 0, 90);
|
||||
path.lineTo(rect.left + cornersRadius + strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius,
|
||||
cornersRadius + rect.left, rect.bottom - strokeWidth), 90, 90);
|
||||
|
||||
path.lineTo(rect.left + strokeWidth, rect.top + arrowHeight + cornersRadius + strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.top + arrowHeight + strokeWidth, cornersRadius
|
||||
+ rect.left, cornersRadius + rect.top + arrowHeight), 180, 90);
|
||||
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initRightSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.width() - arrowWidth - strokeWidth, rect.top + strokeWidth);
|
||||
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + (strokeWidth / 2));
|
||||
path.lineTo(rect.right - strokeWidth - strokeWidth, arrowPosition + arrowHeight / 2);
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + arrowHeight - (strokeWidth / 2));
|
||||
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + strokeWidth, rect.bottom - strokeWidth);
|
||||
path.lineTo(rect.left + strokeWidth, rect.top + strokeWidth);
|
||||
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initRightRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.width() - cornersRadius - arrowWidth - strokeWidth, rect.top + strokeWidth);
|
||||
path.arcTo(new RectF(rect.right - cornersRadius - arrowWidth,
|
||||
rect.top + strokeWidth, rect.right - arrowWidth - strokeWidth, cornersRadius + rect.top), 270, 90);
|
||||
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + (strokeWidth / 2));
|
||||
path.lineTo(rect.right - strokeWidth - strokeWidth, arrowPosition + arrowHeight / 2);
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + arrowHeight - (strokeWidth / 2));
|
||||
path.lineTo(rect.right - arrowWidth - strokeWidth, rect.bottom - cornersRadius - strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.right - cornersRadius - arrowWidth, rect.bottom - cornersRadius,
|
||||
rect.right - arrowWidth - strokeWidth, rect.bottom - strokeWidth), 0, 90);
|
||||
path.lineTo(rect.left + arrowWidth + strokeWidth, rect.bottom - strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius,
|
||||
cornersRadius + rect.left, rect.bottom - strokeWidth), 90, 90);
|
||||
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.top + strokeWidth, cornersRadius
|
||||
+ rect.left, cornersRadius + rect.top), 180, 90);
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initBottomSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.right - strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - arrowHeight - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + arrowWidth / 2, rect.bottom - strokeWidth - strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + strokeWidth, rect.bottom - arrowHeight - strokeWidth);
|
||||
|
||||
path.lineTo(rect.left + strokeWidth, rect.bottom - arrowHeight - strokeWidth);
|
||||
path.lineTo(rect.left + strokeWidth, rect.top + strokeWidth);
|
||||
path.close();
|
||||
}
|
||||
|
||||
private void initBottomRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) {
|
||||
path.moveTo(rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth);
|
||||
path.lineTo(rect.width() - cornersRadius - strokeWidth, rect.top + strokeWidth);
|
||||
path.arcTo(new RectF(rect.right - cornersRadius,
|
||||
rect.top + strokeWidth, rect.right - strokeWidth, cornersRadius + rect.top), 270, 90);
|
||||
|
||||
path.lineTo(rect.right - strokeWidth, rect.bottom - arrowHeight - cornersRadius - strokeWidth);
|
||||
path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius - arrowHeight,
|
||||
rect.right - strokeWidth, rect.bottom - arrowHeight - strokeWidth), 0, 90);
|
||||
|
||||
path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + arrowWidth / 2, rect.bottom - strokeWidth - strokeWidth);
|
||||
path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth);
|
||||
path.lineTo(rect.left + Math.min(cornersRadius, arrowPosition) + strokeWidth, rect.bottom - arrowHeight
|
||||
- strokeWidth);
|
||||
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius - arrowHeight,
|
||||
cornersRadius + rect.left, rect.bottom - arrowHeight - strokeWidth), 90, 90);
|
||||
path.lineTo(rect.left + strokeWidth, rect.top + cornersRadius + strokeWidth);
|
||||
path.arcTo(new RectF(rect.left + strokeWidth, rect.top + strokeWidth, cornersRadius
|
||||
+ rect.left, cornersRadius + rect.top), 180, 90);
|
||||
path.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.R;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.layers.SymbolLayer;
|
||||
|
||||
/**
|
||||
* Bubble View for Android with custom stroke width and color, arrow size, position and direction.
|
||||
* This class has been deprecated as of the 7.0.0 version of the Maps SDK for Android.
|
||||
* However, even though the Maps SDK for Android team hasn't continued work on this class,
|
||||
* the class is still completely fine to use. This class can be used to create a custom
|
||||
* {@link android.view.View}, which is then turned into a {@link android.graphics.Bitmap}.
|
||||
* After the bitmap is added to the {@link Style} object, a
|
||||
* {@link SymbolLayer} or the
|
||||
* <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> can reference the image ID.
|
||||
*/
|
||||
@Deprecated
|
||||
public class BubbleLayout extends LinearLayout {
|
||||
|
||||
public static final float DEFAULT_STROKE_WIDTH = -1;
|
||||
private ArrowDirection arrowDirection;
|
||||
private float arrowWidth;
|
||||
private float arrowHeight;
|
||||
private float arrowPosition;
|
||||
private float cornersRadius;
|
||||
private Bubble bubble;
|
||||
private int bubbleColor;
|
||||
private float strokeWidth;
|
||||
private int strokeColor;
|
||||
|
||||
/**
|
||||
* Creates an instance of bubble layout.
|
||||
*
|
||||
* @param context The context used to inflate this bubble layout
|
||||
*/
|
||||
public BubbleLayout(@NonNull Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of bubble layout.
|
||||
*
|
||||
* @param context The context used to inflate this bubble layout
|
||||
* @param attrs The attribute set to initialise this bubble layout from
|
||||
*/
|
||||
public BubbleLayout(@NonNull Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of bubble layout.
|
||||
*
|
||||
* @param context The context used to inflate this bubble layout
|
||||
* @param attrs The attribute set to initialise this bubble layout from
|
||||
* @param defStyleAttr The default style to apply this bubble layout with
|
||||
*/
|
||||
public BubbleLayout(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.maplibre_BubbleLayout);
|
||||
@ArrowDirection.Value
|
||||
int location = a.getInt(R.styleable.maplibre_BubbleLayout_maplibre_bl_arrowDirection,
|
||||
ArrowDirection.LEFT);
|
||||
arrowDirection = new ArrowDirection(location);
|
||||
arrowWidth = a.getDimension(R.styleable.maplibre_BubbleLayout_maplibre_bl_arrowWidth,
|
||||
convertDpToPixel(8, context));
|
||||
arrowHeight = a.getDimension(R.styleable.maplibre_BubbleLayout_maplibre_bl_arrowHeight,
|
||||
convertDpToPixel(8, context));
|
||||
arrowPosition = a.getDimension(R.styleable.maplibre_BubbleLayout_maplibre_bl_arrowPosition,
|
||||
convertDpToPixel(12, context));
|
||||
cornersRadius = a.getDimension(R.styleable.maplibre_BubbleLayout_maplibre_bl_cornersRadius, 0);
|
||||
bubbleColor = a.getColor(R.styleable.maplibre_BubbleLayout_maplibre_bl_bubbleColor, Color.WHITE);
|
||||
strokeWidth =
|
||||
a.getDimension(R.styleable.maplibre_BubbleLayout_maplibre_bl_strokeWidth, DEFAULT_STROKE_WIDTH);
|
||||
strokeColor = a.getColor(R.styleable.maplibre_BubbleLayout_maplibre_bl_strokeColor, Color.GRAY);
|
||||
|
||||
a.recycle();
|
||||
initPadding();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
initDrawable(0, getWidth(), 0, getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(@NonNull Canvas canvas) {
|
||||
if (bubble != null) {
|
||||
bubble.draw(canvas);
|
||||
}
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
|
||||
static float convertDpToPixel(float dp, Context context) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arrow direction.
|
||||
*
|
||||
* @return the arrow direction
|
||||
*/
|
||||
public ArrowDirection getArrowDirection() {
|
||||
return arrowDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the arrow direction.
|
||||
*
|
||||
* @param arrowDirection The direction of the arrow
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setArrowDirection(ArrowDirection arrowDirection) {
|
||||
resetPadding();
|
||||
this.arrowDirection = arrowDirection;
|
||||
initPadding();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arrow width.
|
||||
*
|
||||
* @return the width of the arrow
|
||||
*/
|
||||
public float getArrowWidth() {
|
||||
return arrowWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the arrow width.
|
||||
*
|
||||
* @param arrowWidth The width of the arrow
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setArrowWidth(float arrowWidth) {
|
||||
resetPadding();
|
||||
this.arrowWidth = arrowWidth;
|
||||
initPadding();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arrow height
|
||||
*
|
||||
* @return the height of the arrow
|
||||
*/
|
||||
public float getArrowHeight() {
|
||||
return arrowHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the arrow height.
|
||||
*
|
||||
* @param arrowHeight The height of the arrow
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setArrowHeight(float arrowHeight) {
|
||||
resetPadding();
|
||||
this.arrowHeight = arrowHeight;
|
||||
initPadding();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arrow position.
|
||||
*
|
||||
* @return the arrow position
|
||||
*/
|
||||
public float getArrowPosition() {
|
||||
return arrowPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arrow position.
|
||||
*
|
||||
* @param arrowPosition The arrow position
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setArrowPosition(float arrowPosition) {
|
||||
resetPadding();
|
||||
this.arrowPosition = arrowPosition;
|
||||
initPadding();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corner radius
|
||||
*
|
||||
* @return the corner radius
|
||||
*/
|
||||
public float getCornersRadius() {
|
||||
return cornersRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the corner radius
|
||||
*
|
||||
* @param cornersRadius The corner radius
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setCornersRadius(float cornersRadius) {
|
||||
this.cornersRadius = cornersRadius;
|
||||
requestLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bubble color.
|
||||
*
|
||||
* @return the bubble color
|
||||
*/
|
||||
public int getBubbleColor() {
|
||||
return bubbleColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bubble color.
|
||||
*
|
||||
* @param bubbleColor The buble color
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setBubbleColor(int bubbleColor) {
|
||||
this.bubbleColor = bubbleColor;
|
||||
requestLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stroke width.
|
||||
*
|
||||
* @return the stroke width
|
||||
*/
|
||||
public float getStrokeWidth() {
|
||||
return strokeWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the stroke width.
|
||||
*
|
||||
* @param strokeWidth The stroke width
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setStrokeWidth(float strokeWidth) {
|
||||
resetPadding();
|
||||
this.strokeWidth = strokeWidth;
|
||||
initPadding();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stroke color.
|
||||
*
|
||||
* @return the stroke color
|
||||
*/
|
||||
public int getStrokeColor() {
|
||||
return strokeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the stroke color.
|
||||
*
|
||||
* @param strokeColor The stroke color
|
||||
* @return this
|
||||
*/
|
||||
@NonNull
|
||||
public BubbleLayout setStrokeColor(int strokeColor) {
|
||||
this.strokeColor = strokeColor;
|
||||
requestLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void initPadding() {
|
||||
int paddingLeft = getPaddingLeft();
|
||||
int paddingRight = getPaddingRight();
|
||||
int paddingTop = getPaddingTop();
|
||||
int paddingBottom = getPaddingBottom();
|
||||
switch (arrowDirection.getValue()) {
|
||||
case ArrowDirection.LEFT:
|
||||
paddingLeft += arrowWidth;
|
||||
break;
|
||||
case ArrowDirection.RIGHT:
|
||||
paddingRight += arrowWidth;
|
||||
break;
|
||||
case ArrowDirection.TOP:
|
||||
paddingTop += arrowHeight;
|
||||
break;
|
||||
case ArrowDirection.BOTTOM:
|
||||
paddingBottom += arrowHeight;
|
||||
break;
|
||||
}
|
||||
if (strokeWidth > 0) {
|
||||
paddingLeft += strokeWidth;
|
||||
paddingRight += strokeWidth;
|
||||
paddingTop += strokeWidth;
|
||||
paddingBottom += strokeWidth;
|
||||
}
|
||||
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
|
||||
}
|
||||
|
||||
private void initDrawable(int left, int right, int top, int bottom) {
|
||||
if (right < left || bottom < top) {
|
||||
return;
|
||||
}
|
||||
|
||||
RectF rectF = new RectF(left, top, right, bottom);
|
||||
bubble = new Bubble(rectF, arrowDirection, arrowWidth, arrowHeight, arrowPosition, cornersRadius,
|
||||
bubbleColor, strokeWidth, strokeColor);
|
||||
}
|
||||
|
||||
private void resetPadding() {
|
||||
int paddingLeft = getPaddingLeft();
|
||||
int paddingRight = getPaddingRight();
|
||||
int paddingTop = getPaddingTop();
|
||||
int paddingBottom = getPaddingBottom();
|
||||
switch (arrowDirection.getValue()) {
|
||||
case ArrowDirection.LEFT:
|
||||
paddingLeft -= arrowWidth;
|
||||
break;
|
||||
case ArrowDirection.RIGHT:
|
||||
paddingRight -= arrowWidth;
|
||||
break;
|
||||
case ArrowDirection.TOP:
|
||||
paddingTop -= arrowHeight;
|
||||
break;
|
||||
case ArrowDirection.BOTTOM:
|
||||
paddingBottom -= arrowHeight;
|
||||
break;
|
||||
}
|
||||
if (strokeWidth > 0) {
|
||||
paddingLeft -= strokeWidth;
|
||||
paddingRight -= strokeWidth;
|
||||
paddingTop -= strokeWidth;
|
||||
paddingBottom -= strokeWidth;
|
||||
}
|
||||
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.R;
|
||||
|
||||
@Deprecated
|
||||
class BubblePopupHelper {
|
||||
|
||||
@NonNull
|
||||
static PopupWindow create(@NonNull Context context, @NonNull BubbleLayout bubbleLayout) {
|
||||
PopupWindow popupWindow = new PopupWindow(context);
|
||||
|
||||
popupWindow.setContentView(bubbleLayout);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setAnimationStyle(android.R.style.Animation_Dialog);
|
||||
// change background color to transparent
|
||||
Drawable drawable;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
drawable = context.getDrawable(R.drawable.maplibre_popup_window_transparent);
|
||||
} else {
|
||||
drawable = context.getResources().getDrawable(R.drawable.maplibre_popup_window_transparent);
|
||||
}
|
||||
popupWindow.setBackgroundDrawable(drawable);
|
||||
|
||||
return popupWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Icon is the visual representation of a Marker on a MapView.
|
||||
*
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public class Icon {
|
||||
|
||||
private Bitmap mBitmap;
|
||||
private String mId;
|
||||
|
||||
Icon(String id, Bitmap bitmap) {
|
||||
mId = id;
|
||||
mBitmap = bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* String identifier for this icon.
|
||||
*
|
||||
* @return String identifier for this icon.
|
||||
*/
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bitmap being used for this icon.
|
||||
*
|
||||
* @return The bitmap being used for the icon.
|
||||
*/
|
||||
public Bitmap getBitmap() {
|
||||
if (mBitmap != null && mBitmap.getConfig() != Bitmap.Config.ARGB_8888) {
|
||||
mBitmap = mBitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
}
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon bitmap scale.
|
||||
* <p>
|
||||
* Requires the bitmap to be set before calling this method.
|
||||
* </p>
|
||||
*
|
||||
* @return the scale of the bitmap
|
||||
*/
|
||||
public float getScale() {
|
||||
if (mBitmap == null) {
|
||||
throw new IllegalStateException("Required to set a Icon before calling getScale");
|
||||
}
|
||||
float density = mBitmap.getDensity();
|
||||
if (density == Bitmap.DENSITY_NONE) {
|
||||
density = DisplayMetrics.DENSITY_DEFAULT;
|
||||
}
|
||||
return density / DisplayMetrics.DENSITY_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon bitmap bytes.
|
||||
* <p>
|
||||
* Requires the bitmap to be set before calling this method.
|
||||
* </p>
|
||||
*
|
||||
* @return the bytes of the bitmap
|
||||
*/
|
||||
@NonNull
|
||||
public byte[] toBytes() {
|
||||
if (mBitmap == null) {
|
||||
throw new IllegalStateException("Required to set a Icon before calling toBytes");
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(mBitmap.getRowBytes() * mBitmap.getHeight());
|
||||
mBitmap.copyPixelsToBuffer(buffer);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this icon object with another icon and determines if they match.
|
||||
*
|
||||
* @param object Another iconi to compare with this object.
|
||||
* @return True if the icon being passed in matches this icon object. Else, false.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (object == null || getClass() != object.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Icon icon = (Icon) object;
|
||||
return mBitmap.equals(icon.mBitmap) && mId.equals(icon.mId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 0;
|
||||
if (mBitmap != null) {
|
||||
result = mBitmap.hashCode();
|
||||
}
|
||||
if (mId != null) {
|
||||
result = 31 * result + mId.hashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.MapStrictMode;
|
||||
import org.maplibre.android.R;
|
||||
import org.maplibre.android.exceptions.TooManyIconsException;
|
||||
import org.maplibre.android.utils.BitmapUtils;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Factory for creating Icons from bitmap images.
|
||||
* <p>
|
||||
* icon is used to display bitmaps on top of the map using {@link Marker}.
|
||||
* </p>
|
||||
*
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class IconFactory {
|
||||
|
||||
private static final String ICON_ID_PREFIX = "com.mapbox.icons.icon_";
|
||||
|
||||
private Context context;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static IconFactory instance;
|
||||
private Icon defaultMarker;
|
||||
private BitmapFactory.Options options;
|
||||
|
||||
private int nextId = 0;
|
||||
|
||||
/**
|
||||
* Get a single instance of IconFactory.
|
||||
*
|
||||
* @param context the context to derive the application context from
|
||||
* @return the single instance of IconFactory
|
||||
*/
|
||||
public static synchronized IconFactory getInstance(@NonNull Context context) {
|
||||
if (instance == null) {
|
||||
instance = new IconFactory(context.getApplicationContext());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private IconFactory(@NonNull Context context) {
|
||||
this.context = context;
|
||||
DisplayMetrics realMetrics = null;
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
realMetrics = new DisplayMetrics();
|
||||
wm.getDefaultDisplay().getRealMetrics(realMetrics);
|
||||
}
|
||||
wm.getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
options = new BitmapFactory.Options();
|
||||
options.inScaled = true;
|
||||
options.inDensity = DisplayMetrics.DENSITY_DEFAULT;
|
||||
options.inTargetDensity = metrics.densityDpi;
|
||||
if (realMetrics != null) {
|
||||
options.inScreenDensity = realMetrics.densityDpi;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an icon from a given Bitmap image.
|
||||
*
|
||||
* @param bitmap image used for creating the Icon.
|
||||
* @return The icon using the given Bitmap image.
|
||||
*/
|
||||
public Icon fromBitmap(@NonNull Bitmap bitmap) {
|
||||
if (nextId < 0) {
|
||||
throw new TooManyIconsException();
|
||||
}
|
||||
String id = ICON_ID_PREFIX + ++nextId;
|
||||
return new Icon(id, bitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an icon using the resource ID of a Bitmap image.
|
||||
*
|
||||
* @param resourceId The resource ID of a Bitmap image.
|
||||
* @return The icon that was loaded from the asset or {@code null} if failed to load.
|
||||
*/
|
||||
public Icon fromResource(@DrawableRes int resourceId) {
|
||||
Drawable drawable = BitmapUtils.getDrawableFromRes(context, resourceId);
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
|
||||
return fromBitmap(bitmapDrawable.getBitmap());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Failed to decode image. The resource provided must be a Bitmap.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an icon using the default marker icon used for {@link Marker}.
|
||||
*
|
||||
* @return An icon with the default {@link Marker} icon.
|
||||
*/
|
||||
public Icon defaultMarker() {
|
||||
if (defaultMarker == null) {
|
||||
defaultMarker = fromResource(R.drawable.maplibre_marker_icon_default);
|
||||
}
|
||||
return defaultMarker;
|
||||
}
|
||||
|
||||
private Icon fromInputStream(@NonNull InputStream is) {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
|
||||
return fromBitmap(bitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Icon using the name of a Bitmap image in the assets directory.
|
||||
*
|
||||
* @param assetName The name of a Bitmap image in the assets directory.
|
||||
* @return The Icon that was loaded from the asset or null if failed to load.
|
||||
*/
|
||||
public Icon fromAsset(@NonNull String assetName) {
|
||||
InputStream is;
|
||||
try {
|
||||
is = context.getAssets().open(assetName);
|
||||
} catch (IOException ioException) {
|
||||
MapStrictMode.strictModeViolation(ioException);
|
||||
return null;
|
||||
}
|
||||
return fromInputStream(is);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Icon using the absolute file path of a Bitmap image.
|
||||
*
|
||||
* @param absolutePath The absolute path of the Bitmap image.
|
||||
* @return The Icon that was loaded from the absolute path or null if failed to load.
|
||||
*/
|
||||
public Icon fromPath(@NonNull String absolutePath) {
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, options);
|
||||
return fromBitmap(bitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Icon using the name of a Bitmap image file located in the internal storage.
|
||||
* In particular, this calls Context#openFileInput(String).
|
||||
*
|
||||
* @param fileName The name of the Bitmap image file.
|
||||
* @return The Icon that was loaded from the asset or null if failed to load.
|
||||
* @see <a href="https://developer.android.com/guide/topics/data/data-storage.html#filesInternal">
|
||||
* Using the Internal Storage</a>
|
||||
*/
|
||||
public Icon fromFile(@NonNull String fileName) {
|
||||
FileInputStream is;
|
||||
try {
|
||||
is = context.openFileInput(fileName);
|
||||
} catch (FileNotFoundException fileNotFoundException) {
|
||||
MapStrictMode.strictModeViolation(fileNotFoundException);
|
||||
return null;
|
||||
}
|
||||
return fromInputStream(is);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Icon using a previously created icon identifier along with a provided Bitmap.
|
||||
*
|
||||
* @param iconId The Icon identifier you'd like to recreate.
|
||||
* @param bitmap a Bitmap used to replace the current one.
|
||||
* @return The Icon using the new Bitmap.
|
||||
*/
|
||||
public static Icon recreate(@NonNull String iconId, @NonNull Bitmap bitmap) {
|
||||
return new Icon(iconId, bitmap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PointF;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.R;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.maps.MapView;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* {@code InfoWindow} is a tooltip shown when a {@link Marker} is tapped. Only
|
||||
* one info window is displayed at a time. When the user clicks on a marker, the currently open info
|
||||
* window will be closed and the new info window will be displayed. If the user clicks the same
|
||||
* marker while its info window is currently open, the info window will be closed.
|
||||
* <p>
|
||||
* The info window is drawn oriented against the device's screen, centered above its associated
|
||||
* marker by default. The default info window contains the title in bold and snippet text below the title.
|
||||
* While either the title and
|
||||
* snippet are optional, at least one is required to open the info window.
|
||||
* </p>
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public class InfoWindow {
|
||||
|
||||
private WeakReference<Marker> boundMarker;
|
||||
private WeakReference<MapLibreMap> mapboxMap;
|
||||
protected WeakReference<View> view;
|
||||
|
||||
private float markerHeightOffset;
|
||||
private float markerWidthOffset;
|
||||
private float viewWidthOffset;
|
||||
private float viewHeightOffset;
|
||||
private PointF coordinates;
|
||||
private boolean isVisible;
|
||||
|
||||
@LayoutRes
|
||||
private int layoutRes;
|
||||
|
||||
InfoWindow(MapView mapView, int layoutResId, MapLibreMap maplibreMap) {
|
||||
layoutRes = layoutResId;
|
||||
View view = LayoutInflater.from(mapView.getContext()).inflate(layoutResId, mapView, false);
|
||||
initialize(view, maplibreMap);
|
||||
}
|
||||
|
||||
InfoWindow(@NonNull View view, MapLibreMap maplibreMap) {
|
||||
initialize(view, maplibreMap);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull View view, MapLibreMap maplibreMap) {
|
||||
this.mapboxMap = new WeakReference<>(maplibreMap);
|
||||
isVisible = false;
|
||||
this.view = new WeakReference<>(view);
|
||||
|
||||
view.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
MapLibreMap maplibreMap = InfoWindow.this.mapboxMap.get();
|
||||
if (maplibreMap != null) {
|
||||
MapLibreMap.OnInfoWindowClickListener onInfoWindowClickListener = maplibreMap.getOnInfoWindowClickListener();
|
||||
boolean handledDefaultClick = false;
|
||||
if (onInfoWindowClickListener != null) {
|
||||
handledDefaultClick = onInfoWindowClickListener.onInfoWindowClick(getBoundMarker());
|
||||
}
|
||||
|
||||
if (!handledDefaultClick) {
|
||||
// default behavior: close it when clicking on the tooltip:
|
||||
closeInfoWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
MapLibreMap maplibreMap = InfoWindow.this.mapboxMap.get();
|
||||
if (maplibreMap != null) {
|
||||
MapLibreMap.OnInfoWindowLongClickListener listener = maplibreMap.getOnInfoWindowLongClickListener();
|
||||
if (listener != null) {
|
||||
listener.onInfoWindowLongClick(getBoundMarker());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void closeInfoWindow() {
|
||||
MapLibreMap mapbox = mapboxMap.get();
|
||||
Marker marker = boundMarker.get();
|
||||
if (marker != null && mapbox != null) {
|
||||
mapbox.deselectMarker(marker);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the info window at the specified position.
|
||||
*
|
||||
* @param boundMarker The marker on which is hooked the view.
|
||||
* @param position to place the window on the map.
|
||||
* @param offsetX The offset of the view to the position, in pixels. This allows to offset
|
||||
* the view from the object position.
|
||||
* @param offsetY The offset of the view to the position, in pixels. This allows to offset
|
||||
* the view from the object position.
|
||||
* @return this {@link InfoWindow}.
|
||||
*/
|
||||
@NonNull
|
||||
InfoWindow open(@NonNull MapView mapView, Marker boundMarker, @NonNull LatLng position, int offsetX, int offsetY) {
|
||||
setBoundMarker(boundMarker);
|
||||
|
||||
MapView.LayoutParams lp = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT,
|
||||
MapView.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
MapLibreMap maplibreMap = this.mapboxMap.get();
|
||||
View view = this.view.get();
|
||||
if (view != null && maplibreMap != null) {
|
||||
view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
markerHeightOffset = offsetY;
|
||||
markerWidthOffset = -offsetX;
|
||||
|
||||
// Calculate default Android x,y coordinate
|
||||
coordinates = maplibreMap.getProjection().toScreenLocation(position);
|
||||
float x = coordinates.x - (view.getMeasuredWidth() / 2) + offsetX;
|
||||
float y = coordinates.y - view.getMeasuredHeight() + offsetY;
|
||||
|
||||
if (view instanceof BubbleLayout) {
|
||||
// only apply repositioning/margin for InfoWindowView
|
||||
Resources resources = mapView.getContext().getResources();
|
||||
|
||||
// get right/left popup window
|
||||
float rightSideInfowWindow = x + view.getMeasuredWidth();
|
||||
float leftSideInfoWindow = x;
|
||||
|
||||
// get right/left map view
|
||||
final float mapRight = mapView.getRight();
|
||||
final float mapLeft = mapView.getLeft();
|
||||
|
||||
float marginHorizontal = resources.getDimension(R.dimen.maplibre_infowindow_margin);
|
||||
float tipViewOffset = resources.getDimension(R.dimen.maplibre_infowindow_tipview_width) / 2;
|
||||
float tipViewMarginLeft = view.getMeasuredWidth() / 2 - tipViewOffset;
|
||||
|
||||
boolean outOfBoundsLeft = false;
|
||||
boolean outOfBoundsRight = false;
|
||||
|
||||
// only optimise margins if view is inside current viewport
|
||||
if (coordinates.x >= 0 && coordinates.x <= mapView.getWidth()
|
||||
&& coordinates.y >= 0 && coordinates.y <= mapView.getHeight()) {
|
||||
|
||||
// if out of bounds right
|
||||
if (rightSideInfowWindow > mapRight) {
|
||||
outOfBoundsRight = true;
|
||||
x -= rightSideInfowWindow - mapRight;
|
||||
tipViewMarginLeft += rightSideInfowWindow - mapRight + tipViewOffset;
|
||||
rightSideInfowWindow = x + view.getMeasuredWidth();
|
||||
}
|
||||
|
||||
// fit screen left
|
||||
if (leftSideInfoWindow < mapLeft) {
|
||||
outOfBoundsLeft = true;
|
||||
x += mapLeft - leftSideInfoWindow;
|
||||
tipViewMarginLeft -= mapLeft - leftSideInfoWindow + tipViewOffset;
|
||||
leftSideInfoWindow = x;
|
||||
}
|
||||
|
||||
// Add margin right
|
||||
if (outOfBoundsRight && mapRight - rightSideInfowWindow < marginHorizontal) {
|
||||
x -= marginHorizontal - (mapRight - rightSideInfowWindow);
|
||||
tipViewMarginLeft += marginHorizontal - (mapRight - rightSideInfowWindow) - tipViewOffset;
|
||||
leftSideInfoWindow = x;
|
||||
}
|
||||
|
||||
// Add margin left
|
||||
if (outOfBoundsLeft && leftSideInfoWindow - mapLeft < marginHorizontal) {
|
||||
x += marginHorizontal - (leftSideInfoWindow - mapLeft);
|
||||
tipViewMarginLeft -= (marginHorizontal - (leftSideInfoWindow - mapLeft)) - tipViewOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust tipView
|
||||
((BubbleLayout) view).setArrowPosition(tipViewMarginLeft);
|
||||
}
|
||||
|
||||
// set anchor popupwindowview
|
||||
view.setX(x);
|
||||
view.setY(y);
|
||||
|
||||
// Calculate x-offset and y-offset for update method
|
||||
viewWidthOffset = x - coordinates.x - offsetX;
|
||||
viewHeightOffset = -view.getMeasuredHeight() + offsetY;
|
||||
|
||||
|
||||
close(); // if it was already opened
|
||||
mapView.addView(view, lp);
|
||||
isVisible = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this {@link InfoWindow} if it is visible, otherwise calling this will do nothing.
|
||||
*
|
||||
* @return This {@link InfoWindow}
|
||||
*/
|
||||
@NonNull
|
||||
InfoWindow close() {
|
||||
MapLibreMap maplibreMap = this.mapboxMap.get();
|
||||
if (isVisible && maplibreMap != null) {
|
||||
isVisible = false;
|
||||
View view = this.view.get();
|
||||
if (view != null && view.getParent() != null) {
|
||||
((ViewGroup) view.getParent()).removeView(view);
|
||||
}
|
||||
|
||||
Marker marker = getBoundMarker();
|
||||
MapLibreMap.OnInfoWindowCloseListener listener = maplibreMap.getOnInfoWindowCloseListener();
|
||||
if (listener != null) {
|
||||
listener.onInfoWindowClose(marker);
|
||||
}
|
||||
|
||||
setBoundMarker(null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the view that is displayed when the InfoWindow opens. This retrieves data from
|
||||
* overlayItem and shows it in the tooltip.
|
||||
*
|
||||
* @param overlayItem the tapped overlay item
|
||||
*/
|
||||
void adaptDefaultMarker(@NonNull Marker overlayItem, MapLibreMap maplibreMap, @NonNull MapView mapView) {
|
||||
View view = this.view.get();
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(mapView.getContext()).inflate(layoutRes, mapView, false);
|
||||
initialize(view, maplibreMap);
|
||||
}
|
||||
this.mapboxMap = new WeakReference<>(maplibreMap);
|
||||
String title = overlayItem.getTitle();
|
||||
TextView titleTextView = ((TextView) view.findViewById(R.id.infowindow_title));
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
titleTextView.setText(title);
|
||||
titleTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
titleTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String snippet = overlayItem.getSnippet();
|
||||
TextView snippetTextView = ((TextView) view.findViewById(R.id.infowindow_description));
|
||||
if (!TextUtils.isEmpty(snippet)) {
|
||||
snippetTextView.setText(snippet);
|
||||
snippetTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
snippetTextView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
InfoWindow setBoundMarker(Marker boundMarker) {
|
||||
this.boundMarker = new WeakReference<>(boundMarker);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Marker getBoundMarker() {
|
||||
if (boundMarker == null) {
|
||||
return null;
|
||||
}
|
||||
return boundMarker.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will result in getting this {@link InfoWindow} and updating the view being displayed.
|
||||
*/
|
||||
public void update() {
|
||||
MapLibreMap maplibreMap = this.mapboxMap.get();
|
||||
Marker marker = boundMarker.get();
|
||||
View view = this.view.get();
|
||||
if (maplibreMap != null && marker != null && view != null) {
|
||||
coordinates = maplibreMap.getProjection().toScreenLocation(marker.getPosition());
|
||||
|
||||
if (view instanceof BubbleLayout) {
|
||||
view.setX(coordinates.x + viewWidthOffset - markerWidthOffset);
|
||||
} else {
|
||||
view.setX(coordinates.x - (view.getMeasuredWidth() / 2) - markerWidthOffset);
|
||||
}
|
||||
view.setY(coordinates.y + viewHeightOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void onContentUpdate() {
|
||||
//recalculate y-offset and update position
|
||||
View view = this.view.get();
|
||||
if (view != null) {
|
||||
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
|
||||
if (viewTreeObserver.isAlive()) {
|
||||
viewTreeObserver.addOnGlobalLayoutListener(contentUpdateListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private final ViewTreeObserver.OnGlobalLayoutListener contentUpdateListener =
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
View view = InfoWindow.this.view.get();
|
||||
if (view != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
viewHeightOffset = -view.getMeasuredHeight() + markerHeightOffset;
|
||||
update();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve this {@link InfoWindow}'s current view being used.
|
||||
*
|
||||
* @return This {@link InfoWindow}'s current View.
|
||||
*/
|
||||
@Nullable
|
||||
public View getView() {
|
||||
return view != null ? view.get() : null;
|
||||
}
|
||||
|
||||
boolean isVisible() {
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.R;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.maps.MapView;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
/**
|
||||
* Marker is an annotation that shows an icon image at a geographical location. The default marker
|
||||
* uses a provided icon. This icon can be customized using {@link IconFactory} to generate an
|
||||
* {@link Icon} using a provided image. Markers are added to the map by first giving a
|
||||
* {@link LatLng} and using {@link MapLibreMap#addMarker(MarkerOptions)}. The marker icon will be
|
||||
* centered at this position so it is common to add padding to the icon image before usage.
|
||||
* <p>
|
||||
* Markers are designed to be interactive. They receive click events by default, and are often used
|
||||
* with event listeners to bring up info windows. An {@link InfoWindow} is displayed by default when
|
||||
* either a title or snippet is provided.
|
||||
* </p>
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public class Marker extends Annotation {
|
||||
|
||||
@Keep
|
||||
private LatLng position;
|
||||
private String snippet;
|
||||
@Nullable
|
||||
private Icon icon;
|
||||
//Redundantly stored for JNI access
|
||||
@Nullable
|
||||
@Keep
|
||||
private String iconId;
|
||||
private String title;
|
||||
|
||||
@Nullable
|
||||
private InfoWindow infoWindow;
|
||||
private boolean infoWindowShown;
|
||||
|
||||
private int topOffsetPixels;
|
||||
private int rightOffsetPixels;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
Marker() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a instance of {@link Marker} using the builder of Marker.
|
||||
*
|
||||
* @param baseMarkerOptions The builder used to construct the Marker.
|
||||
*/
|
||||
public Marker(BaseMarkerOptions baseMarkerOptions) {
|
||||
this(baseMarkerOptions.position, baseMarkerOptions.icon, baseMarkerOptions.title, baseMarkerOptions.snippet);
|
||||
}
|
||||
|
||||
Marker(LatLng position, Icon icon, String title, String snippet) {
|
||||
this.position = position;
|
||||
this.title = title;
|
||||
this.snippet = snippet;
|
||||
setIcon(icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the marker.
|
||||
*
|
||||
* @return A {@link LatLng} object specifying the marker's current position.
|
||||
*/
|
||||
public LatLng getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet of the marker.
|
||||
*
|
||||
* @return A string containing the marker's snippet.
|
||||
*/
|
||||
public String getSnippet() {
|
||||
return snippet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet of the marker.
|
||||
*
|
||||
* @return A string containing the marker's snippet.
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*/
|
||||
public void hideInfoWindow() {
|
||||
if (infoWindow != null) {
|
||||
infoWindow.close();
|
||||
}
|
||||
infoWindowShown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @return true if the infoWindow is shown
|
||||
*/
|
||||
public boolean isInfoWindowShown() {
|
||||
return infoWindowShown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location of the marker.
|
||||
*
|
||||
* @param position A {@link LatLng} defining the marker position.
|
||||
*/
|
||||
public void setPosition(LatLng position) {
|
||||
this.position = position;
|
||||
MapLibreMap map = getMapboxMap();
|
||||
if (map != null) {
|
||||
map.updateMarker(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the snippet of the marker.
|
||||
*
|
||||
* @param snippet A String used in the marker info window. If {@code null}, the snippet is
|
||||
* cleared.
|
||||
*/
|
||||
public void setSnippet(String snippet) {
|
||||
this.snippet = snippet;
|
||||
refreshInfoWindowContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon of the marker.
|
||||
*
|
||||
* @param icon The {@link Icon} to be used as Marker image
|
||||
*/
|
||||
public void setIcon(@Nullable Icon icon) {
|
||||
this.icon = icon;
|
||||
this.iconId = icon != null ? icon.getId() : null;
|
||||
MapLibreMap map = getMapboxMap();
|
||||
if (map != null) {
|
||||
map.updateMarker(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Icon} currently used for the marker. If no Icon was set for the marker, the
|
||||
* default icon will be returned.
|
||||
*
|
||||
* @return The {@link Icon} the marker is using.
|
||||
*/
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of the marker.
|
||||
*
|
||||
* @param title A String used in the marker info window. If {@code null}, the title is
|
||||
* cleared.
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
refreshInfoWindowContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link InfoWindow} the marker is using. If the marker hasn't had an info window
|
||||
* defined, this will return {@code null}.
|
||||
*
|
||||
* @return The info window the marker is using.
|
||||
*/
|
||||
@Nullable
|
||||
public InfoWindow getInfoWindow() {
|
||||
return infoWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update only for default Marker's InfoWindow content for Title and Snippet
|
||||
*/
|
||||
private void refreshInfoWindowContent() {
|
||||
if (isInfoWindowShown() && mapView != null && maplibreMap != null && maplibreMap.getInfoWindowAdapter() == null) {
|
||||
InfoWindow infoWindow = getInfoWindow(mapView);
|
||||
if (mapView.getContext() != null) {
|
||||
infoWindow.adaptDefaultMarker(this, maplibreMap, mapView);
|
||||
}
|
||||
MapLibreMap map = getMapboxMap();
|
||||
if (map != null) {
|
||||
map.updateMarker(this);
|
||||
}
|
||||
infoWindow.onContentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK. Use {@link MapLibreMap#selectMarker(Marker)}
|
||||
* if you want to programmatically display the markers info window.
|
||||
*
|
||||
* @param maplibreMap The hosting mapbox map.
|
||||
* @param mapView The hosting map view.
|
||||
* @return The info window that was shown.
|
||||
*/
|
||||
@Nullable
|
||||
public InfoWindow showInfoWindow(@NonNull MapLibreMap maplibreMap, @NonNull MapView mapView) {
|
||||
setMapboxMap(maplibreMap);
|
||||
setMapView(mapView);
|
||||
MapLibreMap.InfoWindowAdapter infoWindowAdapter = getMapboxMap().getInfoWindowAdapter();
|
||||
if (infoWindowAdapter != null) {
|
||||
// end developer is using a custom InfoWindowAdapter
|
||||
View content = infoWindowAdapter.getInfoWindow(this);
|
||||
if (content != null) {
|
||||
infoWindow = new InfoWindow(content, maplibreMap);
|
||||
showInfoWindow(infoWindow, mapView);
|
||||
return infoWindow;
|
||||
}
|
||||
}
|
||||
|
||||
InfoWindow infoWindow = getInfoWindow(mapView);
|
||||
if (mapView.getContext() != null) {
|
||||
infoWindow.adaptDefaultMarker(this, maplibreMap, mapView);
|
||||
}
|
||||
return showInfoWindow(infoWindow, mapView);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private InfoWindow showInfoWindow(InfoWindow iw, MapView mapView) {
|
||||
iw.open(mapView, this, getPosition(), rightOffsetPixels, topOffsetPixels);
|
||||
infoWindowShown = true;
|
||||
return iw;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private InfoWindow getInfoWindow(@NonNull MapView mapView) {
|
||||
if (infoWindow == null && mapView.getContext() != null) {
|
||||
infoWindow = new InfoWindow(mapView, R.layout.maplibre_infowindow_content, getMapboxMap());
|
||||
}
|
||||
return infoWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @param topOffsetPixels the top offset pixels.
|
||||
*/
|
||||
public void setTopOffsetPixels(int topOffsetPixels) {
|
||||
this.topOffsetPixels = topOffsetPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method, used internally by the SDK.
|
||||
*
|
||||
* @param rightOffsetPixels the right offset pixels.
|
||||
*/
|
||||
public void setRightOffsetPixels(int rightOffsetPixels) {
|
||||
this.rightOffsetPixels = rightOffsetPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a String with the marker position.
|
||||
*
|
||||
* @return A String with the marker position.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Marker [position[" + getPosition() + "]]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.exceptions.InvalidMarkerPositionException;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Builder for composing {@link Marker} objects. See {@link Marker} for additional information.
|
||||
* </p>
|
||||
* <h3>Example</h3>
|
||||
* <pre>
|
||||
* mapView.addMarker(new MarkerOptions()
|
||||
* .title("Intersection")
|
||||
* .snippet("H St NW with 15th St NW")
|
||||
* .position(new LatLng(38.9002073, -77.03364419)));
|
||||
* </pre>
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class MarkerOptions extends BaseMarkerOptions<Marker, MarkerOptions> implements Parcelable {
|
||||
|
||||
/**
|
||||
* Defines options for a Marker.
|
||||
*/
|
||||
public MarkerOptions() {
|
||||
}
|
||||
|
||||
protected MarkerOptions(Parcel in) {
|
||||
position((LatLng) in.readParcelable(LatLng.class.getClassLoader()));
|
||||
snippet(in.readString());
|
||||
title(in.readString());
|
||||
if (in.readByte() != 0) {
|
||||
// this means we have an icon
|
||||
String iconId = in.readString();
|
||||
Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader());
|
||||
Icon icon = new Icon(iconId, iconBitmap);
|
||||
icon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkerOptions getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable's
|
||||
* marshalled representation.
|
||||
*
|
||||
* @return integer 0.
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written. May be 0 or
|
||||
* {@link #PARCELABLE_WRITE_RETURN_VALUE}.
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeParcelable(getPosition(), flags);
|
||||
out.writeString(getSnippet());
|
||||
out.writeString(getTitle());
|
||||
Icon icon = getIcon();
|
||||
out.writeByte((byte) (icon != null ? 1 : 0));
|
||||
if (icon != null) {
|
||||
out.writeString(getIcon().getId());
|
||||
out.writeParcelable(getIcon().getBitmap(), flags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method. Used internally by the SDK.
|
||||
*
|
||||
* @return Marker The build marker
|
||||
*/
|
||||
public Marker getMarker() {
|
||||
if (position == null) {
|
||||
throw new InvalidMarkerPositionException();
|
||||
}
|
||||
|
||||
return new Marker(position, icon, title, snippet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position set for this {@link MarkerOptions} object.
|
||||
*
|
||||
* @return A {@link LatLng} object specifying the marker's current position.
|
||||
*/
|
||||
public LatLng getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet set for this {@link MarkerOptions} object.
|
||||
*
|
||||
* @return A string containing the marker's snippet.
|
||||
*/
|
||||
public String getSnippet() {
|
||||
return snippet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title set for this {@link MarkerOptions} object.
|
||||
*
|
||||
* @return A string containing the marker's title.
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom icon set for this {@link MarkerOptions} object.
|
||||
*
|
||||
* @return A {@link Icon} object that the marker is using. If the icon wasn't set, default icon
|
||||
* will return.
|
||||
*/
|
||||
public Icon getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkerOptions> CREATOR =
|
||||
new Parcelable.Creator<MarkerOptions>() {
|
||||
public MarkerOptions createFromParcel(@NonNull Parcel in) {
|
||||
return new MarkerOptions(in);
|
||||
}
|
||||
|
||||
public MarkerOptions[] newArray(int size) {
|
||||
return new MarkerOptions[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares this {@link MarkerOptions} object with another {@link MarkerOptions} and
|
||||
* determines if their properties match.
|
||||
*
|
||||
* @param o Another {@link MarkerOptions} to compare with this object.
|
||||
* @return True if marker properties match this {@link MarkerOptions} object.
|
||||
* Else, false.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MarkerOptions marker = (MarkerOptions) o;
|
||||
|
||||
if (getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null) {
|
||||
return false;
|
||||
}
|
||||
if (getSnippet() != null ? !getSnippet().equals(marker.getSnippet()) : marker.getSnippet() != null) {
|
||||
return false;
|
||||
}
|
||||
if (getIcon() != null ? !getIcon().equals(marker.getIcon()) : marker.getIcon() != null) {
|
||||
return false;
|
||||
}
|
||||
return !(getTitle() != null ? !getTitle().equals(marker.getTitle()) : marker.getTitle() != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 1;
|
||||
result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0);
|
||||
result = 31 * result + (getSnippet() != null ? getSnippet().hashCode() : 0);
|
||||
result = 31 * result + (getIcon() != null ? getIcon().hashCode() : 0);
|
||||
result = 31 * result + (getTitle() != null ? getTitle().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Polygon is a geometry annotation that's a closed loop of coordinates.
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class Polygon extends BasePointCollection {
|
||||
|
||||
@Keep
|
||||
private int fillColor = Color.BLACK; // default fillColor is black
|
||||
@Keep
|
||||
private int strokeColor = Color.BLACK; // default strokeColor is black
|
||||
@Keep
|
||||
private List<List<LatLng>> holes;
|
||||
|
||||
Polygon() {
|
||||
super();
|
||||
holes = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color of the fill region of the polygon.
|
||||
*
|
||||
* @return The color of the fill.
|
||||
*/
|
||||
public int getFillColor() {
|
||||
return fillColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color of the stroke of the polygon.
|
||||
*
|
||||
* @return The color of the stroke.
|
||||
*/
|
||||
public int getStrokeColor() {
|
||||
return strokeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the holes.
|
||||
*
|
||||
* @return A {@link List} of {@link List} of {@link LatLng} points making up the holes.
|
||||
*/
|
||||
@NonNull
|
||||
public List<List<LatLng>> getHoles() {
|
||||
return new ArrayList<>(holes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of the fill region of the polygon.
|
||||
*
|
||||
* @param color The color in ARGB format.
|
||||
*/
|
||||
public void setFillColor(int color) {
|
||||
fillColor = color;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of the stroke of the polygon.
|
||||
*
|
||||
* @param color The color in ARGB format.
|
||||
*/
|
||||
public void setStrokeColor(int color) {
|
||||
strokeColor = color;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the holes of this polygon. This method will take a copy of the holes, so further
|
||||
* mutations to holes will have no effect on this polygon.
|
||||
*
|
||||
* @param holes A {@link List} of {@link List} of {@link LatLng} points making up the holes.
|
||||
*/
|
||||
public void setHoles(@NonNull List<? extends List<LatLng>> holes) {
|
||||
this.holes = new ArrayList<>(holes);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hole to the polygon.
|
||||
*
|
||||
* @param hole A {@link List} of {@link List} of {@link LatLng} points making up the hole to be added.
|
||||
*/
|
||||
void addHole(List<LatLng> hole) {
|
||||
holes.add(hole);
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
void update() {
|
||||
MapLibreMap maplibreMap = getMapboxMap();
|
||||
if (maplibreMap != null) {
|
||||
maplibreMap.updatePolygon(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builder for composing {@link Polygon} objects.
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class PolygonOptions implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<PolygonOptions> CREATOR =
|
||||
new Parcelable.Creator<PolygonOptions>() {
|
||||
public PolygonOptions createFromParcel(@NonNull Parcel in) {
|
||||
return new PolygonOptions(in);
|
||||
}
|
||||
|
||||
public PolygonOptions[] newArray(int size) {
|
||||
return new PolygonOptions[size];
|
||||
}
|
||||
};
|
||||
|
||||
private PolygonOptions(Parcel in) {
|
||||
polygon = new Polygon();
|
||||
List<LatLng> pointsList = new ArrayList<>();
|
||||
in.readList(pointsList, LatLng.class.getClassLoader());
|
||||
addAll(pointsList);
|
||||
List<List<LatLng>> holes = new ArrayList<>();
|
||||
in.readList(holes, LatLng.class.getClassLoader());
|
||||
addAllHoles(holes);
|
||||
alpha(in.readFloat());
|
||||
fillColor(in.readInt());
|
||||
strokeColor(in.readInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable's
|
||||
* marshalled representation.
|
||||
*
|
||||
* @return integer 0.
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written. May be 0 or
|
||||
* {@link #PARCELABLE_WRITE_RETURN_VALUE}.
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeList(getPoints());
|
||||
out.writeList(getHoles());
|
||||
out.writeFloat(getAlpha());
|
||||
out.writeInt(getFillColor());
|
||||
out.writeInt(getStrokeColor());
|
||||
}
|
||||
|
||||
private Polygon polygon;
|
||||
|
||||
/**
|
||||
* Defines options for a polygon.
|
||||
*/
|
||||
public PolygonOptions() {
|
||||
polygon = new Polygon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a vertex to the outline of the polygon being built.
|
||||
*
|
||||
* @param point {@link LatLng} point to be added to polygon geometry.
|
||||
* @return This {@link PolygonOptions} object with the given point added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions add(LatLng point) {
|
||||
polygon.addPoint(point);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds vertices to the outline of the polygon being built.
|
||||
*
|
||||
* @param points {@link LatLng} points to be added to polygon geometry.
|
||||
* @return This {@link PolygonOptions} object with the given points added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions add(LatLng... points) {
|
||||
for (LatLng point : points) {
|
||||
add(point);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds vertices to the outline of the polygon being built.
|
||||
*
|
||||
* @param points {@link Iterable} list made up of {@link LatLng} points defining the polygon
|
||||
* geometry
|
||||
* @return This {@link PolygonOptions} object with the given points added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions addAll(Iterable<LatLng> points) {
|
||||
for (LatLng point : points) {
|
||||
add(point);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a hole to the outline of the polygon being built.
|
||||
*
|
||||
* @param hole {@link List} list made up of {@link LatLng} points defining the hole
|
||||
* @return This {@link PolygonOptions} object with the given hole added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions addHole(List<LatLng> hole) {
|
||||
polygon.addHole(hole);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds holes to the outline of the polygon being built.
|
||||
*
|
||||
* @param holes {@link List} list made up of {@link LatLng} holes to be added to polygon geometry
|
||||
* @return This {@link PolygonOptions} object with the given holes added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions addHole(List<LatLng>... holes) {
|
||||
for (List<LatLng> hole : holes) {
|
||||
addHole(hole);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds holes to the outline of the polygon being built.
|
||||
*
|
||||
* @param holes {@link Iterable} list made up of {@link List} list of {@link LatLng} holes defining the hole geometry
|
||||
* @return This {@link PolygonOptions} object with the given holes added to the outline.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions addAllHoles(Iterable<List<LatLng>> holes) {
|
||||
for (List<LatLng> hole : holes) {
|
||||
addHole(hole);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the alpha value of the polyline.
|
||||
*
|
||||
* @param alpha float value between 0 (not visible) and 1.
|
||||
* @return This {@link PolygonOptions} object with the given polygon alpha value.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions alpha(float alpha) {
|
||||
polygon.setAlpha(alpha);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the alpha set for this {@link PolygonOptions} object.
|
||||
*
|
||||
* @return float value between 0 and 1 defining the alpha.
|
||||
*/
|
||||
public float getAlpha() {
|
||||
return polygon.getAlpha();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the polygon's fill color, as 32-bit ARGB. The default color is black.
|
||||
*
|
||||
* @param color 32-bit ARGB color.
|
||||
* @return This {@link PolylineOptions} object with a new color set.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions fillColor(int color) {
|
||||
polygon.setFillColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fill color set for this {@link PolygonOptions} object.
|
||||
*
|
||||
* @return The fill color of the polygon in ARGB format.
|
||||
*/
|
||||
public int getFillColor() {
|
||||
return polygon.getFillColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method. Used internally by the SDK.
|
||||
*
|
||||
* @return Polygon the Polygon to return
|
||||
*/
|
||||
public Polygon getPolygon() {
|
||||
return polygon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the polygon's stroke color, as 32-bit ARGB. The default color is black.
|
||||
*
|
||||
* @param color 32-bit ARGB color.
|
||||
* @return This {@link PolygonOptions} object with a new stroke color set.
|
||||
*/
|
||||
@NonNull
|
||||
public PolygonOptions strokeColor(int color) {
|
||||
polygon.setStrokeColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stroke color set for this {@link PolygonOptions} object.
|
||||
*
|
||||
* @return The stroke color of the polygon in ARGB format.
|
||||
*/
|
||||
public int getStrokeColor() {
|
||||
return polygon.getStrokeColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the points set for this {@link PolygonOptions} object.
|
||||
*
|
||||
* @return The list made up of {@link LatLng} points defining the polygon.
|
||||
*/
|
||||
@NonNull
|
||||
public List<LatLng> getPoints() {
|
||||
// the getter gives us a copy, which is the safe thing to do...
|
||||
return polygon.getPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the holes set for this {@link PolygonOptions} object.
|
||||
*
|
||||
* @return The list made up of {@link List} of {@link List} of {@link LatLng} points defining the holes.
|
||||
*/
|
||||
@NonNull
|
||||
public List<List<LatLng>> getHoles() {
|
||||
return polygon.getHoles();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares this {@link PolygonOptions} object with another {@link PolygonOptions} and
|
||||
* determines if their color, alpha, stroke color, and vertices match.
|
||||
*
|
||||
* @param o Another {@link PolygonOptions} to compare with this object.
|
||||
* @return True if color, alpha, stroke color, vertices and holes match this {@link PolygonOptions}
|
||||
* {@link PolygonOptions} object. Else, false.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PolygonOptions polygon = (PolygonOptions) o;
|
||||
|
||||
if (Float.compare(polygon.getAlpha(), getAlpha()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getFillColor() != polygon.getFillColor()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getStrokeColor() != polygon.getStrokeColor()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getPoints().equals(polygon.getPoints())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getHoles().equals(polygon.getHoles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 1;
|
||||
result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0);
|
||||
result = 31 * result + getFillColor();
|
||||
result = 31 * result + getStrokeColor();
|
||||
result = 31 * result + getPoints().hashCode();
|
||||
result = 31 * result + getHoles().hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
/**
|
||||
* Polyline is a geometry feature with an unclosed list of coordinates drawn as a line
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class Polyline extends BasePointCollection {
|
||||
|
||||
@Keep
|
||||
private int color = Color.BLACK; // default color is black
|
||||
@Keep
|
||||
private float width = 10; // As specified by Google API Docs (in pixels)
|
||||
|
||||
Polyline() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color of this polyline.
|
||||
*
|
||||
* @return The color in ARGB format.
|
||||
*/
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of this polyline.
|
||||
*
|
||||
* @return The width in screen pixels.
|
||||
*/
|
||||
public float getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of the polyline.
|
||||
*
|
||||
* @param color - the color in ARGB format
|
||||
*/
|
||||
public void setColor(int color) {
|
||||
this.color = color;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the polyline.
|
||||
*
|
||||
* @param width in pixels
|
||||
*/
|
||||
public void setWidth(float width) {
|
||||
this.width = width;
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
void update() {
|
||||
MapLibreMap maplibreMap = getMapboxMap();
|
||||
if (maplibreMap != null) {
|
||||
maplibreMap.updatePolyline(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package org.maplibre.android.annotations;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builder for composing {@link Polyline} objects.
|
||||
* @deprecated As of 7.0.0,
|
||||
* use <a href="https://github.com/mapbox/mapbox-plugins-android/tree/master/plugin-annotation">
|
||||
* MapLibre Annotation Plugin</a> instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class PolylineOptions implements Parcelable {
|
||||
|
||||
|
||||
public static final Parcelable.Creator<PolylineOptions> CREATOR =
|
||||
new Parcelable.Creator<PolylineOptions>() {
|
||||
public PolylineOptions createFromParcel(@NonNull Parcel in) {
|
||||
return new PolylineOptions(in);
|
||||
}
|
||||
|
||||
public PolylineOptions[] newArray(int size) {
|
||||
return new PolylineOptions[size];
|
||||
}
|
||||
};
|
||||
|
||||
private PolylineOptions(Parcel in) {
|
||||
polyline = new Polyline();
|
||||
ArrayList<LatLng> pointsList = new ArrayList<>();
|
||||
in.readList(pointsList, LatLng.class.getClassLoader());
|
||||
addAll(pointsList);
|
||||
alpha(in.readFloat());
|
||||
color(in.readInt());
|
||||
width(in.readFloat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable's
|
||||
* marshalled representation.
|
||||
*
|
||||
* @return integer 0.
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written. May be 0 or
|
||||
* {@link #PARCELABLE_WRITE_RETURN_VALUE}.
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeList(getPoints());
|
||||
out.writeFloat(getAlpha());
|
||||
out.writeInt(getColor());
|
||||
out.writeFloat(getWidth());
|
||||
}
|
||||
|
||||
private Polyline polyline;
|
||||
|
||||
/**
|
||||
* Defines options for a polyline.
|
||||
*/
|
||||
public PolylineOptions() {
|
||||
polyline = new Polyline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a vertex to the end of the polyline being built.
|
||||
*
|
||||
* @param point {@link LatLng} point to be added to polyline geometry.
|
||||
* @return This {@link PolylineOptions} object with the given point on the end.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions add(LatLng point) {
|
||||
polyline.addPoint(point);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds vertices to the end of the polyline being built.
|
||||
*
|
||||
* @param points {@link LatLng} points defining the polyline geometry.
|
||||
* @return This {@link PolylineOptions} object with the given point on the end.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions add(LatLng... points) {
|
||||
for (LatLng point : points) {
|
||||
add(point);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds vertices to the end of the polyline being built.
|
||||
*
|
||||
* @param points {@link Iterable} list made up of {@link LatLng} points defining the polyline
|
||||
* geometry
|
||||
* @return This {@link PolylineOptions} object with the given points on the end.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions addAll(Iterable<LatLng> points) {
|
||||
for (LatLng point : points) {
|
||||
add(point);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the alpha value of the polyline.
|
||||
*
|
||||
* @param alpha float value between 0 (not visible) and 1.
|
||||
* @return This {@link PolylineOptions} object with the given polyline alpha value.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions alpha(float alpha) {
|
||||
polyline.setAlpha(alpha);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the alpha set for this {@link PolylineOptions} object.
|
||||
*
|
||||
* @return float value between 0 and 1 defining the alpha.
|
||||
*/
|
||||
public float getAlpha() {
|
||||
return polyline.getAlpha();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of the polyline as a 32-bit ARGB color. The default color is black.
|
||||
*
|
||||
* @param color 32-bit ARGB color.
|
||||
* @return This {@link PolylineOptions} object with a new color set.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions color(int color) {
|
||||
polyline.setColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color set for this {@link PolylineOptions} object.
|
||||
*
|
||||
* @return The color of the polyline in ARGB format.
|
||||
*/
|
||||
public int getColor() {
|
||||
return polyline.getColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use this method. Used internally by the SDK.
|
||||
*
|
||||
* @return PolyLine The polyline build by this class.
|
||||
*/
|
||||
public Polyline getPolyline() {
|
||||
return polyline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width set for this {@link PolylineOptions} object.
|
||||
*
|
||||
* @return The width of the polyline in screen pixels.
|
||||
*/
|
||||
public float getWidth() {
|
||||
return polyline.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the polyline in screen pixels. The default is 10.
|
||||
*
|
||||
* @param width float value defining width of polyline using unit pixels.
|
||||
* @return This {@link PolylineOptions} object with a new width set.
|
||||
*/
|
||||
@NonNull
|
||||
public PolylineOptions width(float width) {
|
||||
polyline.setWidth(width);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the points set for this {@link PolylineOptions} object.
|
||||
*
|
||||
* @return a {@link List} of {@link LatLng}s specifying the vertices of the polyline.
|
||||
*/
|
||||
public List<LatLng> getPoints() {
|
||||
// the getter gives us a copy, which is the safe thing to do...
|
||||
return polyline.getPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this {@link PolylineOptions} object with another {@link PolylineOptions} and
|
||||
* determines if their color, alpha, width, and vertices match.
|
||||
*
|
||||
* @param o Another {@link PolylineOptions} to compare with this object.
|
||||
* @return True if color, alpha, width, and vertices match this {@link PolylineOptions} object.
|
||||
* Else, false.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PolylineOptions polyline = (PolylineOptions) o;
|
||||
|
||||
if (Float.compare(polyline.getAlpha(), getAlpha()) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (getColor() != polyline.getColor()) {
|
||||
return false;
|
||||
}
|
||||
if (Float.compare(polyline.getWidth(), getWidth()) != 0) {
|
||||
return false;
|
||||
}
|
||||
return !(getPoints() != null ? !getPoints().equals(polyline.getPoints()) : polyline.getPoints() != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 1;
|
||||
result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0);
|
||||
result = 31 * result + getColor();
|
||||
result = 31 * result + (getWidth() != +0.0f ? Float.floatToIntBits(getWidth()) : 0);
|
||||
result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Annotation API classes.
|
||||
*/
|
||||
package org.maplibre.android.annotations;
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.maplibre.android.attribution;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Attribution {
|
||||
|
||||
private static final String OPENSTREETMAP = "OpenStreetMap";
|
||||
private static final String OPENSTREETMAP_ABBR = "OSM";
|
||||
static final String MAPBOX_URL = "https://www.mapbox.com/about/maps/";
|
||||
static final List<String> IMPROVE_MAP_URLS = new ArrayList<>();
|
||||
|
||||
static {
|
||||
// Using a List makes URL backwards compatible
|
||||
IMPROVE_MAP_URLS.add("https://www.mapbox.com/feedback/");
|
||||
IMPROVE_MAP_URLS.add("https://www.mapbox.com/map-feedback/");
|
||||
IMPROVE_MAP_URLS.add("https://apps.mapbox.com/feedback/");
|
||||
}
|
||||
|
||||
private String title;
|
||||
private String url;
|
||||
|
||||
Attribution(String title, String url) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitleAbbreviated() {
|
||||
if (title.equals(OPENSTREETMAP)) {
|
||||
return OPENSTREETMAP_ABBR;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Attribution that = (Attribution) o;
|
||||
|
||||
if (title != null ? !title.equals(that.title) : that.title != null) {
|
||||
return false;
|
||||
}
|
||||
return url != null ? url.equals(that.url) : that.url == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = title != null ? title.hashCode() : 0;
|
||||
result = 31 * result + (url != null ? url.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.maplibre.android.attribution;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AttributionLayout {
|
||||
|
||||
@Nullable
|
||||
private Bitmap logo;
|
||||
@Nullable
|
||||
private PointF anchorPoint;
|
||||
private boolean shortText;
|
||||
|
||||
public AttributionLayout(@Nullable Bitmap logo, @Nullable PointF anchorPoint, boolean shortText) {
|
||||
this.logo = logo;
|
||||
this.anchorPoint = anchorPoint;
|
||||
this.shortText = shortText;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Bitmap getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PointF getAnchorPoint() {
|
||||
return anchorPoint;
|
||||
}
|
||||
|
||||
public boolean isShortText() {
|
||||
return shortText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AttributionLayout that = (AttributionLayout) o;
|
||||
|
||||
if (logo != null ? !logo.equals(that.logo) : that.logo != null) {
|
||||
return false;
|
||||
}
|
||||
return anchorPoint != null ? anchorPoint.equals(that.anchorPoint) : that.anchorPoint == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = logo != null ? logo.hashCode() : 0;
|
||||
result = 31 * result + (anchorPoint != null ? anchorPoint.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AttributionLayout{"
|
||||
+ "logo=" + logo
|
||||
+ ", anchorPoint=" + anchorPoint
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.maplibre.android.attribution;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AttributionMeasure {
|
||||
|
||||
private Bitmap logo;
|
||||
private Bitmap logoSmall;
|
||||
private Bitmap snapshot;
|
||||
private TextView textView;
|
||||
private TextView textViewShort;
|
||||
private float margin;
|
||||
|
||||
private boolean shorterText;
|
||||
|
||||
AttributionMeasure(Bitmap snapshot, Bitmap logo, Bitmap logoSmall, TextView tv, TextView tvShort, float margin) {
|
||||
this.snapshot = snapshot;
|
||||
this.logo = logo;
|
||||
this.logoSmall = logoSmall;
|
||||
this.textView = tv;
|
||||
this.textViewShort = tvShort;
|
||||
this.margin = margin;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AttributionLayout measure() {
|
||||
Chain chain = new Chain(
|
||||
new FullLogoLongTextCommand(),
|
||||
new FullLogoShortTextCommand(),
|
||||
new SmallLogoLongTextCommand(),
|
||||
new SmallLogoShortTextCommand(),
|
||||
new LongTextCommand(),
|
||||
new ShortTextCommand(),
|
||||
new NoTextCommand()
|
||||
);
|
||||
|
||||
AttributionLayout attributionLayout = chain.start(this);
|
||||
shorterText = attributionLayout.isShortText();
|
||||
return attributionLayout;
|
||||
}
|
||||
|
||||
|
||||
private static class FullLogoLongTextCommand implements Command {
|
||||
@Nullable
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin);
|
||||
return new AttributionLayout(measure.logo, anchor, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FullLogoShortTextCommand implements Command {
|
||||
@Nullable
|
||||
@Override
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin);
|
||||
return new AttributionLayout(measure.logo, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SmallLogoLongTextCommand implements Command {
|
||||
@Nullable
|
||||
@Override
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getLogoSmallContainerWidth() + measure.getTextViewContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin);
|
||||
return new AttributionLayout(measure.logoSmall, anchor, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SmallLogoShortTextCommand implements Command {
|
||||
@Nullable
|
||||
@Override
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin);
|
||||
return new AttributionLayout(measure.logoSmall, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LongTextCommand implements Command {
|
||||
@Nullable
|
||||
@Override
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getTextViewContainerWidth() + measure.margin;
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
return new AttributionLayout(null, calculateAnchor(measure.snapshot, measure.textView, measure.margin), false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ShortTextCommand implements Command {
|
||||
@Nullable
|
||||
@Override
|
||||
public AttributionLayout execute(@NonNull AttributionMeasure measure) {
|
||||
float width = measure.getTextViewShortContainerWidth() + measure.margin;
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin);
|
||||
return new AttributionLayout(null, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoTextCommand implements Command {
|
||||
@NonNull
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
return new AttributionLayout(null, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static PointF calculateAnchor(Bitmap snapshot, TextView textView, float margin) {
|
||||
return new PointF(
|
||||
snapshot.getWidth() - textView.getMeasuredWidth() - margin,
|
||||
snapshot.getHeight() - margin - textView.getMeasuredHeight()
|
||||
);
|
||||
}
|
||||
|
||||
public TextView getTextView() {
|
||||
return shorterText ? textViewShort : textView;
|
||||
}
|
||||
|
||||
private class Chain {
|
||||
public List<Command> commands;
|
||||
|
||||
Chain(Command... commands) {
|
||||
this.commands = Arrays.asList(commands);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AttributionLayout start(AttributionMeasure measure) {
|
||||
AttributionLayout attributionLayout = null;
|
||||
for (Command command : commands) {
|
||||
attributionLayout = command.execute(measure);
|
||||
if (attributionLayout != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return attributionLayout;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Command {
|
||||
@Nullable
|
||||
AttributionLayout execute(AttributionMeasure measure);
|
||||
}
|
||||
|
||||
private float getTextViewContainerWidth() {
|
||||
return textView.getMeasuredWidth() + margin;
|
||||
}
|
||||
|
||||
private float getLogoContainerWidth() {
|
||||
return logo.getWidth() + (2 * margin);
|
||||
}
|
||||
|
||||
private float getTextViewShortContainerWidth() {
|
||||
return textViewShort.getMeasuredWidth() + margin;
|
||||
}
|
||||
|
||||
private float getLogoSmallContainerWidth() {
|
||||
return logoSmall.getWidth() + (2 * margin);
|
||||
}
|
||||
|
||||
private float getMaxSize() {
|
||||
return snapshot.getWidth() * 8 / 10;
|
||||
}
|
||||
|
||||
private float getMaxSizeShort() {
|
||||
return snapshot.getWidth();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Bitmap snapshot;
|
||||
private Bitmap logo;
|
||||
private Bitmap logoSmall;
|
||||
private TextView textView;
|
||||
private TextView textViewShort;
|
||||
private float marginPadding;
|
||||
|
||||
@NonNull
|
||||
public Builder setSnapshot(Bitmap snapshot) {
|
||||
this.snapshot = snapshot;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder setLogo(Bitmap logo) {
|
||||
this.logo = logo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder setLogoSmall(Bitmap logoSmall) {
|
||||
this.logoSmall = logoSmall;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder setTextView(TextView textView) {
|
||||
this.textView = textView;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder setTextViewShort(TextView textViewShort) {
|
||||
this.textViewShort = textViewShort;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder setMarginPadding(float marginPadding) {
|
||||
this.marginPadding = marginPadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AttributionMeasure build() {
|
||||
return new AttributionMeasure(snapshot, logo, logoSmall, textView, textViewShort, marginPadding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package org.maplibre.android.attribution;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Responsible for parsing attribution data coming from Sources and MapSnapshot.
|
||||
* <p>
|
||||
* Exposes multiple configuration options to manipulate data being parsed.
|
||||
* Use the Options object to build these configurations.
|
||||
* </p>
|
||||
*/
|
||||
public class AttributionParser {
|
||||
|
||||
private final WeakReference<Context> context;
|
||||
private final Set<Attribution> attributions = new LinkedHashSet<>();
|
||||
private final String attributionData;
|
||||
private final boolean withImproveMap;
|
||||
private final boolean withCopyrightSign;
|
||||
private final boolean withMapboxAttribution;
|
||||
|
||||
AttributionParser(WeakReference<Context> context, String attributionData, boolean withImproveMap,
|
||||
boolean withCopyrightSign, boolean withMapboxAttribution) {
|
||||
this.context = context;
|
||||
this.attributionData = attributionData;
|
||||
this.withImproveMap = withImproveMap;
|
||||
this.withCopyrightSign = withCopyrightSign;
|
||||
this.withMapboxAttribution = withMapboxAttribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attributions.
|
||||
*
|
||||
* @return the attributions
|
||||
*/
|
||||
@NonNull
|
||||
public Set<Attribution> getAttributions() {
|
||||
return attributions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attribution string.
|
||||
*
|
||||
* @return the parsed attribution string
|
||||
*/
|
||||
@NonNull
|
||||
public String createAttributionString() {
|
||||
return createAttributionString(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attribution string.
|
||||
*
|
||||
* @param shortenedOutput if attribution string should contain shortened output
|
||||
* @return the parsed attribution string
|
||||
*/
|
||||
@NonNull
|
||||
public String createAttributionString(boolean shortenedOutput) {
|
||||
StringBuilder stringBuilder = new StringBuilder(withCopyrightSign ? "" : "© ");
|
||||
int counter = 0;
|
||||
for (Attribution attribution : attributions) {
|
||||
counter++;
|
||||
stringBuilder.append(!shortenedOutput ? attribution.getTitle() : attribution.getTitleAbbreviated());
|
||||
if (counter != attributions.size()) {
|
||||
stringBuilder.append(" / ");
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main attribution for configuration
|
||||
*/
|
||||
protected void parse() {
|
||||
parseAttributions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse attributions
|
||||
*/
|
||||
private void parseAttributions() {
|
||||
SpannableStringBuilder htmlBuilder = (SpannableStringBuilder) fromHtml(attributionData);
|
||||
URLSpan[] urlSpans = htmlBuilder.getSpans(0, htmlBuilder.length(), URLSpan.class);
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
parseUrlSpan(htmlBuilder, urlSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an URLSpan containing an attribution.
|
||||
*
|
||||
* @param htmlBuilder the html builder
|
||||
* @param urlSpan the url span to be parsed
|
||||
*/
|
||||
private void parseUrlSpan(@NonNull SpannableStringBuilder htmlBuilder, @NonNull URLSpan urlSpan) {
|
||||
String url = urlSpan.getURL();
|
||||
if (isUrlValid(url)) {
|
||||
String anchor = parseAnchorValue(htmlBuilder, urlSpan);
|
||||
attributions.add(new Attribution(anchor, url));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid to be included in the final attribution.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid
|
||||
*/
|
||||
private boolean isUrlValid(@NonNull String url) {
|
||||
return isValidForImproveThisMap(url) && isValidForMapbox(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid for the improve map configuration.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid for improve this map
|
||||
*/
|
||||
private boolean isValidForImproveThisMap(@NonNull String url) {
|
||||
return withImproveMap || !(Attribution.IMPROVE_MAP_URLS.contains(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid for the MapLibre configuration.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid for MapLibre
|
||||
*/
|
||||
private boolean isValidForMapbox(@NonNull String url) {
|
||||
return withMapboxAttribution || !url.equals(Attribution.MAPBOX_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the attribution by parsing the anchor value of html href tag.
|
||||
*
|
||||
* @param htmlBuilder the html builder
|
||||
* @param urlSpan the current urlSpan
|
||||
* @return the parsed anchor value
|
||||
*/
|
||||
@NonNull
|
||||
private String parseAnchorValue(SpannableStringBuilder htmlBuilder, URLSpan urlSpan) {
|
||||
int start = htmlBuilder.getSpanStart(urlSpan);
|
||||
int end = htmlBuilder.getSpanEnd(urlSpan);
|
||||
int length = end - start;
|
||||
char[] charKey = new char[length];
|
||||
htmlBuilder.getChars(start, end, charKey, 0);
|
||||
return stripCopyright(String.valueOf(charKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to strip the copyright sign from an attribution
|
||||
*
|
||||
* @param anchor the attribution string to strip
|
||||
* @return the stripped attribution string without the copyright sign
|
||||
*/
|
||||
@NonNull
|
||||
private String stripCopyright(@NonNull String anchor) {
|
||||
if (!withCopyrightSign && anchor.startsWith("© ")) {
|
||||
anchor = anchor.substring(2, anchor.length());
|
||||
}
|
||||
return anchor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to a spanned html representation.
|
||||
*
|
||||
* @param html the string to convert
|
||||
* @return the spanned html representation
|
||||
*/
|
||||
private static Spanned fromHtml(String html) {
|
||||
Spanned result;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
result = Html.fromHtml(html);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to configure using an AttributionParser.
|
||||
* <p>
|
||||
* AttributionData, set with {@link #withAttributionData(String...)}, is the only required property to build
|
||||
* the underlying AttributionParser. Other properties include trimming the copyright sign, hiding
|
||||
* attribution as improve this map and MapLibre.
|
||||
* </p>
|
||||
*/
|
||||
public static class Options {
|
||||
private final WeakReference<Context> context;
|
||||
private boolean withImproveMap = true;
|
||||
private boolean withCopyrightSign = true;
|
||||
private boolean withMapboxAttribution = true;
|
||||
private String[] attributionDataStringArray;
|
||||
|
||||
public Options(@NonNull Context context) {
|
||||
this.context = new WeakReference<>(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Options withAttributionData(String... attributionData) {
|
||||
this.attributionDataStringArray = attributionData;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Options withImproveMap(boolean withImproveMap) {
|
||||
this.withImproveMap = withImproveMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Options withCopyrightSign(boolean withCopyrightSign) {
|
||||
this.withCopyrightSign = withCopyrightSign;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Options withMapboxAttribution(boolean withMapboxAttribution) {
|
||||
this.withMapboxAttribution = withMapboxAttribution;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AttributionParser build() {
|
||||
if (attributionDataStringArray == null) {
|
||||
throw new IllegalStateException("Using builder without providing attribution data");
|
||||
}
|
||||
|
||||
String fullAttributionString = parseAttribution(attributionDataStringArray);
|
||||
AttributionParser attributionParser = new AttributionParser(
|
||||
context,
|
||||
fullAttributionString,
|
||||
withImproveMap,
|
||||
withCopyrightSign,
|
||||
withMapboxAttribution
|
||||
);
|
||||
attributionParser.parse();
|
||||
return attributionParser;
|
||||
}
|
||||
|
||||
private String parseAttribution(String[] attribution) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String attr : attribution) {
|
||||
if (!attr.isEmpty()) {
|
||||
builder.append(attr);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Attribution API classes.
|
||||
*/
|
||||
package org.maplibre.android.attribution;
|
||||
@@ -0,0 +1,394 @@
|
||||
package org.maplibre.android.camera
|
||||
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.Size
|
||||
import org.maplibre.android.R
|
||||
import org.maplibre.android.camera.CameraPosition.Builder
|
||||
import org.maplibre.android.camera.CameraUpdateFactory.CameraPositionUpdate
|
||||
import org.maplibre.android.camera.CameraUpdateFactory.ZoomUpdate
|
||||
import org.maplibre.android.constants.MapLibreConstants
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.utils.MathUtils
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* Resembles the position, angle, zoom and tilt of the user's viewpoint.
|
||||
*/
|
||||
class CameraPosition
|
||||
/**
|
||||
* Constructs a CameraPosition.
|
||||
*
|
||||
* @param target The target location to align with the center of the screen.
|
||||
* @param zoom Zoom level at target. See zoom(float) for details of restrictions.
|
||||
* @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float)
|
||||
* for details of restrictions.
|
||||
* @param bearing Direction that the camera is pointing in, in degrees clockwise from north.
|
||||
* This value will be normalized to be within 0 degrees inclusive and 360 degrees
|
||||
* exclusive.
|
||||
* @param padding Padding in pixels. Specified in left, top, right, bottom order.
|
||||
* @throws NullPointerException if target is null
|
||||
* @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive.
|
||||
*/ @Keep internal constructor(
|
||||
|
||||
/**
|
||||
* The location that the camera is pointing at.
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val target: LatLng?,
|
||||
|
||||
/**
|
||||
* Zoom level near the center of the screen. See [Builder.zoom] for the definition of the camera's
|
||||
* zoom level.
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val zoom: Double,
|
||||
|
||||
/**
|
||||
* The angle, in degrees, of the camera angle from the nadir (directly facing the Earth).
|
||||
* See [Builder.tilt] for details of restrictions on the range of values.
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val tilt: Double,
|
||||
|
||||
/**
|
||||
* Direction that the camera is pointing in, in degrees clockwise from north.
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val bearing: Double,
|
||||
|
||||
/**
|
||||
* Padding in pixels. Specified in left, top, right, bottom order.
|
||||
* See [Builder.padding] for the definition of the camera's padding.
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val padding: DoubleArray?
|
||||
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Constructs a CameraPosition.
|
||||
*
|
||||
* @param target The target location to align with the center of the screen.
|
||||
* @param zoom Zoom level at target. See zoom(float) for details of restrictions.
|
||||
* @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float)
|
||||
* for details of restrictions.
|
||||
* @param bearing Direction that the camera is pointing in, in degrees clockwise from north.
|
||||
* This value will be normalized to be within 0 degrees inclusive and 360 degrees
|
||||
* exclusive.
|
||||
* @throws NullPointerException if target is null
|
||||
* @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive.
|
||||
*/
|
||||
@Deprecated("use {@link CameraPosition#CameraPosition(LatLng, double, double, double, double[])} instead.")
|
||||
internal constructor(target: LatLng?, zoom: Double, tilt: Double, bearing: Double) : this(target, zoom, tilt, bearing, null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable's
|
||||
* marshalled representation.
|
||||
*
|
||||
* @return integer 0.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written. May be 0 or
|
||||
* [.PARCELABLE_WRITE_RETURN_VALUE].
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeDouble(bearing)
|
||||
out.writeParcelable(target, flags)
|
||||
out.writeDouble(tilt)
|
||||
out.writeDouble(zoom)
|
||||
if (padding != null) {
|
||||
val length = padding.size
|
||||
out.writeInt(length)
|
||||
for (v in padding) {
|
||||
out.writeDouble(v)
|
||||
}
|
||||
} else {
|
||||
out.writeInt(-1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a String with the camera target, zoom, bearing and tilt.
|
||||
*
|
||||
* @return A String with CameraPosition information.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return ("Target: " + target + ", Zoom:" + zoom + ", Bearing:" + bearing + ", Tilt:" + tilt + ", Padding:" + Arrays.toString(padding))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this [CameraPosition] object with another [CameraPosition] and
|
||||
* determines if their target, zoom, tilt, and bearing match.
|
||||
*
|
||||
* @param other Another [CameraPosition] to compare with this object.
|
||||
* @return True if target, zoom, tilt, and bearing match this [CameraPosition] object.
|
||||
* Else, false.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val cameraPosition = other as CameraPosition
|
||||
if (target != null && target != cameraPosition.target) {
|
||||
return false
|
||||
} else if (zoom != cameraPosition.zoom) {
|
||||
return false
|
||||
} else if (tilt != cameraPosition.tilt) {
|
||||
return false
|
||||
} else if (bearing != cameraPosition.bearing) {
|
||||
return false
|
||||
} else if (!Arrays.equals(padding, cameraPosition.padding)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an integer which can be used as the bucket number for storing elements of the set/map.
|
||||
* This bucket number is the address of the element inside the set/map. There's no guarantee
|
||||
* that this hash value will be consistent between different Java implementations, or even
|
||||
* between different execution runs of the same program.
|
||||
*
|
||||
* @return integer value you can use for storing element.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
var temp: Long
|
||||
temp = java.lang.Double.doubleToLongBits(bearing)
|
||||
result = (temp xor (temp ushr 32)).toInt()
|
||||
result = 31 * result + (target?.hashCode() ?: 0)
|
||||
temp = java.lang.Double.doubleToLongBits(tilt)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(zoom)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
result = 31 * result + Arrays.hashCode(padding)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for composing CameraPosition objects.
|
||||
*/
|
||||
class Builder {
|
||||
private var bearing = -1.0
|
||||
private var target: LatLng? = null
|
||||
private var tilt = -1.0
|
||||
private var zoom = -1.0
|
||||
private var padding: DoubleArray? = null
|
||||
|
||||
/**
|
||||
* Create an empty builder.
|
||||
*/
|
||||
constructor() : super() {}
|
||||
|
||||
/**
|
||||
* Create a builder with an existing CameraPosition data.
|
||||
*
|
||||
* @param previous Existing CameraPosition values to use
|
||||
*/
|
||||
constructor(previous: CameraPosition?) : super() {
|
||||
if (previous != null) {
|
||||
bearing = previous.bearing
|
||||
target = previous.target
|
||||
tilt = previous.tilt
|
||||
zoom = previous.zoom
|
||||
padding = previous.padding
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with an existing CameraPosition data.
|
||||
*
|
||||
* @param typedArray TypedArray containing attribute values
|
||||
*/
|
||||
constructor(typedArray: TypedArray?) : super() {
|
||||
if (typedArray != null) {
|
||||
bearing = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraBearing, 0.0f).toDouble()
|
||||
val lat = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTargetLat, 0.0f).toDouble()
|
||||
val lng = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTargetLng, 0.0f).toDouble()
|
||||
target = LatLng(lat, lng)
|
||||
tilt = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTilt, 0.0f).toDouble()
|
||||
zoom = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraZoom, 0.0f).toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder from an existing CameraPositionUpdate update.
|
||||
*
|
||||
* @param update Update containing camera options
|
||||
*/
|
||||
constructor(update: CameraPositionUpdate?) : super() {
|
||||
if (update != null) {
|
||||
bearing = update.bearing
|
||||
target = update.target
|
||||
tilt = update.tilt
|
||||
zoom = update.zoom
|
||||
padding = update.padding
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create builder from an existing CameraPositionUpdate update.
|
||||
*
|
||||
* @param update Update containing camera options
|
||||
*/
|
||||
constructor(update: ZoomUpdate?) : super() {
|
||||
if (update != null) {
|
||||
zoom = update.zoom
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the direction that the camera is pointing in, in degrees clockwise from north.
|
||||
*
|
||||
* @param bearing Bearing
|
||||
* @return this
|
||||
*/
|
||||
fun bearing(bearing: Double): Builder {
|
||||
var direction = bearing
|
||||
while (direction >= 360) {
|
||||
direction -= 360.0
|
||||
}
|
||||
while (direction < 0) {
|
||||
direction += 360.0
|
||||
}
|
||||
this.bearing = direction
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location where the camera is pointing at.
|
||||
*
|
||||
* @param location target of the camera
|
||||
* @return this
|
||||
*/
|
||||
fun target(location: LatLng?): Builder {
|
||||
target = location
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tilt of the camera in degrees
|
||||
*
|
||||
*
|
||||
* value is clamped to [MapLibreConstants.MINIMUM_TILT] and [MapLibreConstants.MAXIMUM_TILT].
|
||||
*
|
||||
*
|
||||
* @param tilt Tilt value of the camera
|
||||
* @return this
|
||||
*/
|
||||
fun tilt(@FloatRange(from = MapLibreConstants.MINIMUM_TILT, to = MapLibreConstants.MAXIMUM_TILT) tilt: Double): Builder {
|
||||
this.tilt = MathUtils.clamp(tilt, MapLibreConstants.MINIMUM_TILT, MapLibreConstants.MAXIMUM_TILT)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zoom of the camera
|
||||
*
|
||||
*
|
||||
* Zoom ranges from [MapLibreConstants.MINIMUM_ZOOM] to [MapLibreConstants.MAXIMUM_ZOOM]
|
||||
*
|
||||
*
|
||||
* @param zoom Zoom value of the camera
|
||||
* @return this
|
||||
*/
|
||||
fun zoom(@FloatRange(from = MapLibreConstants.MINIMUM_ZOOM.toDouble(), to = MapLibreConstants.MAXIMUM_ZOOM.toDouble()) zoom: Double): Builder {
|
||||
this.zoom = zoom
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Padding in pixels that shifts the viewport by the specified amount.
|
||||
* Applied padding is going to persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* Specified in left, top, right, bottom order.
|
||||
*
|
||||
*
|
||||
* @param padding Camera padding
|
||||
* @return this
|
||||
*/
|
||||
fun padding(@Size(4) padding: DoubleArray?): Builder {
|
||||
this.padding = padding
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Padding in pixels that shifts the viewport by the specified amount.
|
||||
* Applied padding is going to persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* Specified in left, top, right, bottom order.
|
||||
*
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
fun padding(left: Double, top: Double, right: Double, bottom: Double): Builder {
|
||||
padding = doubleArrayOf(left, top, right, bottom)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the CameraPosition.
|
||||
*
|
||||
* @return CameraPosition
|
||||
*/
|
||||
fun build(): CameraPosition {
|
||||
return CameraPosition(target, zoom, tilt, bearing, padding)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val DEFAULT = CameraPosition(LatLng(), 0.0, 0.0, 0.0, doubleArrayOf(0.0, 0.0, 0.0, 0.0))
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<CameraPosition> = object : Parcelable.Creator<CameraPosition> {
|
||||
override fun createFromParcel(parcel: Parcel): CameraPosition {
|
||||
val bearing = parcel.readDouble()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val target = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
parcel.readParcelable(LatLng::class.java.classLoader, LatLng::class.java)
|
||||
} else {
|
||||
parcel.readParcelable(LatLng::class.java.classLoader)
|
||||
}
|
||||
val tilt = parcel.readDouble()
|
||||
val zoom = parcel.readDouble()
|
||||
var padding: DoubleArray? = null
|
||||
val paddingSize = parcel.readInt()
|
||||
if (paddingSize > 0) {
|
||||
padding = DoubleArray(paddingSize)
|
||||
for (i in 0 until paddingSize) {
|
||||
padding[i] = parcel.readDouble()
|
||||
}
|
||||
}
|
||||
return CameraPosition(target, zoom, tilt, bearing, padding)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CameraPosition?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.maplibre.android.camera
|
||||
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
|
||||
/**
|
||||
* Interface definition for camera updates.
|
||||
*/
|
||||
interface CameraUpdate {
|
||||
/**
|
||||
* Get the camera position from the camera update.
|
||||
*
|
||||
* @param maplibreMap Map object to build the position from
|
||||
* @return the camera position from the implementing camera update
|
||||
*/
|
||||
fun getCameraPosition(maplibreMap: MapLibreMap): CameraPosition?
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package org.maplibre.android.camera
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.PointF
|
||||
import androidx.annotation.IntDef
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.geometry.LatLngBounds
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import timber.log.Timber
|
||||
import java.lang.Double.max
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* Factory for creating CameraUpdate objects.
|
||||
*/
|
||||
object CameraUpdateFactory {
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the camera to a specified CameraPosition.
|
||||
*
|
||||
* @param cameraPosition Camera Position to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newCameraPosition(cameraPosition: CameraPosition): CameraUpdate {
|
||||
return CameraPositionUpdate(cameraPosition.bearing, cameraPosition.target, cameraPosition.tilt, cameraPosition.zoom, cameraPosition.padding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the center of the screen to a latitude and longitude
|
||||
* specified by a LatLng object. This centers the camera on the LatLng object.
|
||||
*
|
||||
* @param latLng Target location to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLng(latLng: LatLng): CameraUpdate {
|
||||
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that transforms the camera such that the specified
|
||||
* latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining
|
||||
* current camera position bearing and tilt values.
|
||||
*
|
||||
*
|
||||
* You can specify padding, in order to inset the bounding box from the map view's edges.
|
||||
* The padding will not persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* @param bounds Bounds to match Camera position with
|
||||
* @param padding Padding added to the bounds
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngBounds(bounds: LatLngBounds, padding: Int): CameraUpdate {
|
||||
return newLatLngBounds(bounds, padding, padding, padding, padding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that transforms the camera such that the specified
|
||||
* latitude/longitude bounds are centered on screen at the greatest possible zoom level while using
|
||||
* provided bearing and tilt values.
|
||||
*
|
||||
*
|
||||
* You can specify padding, in order to inset the bounding box from the map view's edges.
|
||||
* The padding will not persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* @param bounds Bounds to match Camera position with
|
||||
* @param bearing Bearing to take in account when generating the bounds
|
||||
* @param tilt Tilt to take in account when generating the bounds
|
||||
* @param padding Padding added to the bounds
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngBounds(bounds: LatLngBounds, bearing: Double, tilt: Double, padding: Int): CameraUpdate {
|
||||
return newLatLngBounds(bounds, bearing, tilt, padding, padding, padding, padding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that transforms the camera such that the specified
|
||||
* latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining
|
||||
* current camera position bearing and tilt values.
|
||||
*
|
||||
*
|
||||
* You can specify padding, in order to inset the bounding box from the map view's edges.
|
||||
* The padding will not persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* @param bounds Bounds to base the Camera position out of
|
||||
* @param paddingLeft Padding left of the bounds
|
||||
* @param paddingTop Padding top of the bounds
|
||||
* @param paddingRight Padding right of the bounds
|
||||
* @param paddingBottom Padding bottom of the bounds
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngBounds(bounds: LatLngBounds, paddingLeft: Int, paddingTop: Int, paddingRight: Int, paddingBottom: Int): CameraUpdate {
|
||||
return CameraBoundsUpdate(bounds, null, null, paddingLeft, paddingTop, paddingRight, paddingBottom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that transforms the camera such that the specified
|
||||
* latitude/longitude bounds are centered on screen at the greatest possible zoom level while using
|
||||
* provided bearing and tilt values.
|
||||
*
|
||||
*
|
||||
* You can specify padding, in order to inset the bounding box from the map view's edges.
|
||||
* The padding will not persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* @param bounds Bounds to base the Camera position out of
|
||||
* @param bearing Bearing to take in account when generating the bounds
|
||||
* @param tilt Tilt to take in account when generating the bounds
|
||||
* @param paddingLeft Padding left of the bounds
|
||||
* @param paddingTop Padding top of the bounds
|
||||
* @param paddingRight Padding right of the bounds
|
||||
* @param paddingBottom Padding bottom of the bounds
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngBounds(bounds: LatLngBounds, bearing: Double, tilt: Double, paddingLeft: Int, paddingTop: Int, paddingRight: Int, paddingBottom: Int): CameraUpdate {
|
||||
return CameraBoundsUpdate(bounds, bearing, tilt, paddingLeft, paddingTop, paddingRight, paddingBottom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the center of the screen to a latitude and longitude
|
||||
* specified by a LatLng object taking the specified padding into account.
|
||||
*
|
||||
* @param latLng Target location to change to
|
||||
* @param zoom Zoom level to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngZoom(latLng: LatLng, zoom: Double): CameraUpdate {
|
||||
return CameraPositionUpdate(-1.0, latLng, -1.0, zoom, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the center of the screen to a latitude and longitude
|
||||
* specified by a LatLng object, and moves to the given zoom level.
|
||||
* Applied padding is going to persist and impact following camera transformations.
|
||||
*
|
||||
* @param latLng Target location to change to
|
||||
* @param left Left padding
|
||||
* @param top Top padding
|
||||
* @param right Right padding
|
||||
* @param bottom Bottom padding
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newLatLngPadding(latLng: LatLng, left: Double, top: Double, right: Double, bottom: Double): CameraUpdate {
|
||||
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, doubleArrayOf(left, top, right, bottom))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that shifts the zoom level of the current camera viewpoint.
|
||||
*
|
||||
* @param amount Amount of zoom level to change with
|
||||
* @param focus Focus point of zoom
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zoomBy(amount: Double, focus: Point): CameraUpdate {
|
||||
return ZoomUpdate(amount, focus.x.toFloat(), focus.y.toFloat())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that shifts the zoom level of the current camera viewpoint.
|
||||
*
|
||||
* @param amount Amount of zoom level to change with
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zoomBy(amount: Double): CameraUpdate {
|
||||
return ZoomUpdate(ZoomUpdate.ZOOM_BY, amount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that zooms in on the map by moving the viewpoint's height closer to
|
||||
* the Earth's surface. The zoom increment is 1.0.
|
||||
*
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zoomIn(): CameraUpdate {
|
||||
return ZoomUpdate(ZoomUpdate.ZOOM_IN)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that zooms out on the map by moving the viewpoint's height farther
|
||||
* away from the Earth's surface. The zoom increment is -1.0.
|
||||
*
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zoomOut(): CameraUpdate {
|
||||
return ZoomUpdate(ZoomUpdate.ZOOM_OUT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the camera viewpoint to a particular zoom level.
|
||||
*
|
||||
* @param zoom Zoom level to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zoomTo(zoom: Double): CameraUpdate {
|
||||
return ZoomUpdate(ZoomUpdate.ZOOM_TO, zoom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the camera viewpoint to a particular bearing.
|
||||
*
|
||||
* @param bearing Bearing to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun bearingTo(bearing: Double): CameraUpdate {
|
||||
return CameraPositionUpdate(bearing, null, -1.0, -1.0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that moves the camera viewpoint to a particular tilt.
|
||||
*
|
||||
* @param tilt Tilt to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun tiltTo(tilt: Double): CameraUpdate {
|
||||
return CameraPositionUpdate(-1.0, null, tilt, -1.0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that when animated changes the camera padding.
|
||||
* Applied padding is going to persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* Specified in left, top, right, bottom order.
|
||||
*
|
||||
*
|
||||
* @param padding Padding to change to
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun paddingTo(padding: DoubleArray?): CameraUpdate {
|
||||
return CameraPositionUpdate(-1.0, null, -1.0, -1.0, padding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CameraUpdate that when animated changes the camera padding.
|
||||
* Applied padding is going to persist and impact following camera transformations.
|
||||
*
|
||||
*
|
||||
* Specified in left, top, right, bottom order.
|
||||
*
|
||||
*
|
||||
* @return CameraUpdate Final Camera Position
|
||||
*/
|
||||
@JvmStatic
|
||||
fun paddingTo(left: Double, top: Double, right: Double, bottom: Double): CameraUpdate {
|
||||
return paddingTo(doubleArrayOf(left, top, right, bottom))
|
||||
}
|
||||
|
||||
//
|
||||
// CameraUpdate types
|
||||
//
|
||||
class CameraPositionUpdate(val bearing: Double, val target: LatLng?, val tilt: Double, val zoom: Double, val padding: DoubleArray?) : CameraUpdate {
|
||||
|
||||
override fun getCameraPosition(maplibreMap: MapLibreMap): CameraPosition {
|
||||
if (target == null) {
|
||||
val previousPosition = maplibreMap.cameraPosition
|
||||
return CameraPosition.Builder(this).target(previousPosition.target).build()
|
||||
}
|
||||
return CameraPosition.Builder(this).build()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val that = other as CameraPositionUpdate
|
||||
if (java.lang.Double.compare(that.bearing, bearing) != 0) {
|
||||
return false
|
||||
}
|
||||
if (java.lang.Double.compare(that.tilt, tilt) != 0) {
|
||||
return false
|
||||
}
|
||||
if (java.lang.Double.compare(that.zoom, zoom) != 0) {
|
||||
return false
|
||||
}
|
||||
return if (if (target != null) target != that.target else that.target != null) {
|
||||
false
|
||||
} else {
|
||||
Arrays.equals(padding, that.padding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
var temp: Long
|
||||
temp = java.lang.Double.doubleToLongBits(bearing)
|
||||
result = (temp xor (temp ushr 32)).toInt()
|
||||
result = 31 * result + (target?.hashCode() ?: 0)
|
||||
temp = java.lang.Double.doubleToLongBits(tilt)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(zoom)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
result = 31 * result + Arrays.hashCode(padding)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("CameraPositionUpdate{" + "bearing=" + bearing + ", target=" + target + ", tilt=" + tilt + ", zoom=" + zoom + ", padding=" + Arrays.toString(padding) + '}')
|
||||
}
|
||||
}
|
||||
|
||||
internal class CameraBoundsUpdate(val bounds: LatLngBounds, private val bearing: Double?, private val tilt: Double?, val padding: IntArray) : CameraUpdate {
|
||||
constructor(bounds: LatLngBounds, bearing: Double?, tilt: Double?, paddingLeft: Int, paddingTop: Int, paddingRight: Int, paddingBottom: Int) : this(
|
||||
bounds,
|
||||
bearing,
|
||||
tilt,
|
||||
intArrayOf(paddingLeft, paddingTop, paddingRight, paddingBottom)
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getCameraPosition(maplibreMap: MapLibreMap): CameraPosition? {
|
||||
return if (bearing == null && tilt == null) {
|
||||
// use current camera position tilt and bearing
|
||||
maplibreMap.getCameraForLatLngBounds(bounds, padding)
|
||||
} else {
|
||||
// use provided tilt and bearing
|
||||
assert(bearing != null)
|
||||
assert(tilt != null)
|
||||
maplibreMap.getCameraForLatLngBounds(bounds, padding, bearing!!, tilt!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val that = other as CameraBoundsUpdate
|
||||
return if (bounds != that.bounds) {
|
||||
false
|
||||
} else {
|
||||
padding.contentEquals(that.padding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = bounds.hashCode()
|
||||
result = 31 * result + Arrays.hashCode(padding)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("CameraBoundsUpdate{" + "bounds=" + bounds + ", padding=" + padding.contentToString() + '}')
|
||||
}
|
||||
}
|
||||
|
||||
/* This class 1. Does not seem to be used 2. The code that calculates targetPoint seems to be a bit suspicious.
|
||||
commenting out for now.
|
||||
*/
|
||||
/*
|
||||
internal class CameraMoveUpdate(private val x: Float, private val y: Float) : CameraUpdate {
|
||||
override fun getCameraPosition(mapboxMap: MapboxMap): CameraPosition {
|
||||
val uiSettings = mapboxMap.uiSettings
|
||||
val projection = mapboxMap.projection
|
||||
// Calculate the new center point
|
||||
val viewPortWidth = uiSettings.width
|
||||
val viewPortHeight = uiSettings.height
|
||||
val padding = mapboxMap.cameraPosition.padding!!
|
||||
|
||||
// we inverse the map padding, is reapplied when using moveTo/easeTo or animateTo
|
||||
val targetPoint = PointF((viewPortWidth - padding[0].toFloat() + padding[1].toFloat()) / 2 + x,
|
||||
(viewPortHeight + padding[1].toFloat() - padding[3].toFloat()) / 2 + y)
|
||||
val latLng = projection.fromScreenLocation(targetPoint)
|
||||
val previousPosition = mapboxMap.cameraPosition
|
||||
return CameraPosition.Builder().target(latLng).zoom(previousPosition.zoom).tilt(previousPosition.tilt).bearing(previousPosition.bearing).build()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val that = other as CameraMoveUpdate
|
||||
return if (java.lang.Float.compare(that.x, x) != 0) {
|
||||
false
|
||||
} else {
|
||||
that.y.compareTo(y) == 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = if (x != +0.0f) java.lang.Float.floatToIntBits(x) else 0
|
||||
result = 31 * result + if (y != +0.0f) java.lang.Float.floatToIntBits(y) else 0
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("CameraMoveUpdate{" + "x=" + x + ", y=" + y + '}')
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
class ZoomUpdate : CameraUpdate {
|
||||
@IntDef(ZOOM_IN, ZOOM_OUT, ZOOM_BY, ZOOM_TO, ZOOM_TO_POINT)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
internal annotation class Type
|
||||
|
||||
@get:Type
|
||||
@Type
|
||||
val type: Int
|
||||
val zoom: Double
|
||||
var x = 0f
|
||||
private set
|
||||
var y = 0f
|
||||
private set
|
||||
|
||||
constructor(@Type type: Int) {
|
||||
this.type = type
|
||||
zoom = 0.0
|
||||
}
|
||||
|
||||
constructor(@Type type: Int, zoom: Double) {
|
||||
this.type = type
|
||||
this.zoom = zoom
|
||||
}
|
||||
|
||||
constructor(zoom: Double, x: Float, y: Float) {
|
||||
type = ZOOM_TO_POINT
|
||||
this.zoom = zoom
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
|
||||
private fun transformZoom(currentZoomArg: Double): Double {
|
||||
return when (type) {
|
||||
ZOOM_IN -> currentZoomArg + 1
|
||||
ZOOM_OUT -> {
|
||||
max(currentZoomArg - 1, 0.0)
|
||||
}
|
||||
|
||||
ZOOM_TO -> zoom
|
||||
ZOOM_BY, ZOOM_TO_POINT -> currentZoomArg + zoom
|
||||
else -> {
|
||||
Timber.e("Unprocessed when branch")
|
||||
4.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCameraPosition(maplibreMap: MapLibreMap): CameraPosition {
|
||||
val cameraPosition = maplibreMap.cameraPosition
|
||||
return if (type != ZOOM_TO_POINT) {
|
||||
CameraPosition.Builder(cameraPosition).zoom(transformZoom(cameraPosition.zoom)).build()
|
||||
} else {
|
||||
CameraPosition.Builder(cameraPosition).zoom(transformZoom(cameraPosition.zoom)).target(maplibreMap.projection.fromScreenLocation(PointF(x, y))).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val that = other as ZoomUpdate
|
||||
if (type != that.type) {
|
||||
return false
|
||||
}
|
||||
if (java.lang.Double.compare(that.zoom, zoom) != 0) {
|
||||
return false
|
||||
}
|
||||
return if (java.lang.Float.compare(that.x, x) != 0) {
|
||||
false
|
||||
} else {
|
||||
that.y.compareTo(y) == 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
val temp: Long
|
||||
result = type
|
||||
temp = java.lang.Double.doubleToLongBits(zoom)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
result = 31 * result + if (x != +0.0f) java.lang.Float.floatToIntBits(x) else 0
|
||||
result = 31 * result + if (y != +0.0f) java.lang.Float.floatToIntBits(y) else 0
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("ZoomUpdate{" + "type=" + type + ", zoom=" + zoom + ", x=" + x + ", y=" + y + '}')
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ZOOM_IN = 0
|
||||
const val ZOOM_OUT = 1
|
||||
const val ZOOM_BY = 2
|
||||
const val ZOOM_TO = 3
|
||||
const val ZOOM_TO_POINT = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Camera API classes.
|
||||
*/
|
||||
package org.maplibre.android.camera
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.maplibre.android.constants;
|
||||
|
||||
/**
|
||||
* Contains constants used throughout the sdk classes.
|
||||
*/
|
||||
public class GeometryConstants {
|
||||
|
||||
/**
|
||||
* The <a href='http://en.wikipedia.org/wiki/Earth_radius#Equatorial_radius'>equatorial radius</a>
|
||||
* value in meters
|
||||
*/
|
||||
public static final int RADIUS_EARTH_METERS = 6378137;
|
||||
|
||||
/**
|
||||
* This constant represents the lowest longitude value available to represent a wrapped geolocation.
|
||||
*/
|
||||
public static final double MIN_WRAP_LONGITUDE = -180;
|
||||
|
||||
/**
|
||||
* This constant represents the highest longitude value available to represent a wrapped geolocation.
|
||||
*/
|
||||
public static final double MAX_WRAP_LONGITUDE = 180;
|
||||
|
||||
/**
|
||||
* This constant represents the lowest longitude value available to represent a geolocation.
|
||||
*/
|
||||
public static final double MIN_LONGITUDE = -Double.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* This constant represents the highest longitude value available to represent a geolocation.
|
||||
*/
|
||||
public static final double MAX_LONGITUDE = Double.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* This constant represents the lowest latitude value available to represent a geolocation.
|
||||
*/
|
||||
public static final double MIN_LATITUDE = -90;
|
||||
|
||||
/**
|
||||
* This constant represents the latitude span when representing a geolocation.
|
||||
*/
|
||||
public static final double LATITUDE_SPAN = 180;
|
||||
|
||||
/**
|
||||
* This constant represents the longitude span when representing a geolocation.
|
||||
*/
|
||||
public static final double LONGITUDE_SPAN = 360;
|
||||
|
||||
/**
|
||||
* This constant represents the highest latitude value available to represent a geolocation.
|
||||
*/
|
||||
public static final double MAX_LATITUDE = 90;
|
||||
|
||||
/**
|
||||
* Maximum latitude value in Mercator projection.
|
||||
*/
|
||||
public static final double MAX_MERCATOR_LATITUDE = 85.05112877980659;
|
||||
|
||||
/**
|
||||
* Minimum latitude value in Mercator projection.
|
||||
*/
|
||||
public static final double MIN_MERCATOR_LATITUDE = -85.05112877980659;
|
||||
|
||||
private GeometryConstants() {
|
||||
// Private constructor to prevent initializing of this class.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.maplibre.android.constants;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* MapLibreConstants exposes MapLibre related constants
|
||||
*/
|
||||
public class MapLibreConstants {
|
||||
|
||||
/**
|
||||
* Default Locale for data processing (ex: String.toLowerCase(MAPBOX_LOCALE, "foo"))
|
||||
*/
|
||||
public static final Locale MAPLIBRE_LOCALE = Locale.US;
|
||||
|
||||
/**
|
||||
* The name of the desired preferences file for Android's SharedPreferences.
|
||||
*/
|
||||
public static final String MAPLIBRE_SHARED_PREFERENCES = "MapboxSharedPreferences";
|
||||
|
||||
/**
|
||||
* Key used to switch storage to external in AndroidManifest.xml
|
||||
*/
|
||||
public static final String KEY_META_DATA_SET_STORAGE_EXTERNAL = "com.mapbox.SetStorageExternal";
|
||||
|
||||
/**
|
||||
* Default value for KEY_META_DATA_SET_STORAGE_EXTERNAL (default is internal storage)
|
||||
*/
|
||||
public static final boolean DEFAULT_SET_STORAGE_EXTERNAL = false;
|
||||
|
||||
/**
|
||||
* Key used to switch Tile Download Measuring on/off in AndroidManifest.xml
|
||||
*/
|
||||
public static final String KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON = "com.mapbox.MeasureTileDownloadOn";
|
||||
|
||||
/**
|
||||
* Default value for KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON (default is off)
|
||||
*/
|
||||
public static final boolean DEFAULT_MEASURE_TILE_DOWNLOAD_ON = false;
|
||||
|
||||
/**
|
||||
* Default value for font fallback for local ideograph fonts
|
||||
*/
|
||||
public static final String DEFAULT_FONT = "sans-serif";
|
||||
|
||||
/**
|
||||
* Unmeasured state
|
||||
*/
|
||||
public static final float UNMEASURED = -1f;
|
||||
|
||||
/**
|
||||
* Default animation time
|
||||
*/
|
||||
public static final int ANIMATION_DURATION = 300;
|
||||
|
||||
/**
|
||||
* Default short animation time
|
||||
*/
|
||||
public static final int ANIMATION_DURATION_SHORT = 150;
|
||||
|
||||
/**
|
||||
* Animation time of a fling gesture
|
||||
*/
|
||||
public static final long ANIMATION_DURATION_FLING_BASE = ANIMATION_DURATION_SHORT;
|
||||
|
||||
/**
|
||||
* Velocity threshold for a fling gesture
|
||||
*/
|
||||
public static final long VELOCITY_THRESHOLD_IGNORE_FLING = 1000;
|
||||
|
||||
/**
|
||||
* Vertical angle threshold for a horizontal disabled fling gesture
|
||||
*/
|
||||
public static final long ANGLE_THRESHOLD_IGNORE_VERTICAL_FLING = 75;
|
||||
|
||||
/**
|
||||
* Value by which the default rotation threshold will be increased when scaling
|
||||
*
|
||||
* @deprecated unused, see {@link org.maplibre.android.maps.UiSettings#setDisableRotateWhenScaling(boolean)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final float ROTATION_THRESHOLD_INCREASE_WHEN_SCALING = 25f;
|
||||
|
||||
/**
|
||||
* Maximum absolute zoom change for multi-pointer scale velocity animation
|
||||
*/
|
||||
public static final double MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE = 2.5;
|
||||
|
||||
/**
|
||||
* Maximum possible zoom change during the quick zoom gesture executed across the whole screen
|
||||
*/
|
||||
public static final double QUICK_ZOOM_MAX_ZOOM_CHANGE = 4.0;
|
||||
|
||||
/**
|
||||
* Scale velocity animation duration multiplier.
|
||||
*/
|
||||
public static final double SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER = 150;
|
||||
|
||||
/**
|
||||
* Minimum angular velocity for rotation animation
|
||||
*
|
||||
* @deprecated unused, see {@link #ROTATE_VELOCITY_RATIO_THRESHOLD}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final float MINIMUM_ANGULAR_VELOCITY = 1.5f;
|
||||
|
||||
/**
|
||||
* Last scale span delta to XY velocity ratio required to execute scale velocity animation.
|
||||
*/
|
||||
public static final double SCALE_VELOCITY_RATIO_THRESHOLD = 4 * 1e-3;
|
||||
|
||||
/**
|
||||
* Last rotation delta to XY velocity ratio required to execute rotation velocity animation.
|
||||
*/
|
||||
public static final double ROTATE_VELOCITY_RATIO_THRESHOLD = 2.2 * 1e-4;
|
||||
|
||||
/**
|
||||
* Time within which user needs to lift fingers for velocity animation to start.
|
||||
*/
|
||||
public static final long SCHEDULED_ANIMATION_TIMEOUT = 150L;
|
||||
|
||||
/**
|
||||
* Maximum angular velocity for rotation animation
|
||||
*/
|
||||
public static final float MAXIMUM_ANGULAR_VELOCITY = 30f;
|
||||
|
||||
/**
|
||||
* Factor to calculate tilt change based on pixel change during shove gesture.
|
||||
*/
|
||||
public static final float SHOVE_PIXEL_CHANGE_FACTOR = 0.1f;
|
||||
|
||||
/**
|
||||
* The currently supported minimum zoom level.
|
||||
*/
|
||||
public static final float MINIMUM_ZOOM = 0.0f;
|
||||
|
||||
/**
|
||||
* The currently supported maximum zoom level.
|
||||
*/
|
||||
public static final float MAXIMUM_ZOOM = 25.5f;
|
||||
|
||||
/**
|
||||
* The currently supported minimum pitch level.
|
||||
*/
|
||||
public static final float MINIMUM_PITCH = 0.0f;
|
||||
|
||||
/**
|
||||
* The currently supported maximum pitch level.
|
||||
*/
|
||||
public static final float MAXIMUM_PITCH = 60.0f;
|
||||
|
||||
/**
|
||||
* The currently supported maximum tilt value.
|
||||
*/
|
||||
public static final double MAXIMUM_TILT = 60;
|
||||
|
||||
/**
|
||||
* The currently supported minimum tilt value.
|
||||
*/
|
||||
public static final double MINIMUM_TILT = 0;
|
||||
|
||||
/**
|
||||
* The currently supported maximum direction
|
||||
*/
|
||||
public static final double MAXIMUM_DIRECTION = 360;
|
||||
|
||||
/**
|
||||
* The currently supported minimum direction
|
||||
*/
|
||||
public static final double MINIMUM_DIRECTION = 0;
|
||||
|
||||
/**
|
||||
* The currently used minimum scale factor to clamp to when a quick zoom gesture occurs
|
||||
*
|
||||
* @deprecated unused
|
||||
*/
|
||||
@Deprecated
|
||||
public static final float MINIMUM_SCALE_FACTOR_CLAMP = 0.00f;
|
||||
|
||||
/**
|
||||
* The currently used maximum scale factor to clamp to when a quick zoom gesture occurs
|
||||
*
|
||||
* @deprecated unused
|
||||
*/
|
||||
@Deprecated
|
||||
public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 0.15f;
|
||||
|
||||
/**
|
||||
* Zoom value multiplier for scale gestures.
|
||||
*/
|
||||
public static final float ZOOM_RATE = 0.65f;
|
||||
|
||||
/**
|
||||
* Fragment Argument Key for MapboxMapOptions
|
||||
*/
|
||||
public static final String FRAG_ARG_MAPLIBREMAPOPTIONS = "MapboxMapOptions";
|
||||
|
||||
/**
|
||||
* Layer Id of annotations layer
|
||||
*/
|
||||
public static final String LAYER_ID_ANNOTATIONS = "com.mapbox.annotations.points";
|
||||
|
||||
// Save instance state keys
|
||||
public static final String STATE_HAS_SAVED_STATE = "mapbox_savedState";
|
||||
public static final String STATE_CAMERA_POSITION = "mapbox_cameraPosition";
|
||||
public static final String STATE_ZOOM_ENABLED = "mapbox_zoomEnabled";
|
||||
public static final String STATE_SCROLL_ENABLED = "mapbox_scrollEnabled";
|
||||
public static final String STATE_HORIZONAL_SCROLL_ENABLED = "mapbox_horizontalScrollEnabled";
|
||||
public static final String STATE_ROTATE_ENABLED = "mapbox_rotateEnabled";
|
||||
public static final String STATE_TILT_ENABLED = "mapbox_tiltEnabled";
|
||||
public static final String STATE_DOUBLE_TAP_ENABLED = "mapbox_doubleTapEnabled";
|
||||
public static final String STATE_QUICK_ZOOM_ENABLED = "mapbox_quickZoom";
|
||||
public static final String STATE_ZOOM_RATE = "mapbox_zoomRate";
|
||||
public static final String STATE_DEBUG_ACTIVE = "mapbox_debugActive";
|
||||
public static final String STATE_COMPASS_ENABLED = "mapbox_compassEnabled";
|
||||
public static final String STATE_COMPASS_GRAVITY = "mapbox_compassGravity";
|
||||
public static final String STATE_COMPASS_MARGIN_LEFT = "mapbox_compassMarginLeft";
|
||||
public static final String STATE_COMPASS_MARGIN_TOP = "mapbox_compassMarginTop";
|
||||
public static final String STATE_COMPASS_MARGIN_RIGHT = "mapbox_compassMarginRight";
|
||||
public static final String STATE_COMPASS_MARGIN_BOTTOM = "mapbox_compassMarginBottom";
|
||||
public static final String STATE_COMPASS_FADE_WHEN_FACING_NORTH = "mapbox_compassFade";
|
||||
public static final String STATE_COMPASS_IMAGE_BITMAP = "mapbox_compassImage";
|
||||
public static final String STATE_LOGO_GRAVITY = "mapbox_logoGravity";
|
||||
public static final String STATE_LOGO_MARGIN_LEFT = "mapbox_logoMarginLeft";
|
||||
public static final String STATE_LOGO_MARGIN_TOP = "mapbox_logoMarginTop";
|
||||
public static final String STATE_LOGO_MARGIN_RIGHT = "mapbox_logoMarginRight";
|
||||
public static final String STATE_LOGO_MARGIN_BOTTOM = "mapbox_logoMarginBottom";
|
||||
public static final String STATE_LOGO_ENABLED = "mapbox_logoEnabled";
|
||||
public static final String STATE_ATTRIBUTION_GRAVITY = "mapbox_attrGravity";
|
||||
public static final String STATE_ATTRIBUTION_MARGIN_LEFT = "mapbox_attrMarginLeft";
|
||||
public static final String STATE_ATTRIBUTION_MARGIN_TOP = "mapbox_attrMarginTop";
|
||||
public static final String STATE_ATTRIBUTION_MARGIN_RIGHT = "mapbox_attrMarginRight";
|
||||
public static final String STATE_ATTRIBUTION_MARGIN_BOTTOM = "mapbox_atrrMarginBottom";
|
||||
public static final String STATE_ATTRIBUTION_ENABLED = "mapbox_atrrEnabled";
|
||||
public static final String STATE_DESELECT_MARKER_ON_TAP = "mapbox_deselectMarkerOnTap";
|
||||
public static final String STATE_USER_FOCAL_POINT = "mapbox_userFocalPoint";
|
||||
public static final String STATE_SCALE_ANIMATION_ENABLED = "mapbox_scaleAnimationEnabled";
|
||||
public static final String STATE_ROTATE_ANIMATION_ENABLED = "mapbox_rotateAnimationEnabled";
|
||||
public static final String STATE_FLING_ANIMATION_ENABLED = "mapbox_flingAnimationEnabled";
|
||||
public static final String STATE_INCREASE_ROTATE_THRESHOLD = "mapbox_increaseRotateThreshold";
|
||||
public static final String STATE_DISABLE_ROTATE_WHEN_SCALING = "mapbox_disableRotateWhenScaling";
|
||||
public static final String STATE_INCREASE_SCALE_THRESHOLD = "mapbox_increaseScaleThreshold";
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Constant API classes.
|
||||
*/
|
||||
package org.maplibre.android.constants;
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
public class CalledFromWorkerThreadException extends RuntimeException {
|
||||
|
||||
public CalledFromWorkerThreadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
/**
|
||||
* A ConversionException is thrown when a conversion failed to execute.
|
||||
*/
|
||||
public class ConversionException extends RuntimeException {
|
||||
|
||||
public ConversionException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
import org.maplibre.android.annotations.Icon;
|
||||
import org.maplibre.android.annotations.Marker;
|
||||
import org.maplibre.android.maps.MapView;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* An IconBitmapChangedException is thrown by MapView when a Marker is added
|
||||
* that has an Icon with a Bitmap that has been modified since the creation of the Icon.
|
||||
* </p>
|
||||
* You cannot modify a {@code Icon} after it has been added to the map in a {@code Marker}
|
||||
*
|
||||
* @see MapView
|
||||
* @see Icon
|
||||
* @see Marker
|
||||
*/
|
||||
public class IconBitmapChangedException extends RuntimeException {
|
||||
|
||||
public IconBitmapChangedException() {
|
||||
super("The added Marker has an Icon with a bitmap that has been modified. An Icon cannot be modified"
|
||||
+ "after it has been added to the map in a Marker.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
/**
|
||||
* An InvalidLatLngBoundsException is thrown by LatLngBounds
|
||||
* when there aren't enough LatLng to create a bounds.
|
||||
*/
|
||||
public class InvalidLatLngBoundsException extends RuntimeException {
|
||||
|
||||
public InvalidLatLngBoundsException(int latLngsListSize) {
|
||||
super("Cannot create a LatLngBounds from " + latLngsListSize + " items");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
/**
|
||||
* A InvalidMarkerPositionException is thrown when a Marker object is created with an invalid LatLng position.
|
||||
*/
|
||||
public class InvalidMarkerPositionException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a invalid marker position exception thrown when a Marker object is created with an invalid LatLng position.
|
||||
*/
|
||||
public InvalidMarkerPositionException() {
|
||||
super("Adding an invalid Marker to a Map. "
|
||||
+ "Missing the required position field. "
|
||||
+ "Provide a non null LatLng as position to the Marker.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
import org.maplibre.android.MapLibre;
|
||||
import org.maplibre.android.WellKnownTileServer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A MapboxConfigurationException is thrown by MapLibreMap when the SDK hasn't been properly initialised.
|
||||
* <p>
|
||||
* This occurs either when {@link MapLibre} is not correctly initialised or the provided apiKey
|
||||
* through {@link MapLibre#getInstance(Context, String, WellKnownTileServer)} isn't valid.
|
||||
* </p>
|
||||
*
|
||||
* @see MapLibre#getInstance(Context, String, WellKnownTileServer)
|
||||
*/
|
||||
public class MapLibreConfigurationException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a MapLibre configuration exception thrown by MapboxMap when the SDK hasn't been properly initialised.
|
||||
*/
|
||||
public MapLibreConfigurationException() {
|
||||
super("\nUsing MapView requires calling MapLibre.getInstance(Context context, String apiKey, "
|
||||
+ "WellKnownTileServer wellKnownTileServer) before inflating or creating the view.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MapLibre configuration exception thrown by MapboxMap when the SDK hasn't been properly initialised.
|
||||
*/
|
||||
public MapLibreConfigurationException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.maplibre.android.exceptions;
|
||||
|
||||
import org.maplibre.android.annotations.IconFactory;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A TooManyIconsException is thrown by IconFactory when it
|
||||
* cannot create a Icon because there are already too many icons created.
|
||||
* </p>
|
||||
* You should try to reuse Icon objects whenever possible.
|
||||
*
|
||||
* @see IconFactory
|
||||
*/
|
||||
public class TooManyIconsException extends RuntimeException {
|
||||
|
||||
public TooManyIconsException() {
|
||||
super("Cannot create an Icon because there are already too many. Try reusing Icon objects whenever possible.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Exception API classes.
|
||||
*/
|
||||
package org.maplibre.android.exceptions;
|
||||
@@ -0,0 +1,277 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.location.Location
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.Keep
|
||||
import com.mapbox.geojson.Point
|
||||
import org.maplibre.android.constants.GeometryConstants
|
||||
import com.mapbox.turf.TurfConstants
|
||||
import com.mapbox.turf.TurfMeasurement
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* A geographical location which contains a single latitude, longitude pair, with
|
||||
* optional altitude.
|
||||
*
|
||||
*
|
||||
* Latitude and longitude are expressed as decimal degrees
|
||||
* in the WGS84 datum. By default, altitude is 0.0, or sea level.
|
||||
*
|
||||
*
|
||||
*
|
||||
* MapLibre GL displays maps in the Mercator Projection and projects geographical
|
||||
* data automatically, so all data enters in the WGS84 datum.
|
||||
*
|
||||
*/
|
||||
class LatLng : Parcelable {
|
||||
|
||||
/**
|
||||
* The latitude, in degrees.
|
||||
*
|
||||
* This value is in the range of [-90, 90], see [GeometryConstants.MIN_LATITUDE] and
|
||||
* [GeometryConstants.MAX_LATITUDE]
|
||||
*
|
||||
* @see GeometryConstants.MIN_LATITUDE
|
||||
*
|
||||
* @see GeometryConstants.MAX_LATITUDE
|
||||
*/
|
||||
@Keep
|
||||
var latitude = 0.0
|
||||
set(
|
||||
@FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) latitude
|
||||
) {
|
||||
require(!latitude.isNaN()) { "latitude must not be NaN" }
|
||||
require(abs(latitude) <= GeometryConstants.MAX_LATITUDE) { "latitude must be between -90 and 90" }
|
||||
field = latitude
|
||||
}
|
||||
|
||||
/**
|
||||
* The longitude, in degrees.
|
||||
*
|
||||
* This value is in the range of [-180, 180], see [GeometryConstants.MIN_LONGITUDE] and
|
||||
* [GeometryConstants.MAX_LONGITUDE]
|
||||
*
|
||||
* @see GeometryConstants.MIN_LONGITUDE
|
||||
* @see GeometryConstants.MAX_LONGITUDE
|
||||
*/
|
||||
@Keep
|
||||
var longitude = 0.0
|
||||
/**
|
||||
* Set the longitude, in degrees.
|
||||
*
|
||||
*
|
||||
* This value is in the range of [-180, 180], see [GeometryConstants.MIN_LONGITUDE] and
|
||||
* [GeometryConstants.MAX_LONGITUDE]
|
||||
*
|
||||
*
|
||||
* @param longitude the longitude value in degrees
|
||||
* @see GeometryConstants.MIN_LONGITUDE
|
||||
*
|
||||
* @see GeometryConstants.MAX_LONGITUDE
|
||||
*/
|
||||
set(@FloatRange(from = GeometryConstants.MIN_LONGITUDE, to = GeometryConstants.MAX_LONGITUDE) longitude) {
|
||||
require(!longitude.isNaN()) { "longitude must not be NaN" }
|
||||
require(!longitude.isInfinite()) { "longitude must not be infinite" }
|
||||
field = longitude
|
||||
}
|
||||
|
||||
/**
|
||||
* The altitude, in meters.
|
||||
*/
|
||||
var altitude = 0.0
|
||||
|
||||
/**
|
||||
* Construct a new latitude, longitude point at (0, 0)
|
||||
*/
|
||||
constructor() {
|
||||
latitude = 0.0
|
||||
longitude = 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new latitude, longitude point given double arguments
|
||||
*
|
||||
* @param latitude Latitude in degrees
|
||||
* @param longitude Longitude in degrees
|
||||
*/
|
||||
@Keep
|
||||
constructor(latitude: Double, longitude: Double) {
|
||||
this.latitude = latitude
|
||||
this.longitude = longitude
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new latitude, longitude, altitude point given double arguments
|
||||
*
|
||||
* @param latitude Latitude in degrees
|
||||
* @param longitude Longitude in degress
|
||||
* @param altitude Altitude in meters
|
||||
*/
|
||||
constructor(latitude: Double, longitude: Double, altitude: Double) {
|
||||
this.latitude = latitude
|
||||
this.longitude = longitude
|
||||
this.altitude = altitude
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new latitude, longitude, altitude point given location argument
|
||||
*
|
||||
* @param location Android Location
|
||||
*/
|
||||
constructor(location: Location) : this(location.latitude, location.longitude, location.altitude) {}
|
||||
|
||||
/**
|
||||
* Construct a new latitude, longitude, altitude point given another latitude, longitude, altitude point.
|
||||
*
|
||||
* @param latLng LatLng to be cloned.
|
||||
*/
|
||||
constructor(latLng: LatLng) {
|
||||
latitude = latLng.latitude
|
||||
longitude = latLng.longitude
|
||||
altitude = latLng.altitude
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new latitude, longitude, altitude tuple given a parcel.
|
||||
*
|
||||
* @param in the parcel containing the latitude, longitude, altitude values
|
||||
*/
|
||||
constructor(`in`: Parcel) {
|
||||
latitude = (`in`.readDouble())
|
||||
longitude = (`in`.readDouble())
|
||||
altitude = `in`.readDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new LatLng object with a wrapped Longitude. This allows original data object
|
||||
* to remain unchanged.
|
||||
*
|
||||
* @return new LatLng object with wrapped Longitude
|
||||
*/
|
||||
fun wrap(): LatLng {
|
||||
return LatLng(
|
||||
latitude,
|
||||
wrap(longitude, GeometryConstants.MIN_WRAP_LONGITUDE, GeometryConstants.MAX_WRAP_LONGITUDE)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*
|
||||
* @param other The object to compare
|
||||
* @return True if equal, false if not
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val latLng = other as LatLng
|
||||
return java.lang.Double.compare(latLng.altitude, altitude) == 0 && java.lang.Double.compare(latLng.latitude, latitude) == 0 && java.lang.Double.compare(latLng.longitude, longitude) == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return the hash code value
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
var temp: Long
|
||||
temp = java.lang.Double.doubleToLongBits(latitude)
|
||||
result = (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(longitude)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(altitude)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return the string representation
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "LatLng [latitude=$latitude, longitude=$longitude, altitude=$altitude]"
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable instance's marshaled representation.
|
||||
*
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeDouble(latitude)
|
||||
out.writeDouble(longitude)
|
||||
out.writeDouble(altitude)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points
|
||||
*
|
||||
* @param other Other LatLng to compare to
|
||||
* @return distance in meters
|
||||
*/
|
||||
fun distanceTo(other: LatLng): Double {
|
||||
return TurfMeasurement.distance(Point.fromLngLat(longitude, latitude), Point.fromLngLat(other.longitude, other.latitude), TurfConstants.UNIT_METRES)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Inner class responsible for recreating Parcels into objects.
|
||||
*/
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<LatLng> = object : Parcelable.Creator<LatLng> {
|
||||
override fun createFromParcel(parcel: Parcel): LatLng {
|
||||
return LatLng(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LatLng?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the given range (including min & max) via modular arithmetic.
|
||||
*
|
||||
*
|
||||
* Same formula as used in Core GL (wrap.hpp)
|
||||
* std::fmod((std::fmod((value - min), d) + d), d) + min;
|
||||
*
|
||||
*
|
||||
*
|
||||
* Multiples of max value will be wrapped to max.
|
||||
*
|
||||
*
|
||||
* @param value Value to wrap
|
||||
* @param min Minimum value
|
||||
* @param max Maximum value
|
||||
* @return Wrapped value
|
||||
*/
|
||||
fun wrap(value: Double, min: Double, max: Double): Double {
|
||||
val delta = max - min
|
||||
val firstMod = (value - min) % delta
|
||||
val secondMod = (firstMod + delta) % delta
|
||||
return if (value >= max && secondMod == 0.0) {
|
||||
max
|
||||
} else {
|
||||
secondMod + min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.Keep
|
||||
import org.maplibre.android.constants.GeometryConstants
|
||||
import org.maplibre.android.exceptions.InvalidLatLngBoundsException
|
||||
import org.maplibre.android.utils.isInfinite
|
||||
import org.maplibre.android.utils.isNaN
|
||||
|
||||
/**
|
||||
* A geographical area representing a latitude/longitude aligned rectangle.
|
||||
*
|
||||
*
|
||||
* This class does not wrap values to the world bounds.
|
||||
*
|
||||
*/
|
||||
|
||||
@Keep
|
||||
class LatLngBounds
|
||||
/**
|
||||
* Construct a new LatLngBounds based on its corners, given in NESW
|
||||
* order.
|
||||
*
|
||||
*
|
||||
* @since 7.0.0 LatLngBounds cannot be wrapped any more, i.e longitudeWest has to be
|
||||
* less or equal to longitudeEast.
|
||||
*
|
||||
* For example, to represent bounds spanning 20 degrees crossing antimeridian with
|
||||
* the NE point as (10, -170) and the SW point as (-10, 170),
|
||||
* use (10, -190) and (-10, -170), or (10, -170) and (-10, -150).
|
||||
*
|
||||
* @param latitudeNorth Northern Latitude
|
||||
* @param longitudeEast Eastern Longitude
|
||||
* @param latitudeSouth Southern Latitude
|
||||
* @param longitudeWest Western Longitude
|
||||
*/ @Keep internal constructor(
|
||||
/**
|
||||
* Get the north latitude value of this bounds.
|
||||
*
|
||||
* @return double latitude value for north
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val latitudeNorth: Double,
|
||||
/**
|
||||
* Get the east longitude value of this bounds.
|
||||
*
|
||||
* @return double longitude value for east
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val longitudeEast: Double,
|
||||
/**
|
||||
* Get the south latitude value of this bounds.
|
||||
*
|
||||
* @return double latitude value for south
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val latitudeSouth: Double,
|
||||
/**
|
||||
* Get the west longitude value of this bounds.
|
||||
*
|
||||
* @return double longitude value for west
|
||||
*/
|
||||
@field:Keep
|
||||
@JvmField
|
||||
val longitudeWest: Double
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Calculates the centerpoint of this LatLngBounds by simple interpolation and returns
|
||||
* it as a point. This is a non-geodesic calculation which is not the geographic center.
|
||||
*
|
||||
* @return LatLng center of this LatLngBounds
|
||||
*/
|
||||
val center: LatLng
|
||||
get() {
|
||||
val latCenter = (latitudeNorth + latitudeSouth) / 2.0
|
||||
val longCenter = (longitudeEast + longitudeWest) / 2.0
|
||||
return LatLng(latCenter, longCenter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the north latitude value of this bounds.
|
||||
*
|
||||
* @return double latitude value for north
|
||||
*/
|
||||
fun getLatNorth(): Double {
|
||||
return latitudeNorth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the south latitude value of this bounds.
|
||||
*
|
||||
* @return double latitude value for south
|
||||
*/
|
||||
fun getLatSouth(): Double {
|
||||
return latitudeSouth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the east longitude value of this bounds.
|
||||
*
|
||||
* @return double longitude value for east
|
||||
*/
|
||||
fun getLonEast(): Double {
|
||||
return longitudeEast
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the west longitude value of this bounds.
|
||||
*
|
||||
* @return double longitude value for west
|
||||
*/
|
||||
fun getLonWest(): Double {
|
||||
return longitudeWest
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latitude-longitude pair of the south west corner of this bounds.
|
||||
*
|
||||
* @return LatLng of the south west corner
|
||||
*/
|
||||
val southWest: LatLng
|
||||
get() = LatLng(latitudeSouth, longitudeWest)
|
||||
|
||||
/**
|
||||
* Get the latitude-longitude pair if the north east corner of this bounds.
|
||||
*
|
||||
* @return LatLng of the north east corner
|
||||
*/
|
||||
val northEast: LatLng
|
||||
get() = LatLng(latitudeNorth, longitudeEast)
|
||||
|
||||
/**
|
||||
* Get the latitude-longitude pair of the south east corner of this bounds.
|
||||
*
|
||||
* @return LatLng of the south east corner
|
||||
*/
|
||||
val southEast: LatLng
|
||||
get() = LatLng(latitudeSouth, longitudeEast)
|
||||
|
||||
/**
|
||||
* Get the latitude-longitude pair of the north west corner of this bounds.
|
||||
*
|
||||
* @return LatLng of the north west corner
|
||||
*/
|
||||
val northWest: LatLng
|
||||
get() = LatLng(latitudeNorth, longitudeWest)
|
||||
|
||||
/**
|
||||
* Get the area spanned by this LatLngBounds
|
||||
*
|
||||
* @return LatLngSpan area
|
||||
*/
|
||||
val span: LatLngSpan
|
||||
get() = LatLngSpan(latitudeSpan, longitudeSpan)
|
||||
|
||||
/**
|
||||
* Get the absolute distance, in degrees, between the north and
|
||||
* south boundaries of this LatLngBounds
|
||||
*
|
||||
* @return Span distance
|
||||
*/
|
||||
val latitudeSpan: Double
|
||||
get() = Math.abs(latitudeNorth - latitudeSouth)
|
||||
|
||||
/**
|
||||
* Get the absolute distance, in degrees, between the west and
|
||||
* east boundaries of this LatLngBounds
|
||||
*
|
||||
* @return Span distance
|
||||
*/
|
||||
val longitudeSpan: Double
|
||||
get() = Math.abs(longitudeEast - longitudeWest)
|
||||
|
||||
/**
|
||||
* Validate if LatLngBounds is empty, determined if absolute distance is
|
||||
*
|
||||
* @return boolean indicating if span is empty
|
||||
*/
|
||||
val isEmptySpan: Boolean
|
||||
get() = longitudeSpan == 0.0 || latitudeSpan == 0.0
|
||||
|
||||
/**
|
||||
* Returns a string representaton of the object.
|
||||
*
|
||||
* @return the string representation
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return ("N:" + latitudeNorth + "; E:" + longitudeEast + "; S:" + latitudeSouth + "; W:" + longitudeWest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of LatLng objects resembling this bounds.
|
||||
*
|
||||
* @return an array of 2 LatLng objects.
|
||||
*/
|
||||
fun toLatLngs(): Array<LatLng> {
|
||||
return arrayOf(northEast, southWest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a LatLngBounds from current bounds with an additional latitude-longitude pair.
|
||||
*
|
||||
* @param latLng the latitude lognitude pair to include in the bounds.
|
||||
* @return the newly constructed bounds
|
||||
*/
|
||||
fun include(latLng: LatLng): LatLngBounds {
|
||||
return Builder().include(northEast).include(southWest).include(latLng).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this LatLngBounds matches another one via LatLng.
|
||||
*
|
||||
* @param other another object
|
||||
* @return a boolean indicating whether the LatLngBounds are equal
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other is LatLngBounds) {
|
||||
return latitudeNorth == other.latitudeNorth && latitudeSouth == other.latitudeSouth && longitudeEast == other.longitudeEast && longitudeWest == other.longitudeWest
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun containsLatitude(latitude: Double): Boolean {
|
||||
return latitude <= latitudeNorth && latitude >= latitudeSouth
|
||||
}
|
||||
|
||||
private fun containsLongitude(longitude: Double): Boolean {
|
||||
return longitude <= longitudeEast && longitude >= longitudeWest
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this LatLngBounds contains a point.
|
||||
*
|
||||
* @param latLng the point which may be contained
|
||||
* @return true, if the point is contained within the bounds
|
||||
*/
|
||||
operator fun contains(latLng: LatLng): Boolean {
|
||||
return (containsLatitude(latLng.latitude) && containsLongitude(latLng.longitude))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this LatLngBounds contains another bounds.
|
||||
*
|
||||
* @param other the bounds which may be contained
|
||||
* @return true, if the bounds is contained within the bounds
|
||||
*/
|
||||
operator fun contains(other: LatLngBounds): Boolean {
|
||||
return (contains(other.northEast) && contains(other.southWest))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new LatLngBounds that stretches to contain both this and another LatLngBounds.
|
||||
*
|
||||
* @param bounds LatLngBounds to add
|
||||
* @return LatLngBounds
|
||||
*/
|
||||
fun union(bounds: LatLngBounds): LatLngBounds {
|
||||
return unionNoParamCheck(bounds.latitudeNorth, bounds.longitudeEast, bounds.latitudeSouth, bounds.longitudeWest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new LatLngBounds that stretches to contain both this and another LatLngBounds.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This values of northLat and southLat should be in the range of [-90, 90],
|
||||
* see [GeometryConstants.MIN_LATITUDE] and [GeometryConstants.MAX_LATITUDE],
|
||||
* otherwise IllegalArgumentException will be thrown.
|
||||
* northLat should be greater or equal southLat, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
*
|
||||
*
|
||||
* eastLon should be greater or equal westLon, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
* @param northLat Northern Latitude corner point
|
||||
* @param eastLon Eastern Longitude corner point
|
||||
* @param southLat Southern Latitude corner point
|
||||
* @param westLon Western Longitude corner point
|
||||
* @return LatLngBounds
|
||||
*/
|
||||
fun union(northLat: Double, eastLon: Double, southLat: Double, westLon: Double): LatLngBounds {
|
||||
checkParams(northLat, eastLon, southLat, westLon)
|
||||
return unionNoParamCheck(northLat, eastLon, southLat, westLon)
|
||||
}
|
||||
|
||||
private fun unionNoParamCheck(northLat: Double, eastLon: Double, southLat: Double, westLon: Double): LatLngBounds {
|
||||
return LatLngBounds(
|
||||
if (latitudeNorth < northLat) northLat else latitudeNorth,
|
||||
if (longitudeEast < eastLon) eastLon else longitudeEast,
|
||||
if (latitudeSouth > southLat) southLat else latitudeSouth,
|
||||
if (longitudeWest > westLon) westLon else longitudeWest
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new LatLngBounds that is the intersection of this with another LatLngBounds,
|
||||
*
|
||||
* @param box LatLngBounds to intersect with
|
||||
* @return LatLngBounds
|
||||
*/
|
||||
fun intersect(box: LatLngBounds): LatLngBounds? {
|
||||
return intersectNoParamCheck(box.latitudeNorth, box.longitudeEast, box.latitudeSouth, box.longitudeWest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new LatLngBounds that is the intersection of this with another box.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This values of northLat and southLat should be in the range of [-90, 90],
|
||||
* see [GeometryConstants.MIN_LATITUDE] and [GeometryConstants.MAX_LATITUDE],
|
||||
* otherwise IllegalArgumentException will be thrown.
|
||||
* northLat should be greater or equal southLat, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
*
|
||||
*
|
||||
* eastLon should be greater or equal westLon, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
* @param northLat Northern Latitude corner point
|
||||
* @param eastLon Eastern Longitude corner point
|
||||
* @param southLat Southern Latitude corner point
|
||||
* @param westLon Western Longitude corner point
|
||||
* @return LatLngBounds
|
||||
*/
|
||||
fun intersect(northLat: Double, eastLon: Double, southLat: Double, westLon: Double): LatLngBounds {
|
||||
checkParams(northLat, eastLon, southLat, westLon)
|
||||
return intersectNoParamCheck(northLat, eastLon, southLat, westLon)!!
|
||||
}
|
||||
|
||||
private fun intersectNoParamCheck(northLat: Double, eastLon: Double, southLat: Double, westLon: Double): LatLngBounds? {
|
||||
val minLonWest = Math.max(longitudeWest, westLon)
|
||||
val maxLonEast = Math.min(longitudeEast, eastLon)
|
||||
if (maxLonEast >= minLonWest) {
|
||||
val minLatSouth = Math.max(latitudeSouth, southLat)
|
||||
val maxLatNorth = Math.min(latitudeNorth, northLat)
|
||||
if (maxLatNorth >= minLatSouth) {
|
||||
return LatLngBounds(maxLatNorth, maxLonEast, minLatSouth, minLonWest)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return the hash code
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
return (latitudeNorth + 90 + (latitudeSouth + 90) * 1000 + (longitudeEast + 180) * 1000000 + (longitudeWest + 180) * 1000000000).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable instance's marshaled representation.
|
||||
*
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeDouble(latitudeNorth)
|
||||
out.writeDouble(longitudeEast)
|
||||
out.writeDouble(latitudeSouth)
|
||||
out.writeDouble(longitudeWest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for composing LatLngBounds objects.
|
||||
*/
|
||||
class Builder {
|
||||
private val latLngList: MutableList<LatLng> = ArrayList()
|
||||
|
||||
/**
|
||||
* Builds a new LatLngBounds.
|
||||
*
|
||||
*
|
||||
* Throws an [InvalidLatLngBoundsException] when no LatLngBounds can be created.
|
||||
*
|
||||
*
|
||||
* @return the build LatLngBounds
|
||||
*/
|
||||
fun build(): LatLngBounds {
|
||||
if (latLngList.size < 2) {
|
||||
throw InvalidLatLngBoundsException(latLngList.size)
|
||||
}
|
||||
return fromLatLngs(latLngList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a LatLng object to the LatLngBounds.Builder.
|
||||
*
|
||||
* @param latLngs the List of LatLng objects to be added
|
||||
* @return this
|
||||
*/
|
||||
fun includes(latLngs: List<LatLng>): Builder {
|
||||
latLngList.addAll(latLngs)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a LatLng object to the LatLngBounds.Builder.
|
||||
*
|
||||
* @param latLng the LatLng to be added
|
||||
* @return this
|
||||
*/
|
||||
fun include(latLng: LatLng): Builder {
|
||||
latLngList.add(latLng)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns the world bounds.
|
||||
*
|
||||
* @return the bounds representing the world
|
||||
*/
|
||||
@JvmStatic
|
||||
fun world(): LatLngBounds {
|
||||
return from(GeometryConstants.MAX_LATITUDE, GeometryConstants.MAX_WRAP_LONGITUDE, GeometryConstants.MIN_LATITUDE, GeometryConstants.MIN_WRAP_LONGITUDE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a LatLngBounds that contains all of a list of LatLng
|
||||
* objects. Empty lists will yield invalid LatLngBounds.
|
||||
*
|
||||
* @param latLngs List of LatLng objects
|
||||
* @return LatLngBounds
|
||||
*/
|
||||
fun fromLatLngs(latLngs: List<LatLng>): LatLngBounds {
|
||||
var minLat = GeometryConstants.MAX_LATITUDE
|
||||
var minLon = GeometryConstants.MAX_LONGITUDE
|
||||
var maxLat = GeometryConstants.MIN_LATITUDE
|
||||
var maxLon = GeometryConstants.MIN_LONGITUDE
|
||||
for (gp in latLngs) {
|
||||
val latitude = gp.latitude
|
||||
val longitude = gp.longitude
|
||||
minLat = Math.min(minLat, latitude)
|
||||
minLon = Math.min(minLon, longitude)
|
||||
maxLat = Math.max(maxLat, latitude)
|
||||
maxLon = Math.max(maxLon, longitude)
|
||||
}
|
||||
return LatLngBounds(maxLat, maxLon, minLat, minLon)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a LatLngBounds from doubles representing a LatLng pair.
|
||||
*
|
||||
*
|
||||
* This values of latNorth and latSouth should be in the range of [-90, 90],
|
||||
* see [GeometryConstants.MIN_LATITUDE] and [GeometryConstants.MAX_LATITUDE],
|
||||
* otherwise IllegalArgumentException will be thrown.
|
||||
* latNorth should be greater or equal latSouth, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
*
|
||||
* This method doesn't recalculate most east or most west boundaries.
|
||||
* Note @since 7.0.0 lonEast and lonWest will NOT be wrapped to be in the range of [-180, 180],
|
||||
* see [GeometryConstants.MIN_LONGITUDE] and [GeometryConstants.MAX_LONGITUDE]
|
||||
* lonEast should be greater or equal lonWest, otherwise IllegalArgumentException will be thrown.
|
||||
*
|
||||
*/
|
||||
@JvmStatic
|
||||
fun from(
|
||||
@FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) latNorth: Double,
|
||||
lonEast: Double,
|
||||
@FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) latSouth: Double,
|
||||
lonWest: Double
|
||||
): LatLngBounds {
|
||||
checkParams(latNorth, lonEast, latSouth, lonWest)
|
||||
return LatLngBounds(latNorth, lonEast, latSouth, lonWest)
|
||||
}
|
||||
|
||||
private fun checkParams(
|
||||
@FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) latNorth: Double,
|
||||
lonEast: Double,
|
||||
@FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) latSouth: Double,
|
||||
lonWest: Double
|
||||
) {
|
||||
require(!(Double.isNaN(latNorth) || Double.isNaN(latSouth))) { "latitude must not be NaN" }
|
||||
require(!(Double.isNaN(lonEast) || Double.isNaN(lonWest))) { "longitude must not be NaN" }
|
||||
require(!(Double.isInfinite(lonEast) || Double.isInfinite(lonWest))) { "longitude must not be infinite" }
|
||||
require(
|
||||
!(latNorth > GeometryConstants.MAX_LATITUDE || latNorth < GeometryConstants.MIN_LATITUDE || latSouth > GeometryConstants.MAX_LATITUDE || latSouth < GeometryConstants.MIN_LATITUDE)
|
||||
) { "latitude must be between -90 and 90" }
|
||||
require(latNorth >= latSouth) { "latNorth cannot be less than latSouth" }
|
||||
require(lonEast >= lonWest) { "lonEast cannot be less than lonWest" }
|
||||
}
|
||||
|
||||
private fun lat_(z: Int, y: Int): Double {
|
||||
val n = Math.PI - 2.0 * Math.PI * y / Math.pow(2.0, z.toDouble())
|
||||
return Math.toDegrees(Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))))
|
||||
}
|
||||
|
||||
private fun lon_(z: Int, x: Int): Double {
|
||||
return x / Math.pow(2.0, z.toDouble()) * 360.0 - GeometryConstants.MAX_WRAP_LONGITUDE
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a LatLngBounds from a Tile identifier.
|
||||
*
|
||||
*
|
||||
* Returned bounds will have latitude in the range of Mercator projection.
|
||||
*
|
||||
* @param z Tile zoom level.
|
||||
* @param x Tile X coordinate.
|
||||
* @param y Tile Y coordinate.
|
||||
* @see GeometryConstants.MIN_MERCATOR_LATITUDE
|
||||
*
|
||||
* @see GeometryConstants.MAX_MERCATOR_LATITUDE
|
||||
*/
|
||||
@JvmStatic
|
||||
fun from(z: Int, x: Int, y: Int): LatLngBounds {
|
||||
return LatLngBounds(lat_(z, y), lon_(z, x + 1), lat_(z, y + 1), lon_(z, x))
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class responsible for recreating Parcels into objects.
|
||||
*/
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<LatLngBounds?> = object : Parcelable.Creator<LatLngBounds?> {
|
||||
override fun createFromParcel(parcel: Parcel): LatLngBounds {
|
||||
return readFromParcel(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LatLngBounds?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFromParcel(parcel: Parcel): LatLngBounds {
|
||||
val northLat = parcel.readDouble()
|
||||
val eastLon = parcel.readDouble()
|
||||
val southLat = parcel.readDouble()
|
||||
val westLon = parcel.readDouble()
|
||||
return LatLngBounds(northLat, eastLon, southLat, westLon)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* A geographical area representing a non-aligned quadrilateral
|
||||
*
|
||||
*
|
||||
* This class does not wrap values to the world bounds
|
||||
*
|
||||
*/
|
||||
class LatLngQuad
|
||||
/**
|
||||
* Construct a new LatLngQuad based on its corners,
|
||||
* in order top left, top right, bottom left, bottom right
|
||||
*/
|
||||
@Keep constructor(@field:Keep val topLeft: LatLng, @field:Keep val topRight: LatLng, @field:Keep val bottomRight: LatLng, @field:Keep val bottomLeft: LatLng) : Parcelable {
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, arg1: Int) {
|
||||
topLeft.writeToParcel(out, arg1)
|
||||
topRight.writeToParcel(out, arg1)
|
||||
bottomRight.writeToParcel(out, arg1)
|
||||
bottomLeft.writeToParcel(out, arg1)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LatLngQuad
|
||||
|
||||
if (topLeft != other.topLeft) return false
|
||||
if (topRight != other.topRight) return false
|
||||
if (bottomRight != other.bottomRight) return false
|
||||
if (bottomLeft != other.bottomLeft) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = topLeft.hashCode()
|
||||
result = 31 * result + topRight.hashCode()
|
||||
result = 31 * result + bottomRight.hashCode()
|
||||
result = 31 * result + bottomLeft.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<LatLngQuad> = object : Parcelable.Creator<LatLngQuad> {
|
||||
override fun createFromParcel(parcel: Parcel): LatLngQuad {
|
||||
return readFromParcel(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LatLngQuad?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFromParcel(parcel: Parcel): LatLngQuad {
|
||||
val topLeft = LatLng(parcel)
|
||||
val topRight = LatLng(parcel)
|
||||
val bottomRight = LatLng(parcel)
|
||||
val bottomLeft = LatLng(parcel)
|
||||
return LatLngQuad(topLeft, topRight, bottomRight, bottomLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
/**
|
||||
* A geographical span defined by its latitude and longitude span.
|
||||
*/
|
||||
class LatLngSpan : Parcelable {
|
||||
/**
|
||||
* Returns the latitude span.
|
||||
*
|
||||
* @return The latitude span.
|
||||
*/
|
||||
/**
|
||||
* Sets the latitude span.
|
||||
*
|
||||
* @var latitudeSpan The latitude span to set.
|
||||
*/
|
||||
var latitudeSpan: Double
|
||||
/**
|
||||
* Returns to longitude span.
|
||||
*
|
||||
* @return The longitude span.
|
||||
*/
|
||||
/**
|
||||
* Sets the longitude span.
|
||||
*
|
||||
* @var longitudeSpan The longitude span to set.
|
||||
*/
|
||||
var longitudeSpan: Double
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
latitudeSpan = parcel.readDouble()
|
||||
longitudeSpan = parcel.readDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LatLgnSpan.
|
||||
*
|
||||
* @param latitudeSpan The span used for latitude.
|
||||
* @param longitudeSpan The span used for longitude.
|
||||
*/
|
||||
constructor(latitudeSpan: Double, longitudeSpan: Double) {
|
||||
this.latitudeSpan = latitudeSpan
|
||||
this.longitudeSpan = longitudeSpan
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*
|
||||
* @param other The object to compare
|
||||
* @return True if equal, false if not
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other is LatLngSpan) {
|
||||
return (longitudeSpan == other.longitudeSpan && latitudeSpan == other.latitudeSpan)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable instance's marshaled representation.
|
||||
*
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out Parcel in which the object should be written
|
||||
* @param flags Additional flags about how the object should be written
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeDouble(latitudeSpan)
|
||||
out.writeDouble(longitudeSpan)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return hash code value of this
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
var temp: Long
|
||||
temp = java.lang.Double.doubleToLongBits(latitudeSpan)
|
||||
result = (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(longitudeSpan)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Inner class responsible for recreating Parcels into objects.
|
||||
*/
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<LatLngSpan> = object : Parcelable.Creator<LatLngSpan> {
|
||||
override fun createFromParcel(parcel: Parcel): LatLngSpan {
|
||||
return LatLngSpan(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LatLngSpan?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains the MapLibre Maps Android Geometry API classes.
|
||||
*/
|
||||
package org.maplibre.android.geometry
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* ProjectedMeters is a projection of longitude, latitude points in Mercator meters.
|
||||
*
|
||||
*
|
||||
* these have been projected into MapLibre GL's Mercator projection. Instead of decimal
|
||||
* degrees, it uses Mercator meters (which are notably not equivalent to SI meters)
|
||||
* except at the equator.
|
||||
*
|
||||
*/
|
||||
class ProjectedMeters : Parcelable {
|
||||
/**
|
||||
* Get projected meters in north direction.
|
||||
*
|
||||
* @return Projected meters in north.
|
||||
*/
|
||||
var northing: Double
|
||||
private set
|
||||
|
||||
/**
|
||||
* Get projected meters in east direction.
|
||||
*
|
||||
* @return Projected meters in east.
|
||||
*/
|
||||
var easting: Double
|
||||
private set
|
||||
|
||||
/**
|
||||
* Creates a ProjectedMeters based on projected meters in north and east direction.
|
||||
*
|
||||
* @param northing the northing in meters
|
||||
* @param easting the easting in meters
|
||||
*/
|
||||
@Keep
|
||||
constructor(northing: Double, easting: Double) {
|
||||
this.northing = northing
|
||||
this.easting = easting
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ProjecteMeters based on another set of projected meters.
|
||||
*
|
||||
* @param projectedMeters The projected meters to be based on.
|
||||
*/
|
||||
constructor(projectedMeters: ProjectedMeters) {
|
||||
northing = projectedMeters.northing
|
||||
easting = projectedMeters.easting
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ProjectedMeters from a Parcel.
|
||||
*
|
||||
* @param `in` The parcel to create from
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
private constructor(parcel: Parcel) {
|
||||
northing = parcel.readDouble()
|
||||
easting = parcel.readDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*
|
||||
* @param other The object to compare this to
|
||||
* @return true if equal, false if not
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null || javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val projectedMeters = other as ProjectedMeters
|
||||
return (java.lang.Double.compare(projectedMeters.easting, easting) == 0 && java.lang.Double.compare(projectedMeters.northing, northing) == 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return the hash code of this
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result: Int
|
||||
var temp: Long
|
||||
temp = java.lang.Double.doubleToLongBits(easting)
|
||||
result = (temp xor (temp ushr 32)).toInt()
|
||||
temp = java.lang.Double.doubleToLongBits(northing)
|
||||
result = 31 * result + (temp xor (temp ushr 32)).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return the string representation of this
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "ProjectedMeters [northing=$northing, easting=$easting]"
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable instance's marshaled representation.
|
||||
*
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeDouble(northing)
|
||||
out.writeDouble(easting)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Inner class responsible for recreating Parcels into objects.
|
||||
*/
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<ProjectedMeters> = object : Parcelable.Creator<ProjectedMeters> {
|
||||
override fun createFromParcel(parcel: Parcel): ProjectedMeters {
|
||||
return ProjectedMeters(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<ProjectedMeters?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.maplibre.android.geometry
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
/**
|
||||
* Contains the four points defining the four-sided polygon that is visible in a map's camera.
|
||||
* This polygon can be a trapezoid instead of a rectangle, because a camera can have tilt.
|
||||
* If the camera is directly over the center of the camera, the shape is rectangular,
|
||||
* but if the camera is tilted, the shape will appear to be a trapezoid whose
|
||||
* smallest side is closest to the point of view.
|
||||
*/
|
||||
class VisibleRegion : Parcelable {
|
||||
/**
|
||||
* LatLng object that defines the far left corner of the camera.
|
||||
*/
|
||||
@JvmField
|
||||
val farLeft: LatLng?
|
||||
|
||||
/**
|
||||
* LatLng object that defines the far right corner of the camera.
|
||||
*/
|
||||
@JvmField
|
||||
val farRight: LatLng?
|
||||
|
||||
/**
|
||||
* LatLng object that defines the bottom left corner of the camera.
|
||||
*/
|
||||
@JvmField
|
||||
val nearLeft: LatLng?
|
||||
|
||||
/**
|
||||
* LatLng object that defines the bottom right corner of the camera.
|
||||
*/
|
||||
@JvmField
|
||||
val nearRight: LatLng?
|
||||
|
||||
/**
|
||||
* The smallest bounding box that includes the visible region defined in this class.
|
||||
*/
|
||||
@JvmField
|
||||
val latLngBounds: LatLngBounds
|
||||
|
||||
/**
|
||||
* Creates a VisibleRegion from a Parcel.
|
||||
*
|
||||
* @param `in` The Parcel to create this from
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private constructor(parcel: Parcel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
farLeft = parcel.readParcelable<LatLng>(LatLng::class.java.classLoader, LatLng::class.java)
|
||||
farRight = parcel.readParcelable(LatLng::class.java.classLoader, LatLng::class.java)
|
||||
nearLeft = parcel.readParcelable(LatLng::class.java.classLoader, LatLng::class.java)
|
||||
nearRight = parcel.readParcelable(LatLng::class.java.classLoader, LatLng::class.java)
|
||||
latLngBounds = parcel.readParcelable(LatLngBounds::class.java.classLoader, LatLngBounds::class.java)!!
|
||||
} else {
|
||||
farLeft = parcel.readParcelable<LatLng>(LatLng::class.java.classLoader)
|
||||
farRight = parcel.readParcelable(LatLng::class.java.classLoader)
|
||||
nearLeft = parcel.readParcelable(LatLng::class.java.classLoader)
|
||||
nearRight = parcel.readParcelable(LatLng::class.java.classLoader)
|
||||
latLngBounds = parcel.readParcelable(LatLngBounds::class.java.classLoader)!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a VisibleRegion given the four corners of the camera.
|
||||
*
|
||||
* @param farLeft A LatLng object containing the latitude and longitude of the far left corner of the region.
|
||||
* @param farRight A LatLng object containing the latitude and longitude of the far right corner of the region.
|
||||
* @param nearLeft A LatLng object containing the latitude and longitude of the near left corner of the region.
|
||||
* @param nearRight A LatLng object containing the latitude and longitude of the near right corner of the region.
|
||||
* @param latLngBounds The smallest bounding box that includes the visible region defined in this class.
|
||||
*/
|
||||
constructor(farLeft: LatLng, farRight: LatLng, nearLeft: LatLng, nearRight: LatLng, latLngBounds: LatLngBounds) {
|
||||
this.farLeft = farLeft
|
||||
this.farRight = farRight
|
||||
this.nearLeft = nearLeft
|
||||
this.nearRight = nearRight
|
||||
this.latLngBounds = latLngBounds
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this VisibleRegion to another object.
|
||||
* If the other object is actually a pointer to this object,
|
||||
* or if all four corners and the bounds of the two objects are the same,
|
||||
* this method returns true. Otherwise, this method returns false.
|
||||
*
|
||||
* @param other The Object to compare with.
|
||||
* @return true if both objects are the same object.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is VisibleRegion) {
|
||||
return false
|
||||
}
|
||||
if (other === this) {
|
||||
return true
|
||||
}
|
||||
return farLeft == other.farLeft && farRight == other.farRight && nearLeft == other.nearLeft && nearRight == other.nearRight && latLngBounds == other.latLngBounds
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of the object.
|
||||
*
|
||||
* @return the string representation of this
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return ("[farLeft [" + farLeft + "], farRight [" + farRight + "], nearLeft [" + nearLeft + "], nearRight [" + nearRight + "], latLngBounds [" + latLngBounds + "]]")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return the hash code
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
return farLeft.hashCode() + 90 + (farRight.hashCode() + 90) * 1000 + (nearLeft.hashCode() + 180) * 1000000 + (nearRight.hashCode() + 180) * 1000000000
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable instance's marshaled representation.
|
||||
*
|
||||
* @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance.
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten this object in to a Parcel.
|
||||
*
|
||||
* @param out The Parcel in which the object should be written.
|
||||
* @param flags Additional flags about how the object should be written
|
||||
*/
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
out.writeParcelable(farLeft, flags)
|
||||
out.writeParcelable(farRight, flags)
|
||||
out.writeParcelable(nearLeft, flags)
|
||||
out.writeParcelable(nearRight, flags)
|
||||
out.writeParcelable(latLngBounds, flags)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Inner class responsible for recreating Parcels into objects.
|
||||
*/
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<VisibleRegion> = object : Parcelable.Creator<VisibleRegion> {
|
||||
override fun createFromParcel(parcel: Parcel): VisibleRegion {
|
||||
return VisibleRegion(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<VisibleRegion?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.MapStrictMode;
|
||||
import org.maplibre.android.MapLibre;
|
||||
|
||||
public class HttpIdentifier {
|
||||
|
||||
private HttpIdentifier() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application identifier, consisting out the package name, version name and version code.
|
||||
*
|
||||
* @return the application identifier
|
||||
*/
|
||||
public static String getIdentifier() {
|
||||
return getIdentifier(MapLibre.getApplicationContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application identifier, consisting out the package name, version name and version code.
|
||||
*
|
||||
* @param context the context used to retrieve the package manager from
|
||||
* @return the application identifier
|
||||
*/
|
||||
private static String getIdentifier(@NonNull Context context) {
|
||||
try {
|
||||
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
return String.format("%s/%s (%s)", context.getPackageName(), packageInfo.versionName, packageInfo.versionCode);
|
||||
} catch (Exception exception) {
|
||||
MapStrictMode.strictModeViolation(exception);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.maplibre.android.log.Logger;
|
||||
|
||||
import static org.maplibre.android.http.HttpRequest.CONNECTION_ERROR;
|
||||
import static org.maplibre.android.http.HttpRequest.TEMPORARY_ERROR;
|
||||
|
||||
public class HttpLogger {
|
||||
|
||||
private static final String TAG = "Mbgl-HttpRequest";
|
||||
|
||||
public static boolean logRequestUrl;
|
||||
public static boolean logEnabled = true;
|
||||
|
||||
private HttpLogger(){
|
||||
}
|
||||
|
||||
public static void logFailure(int type, String errorMessage, String requestUrl) {
|
||||
log(type == TEMPORARY_ERROR ? Log.DEBUG : type == CONNECTION_ERROR ? Log.INFO : Log.WARN,
|
||||
String.format(
|
||||
"Request failed due to a %s error: %s %s",
|
||||
type == TEMPORARY_ERROR ? "temporary" : type == CONNECTION_ERROR ? "connection" : "permanent",
|
||||
errorMessage,
|
||||
logRequestUrl ? requestUrl : ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static void log(int type, String errorMessage) {
|
||||
if (logEnabled) {
|
||||
Logger.log(type, TAG, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
/**
|
||||
* Interface definition for performing http requests.
|
||||
* <p>
|
||||
* This allows to provide alternative implementations for the http interaction of this library.
|
||||
* </p>
|
||||
*/
|
||||
public interface HttpRequest {
|
||||
|
||||
int CONNECTION_ERROR = 0;
|
||||
int TEMPORARY_ERROR = 1;
|
||||
int PERMANENT_ERROR = 2;
|
||||
|
||||
/**
|
||||
* Executes the request.
|
||||
*
|
||||
* @param httpRequest callback to be invoked when we receive a response
|
||||
* @param nativePtr the pointer associated to the request
|
||||
* @param resourceUrl the resource url to download
|
||||
* @param etag http header, identifier for a specific version of a resource
|
||||
* @param modified http header, used to determine if a resource hasn't been modified since
|
||||
* @param offlineUsage flag to indicate a resource will be used for offline, appends offline=true as a query parameter
|
||||
*/
|
||||
void executeRequest(HttpResponder httpRequest, long nativePtr, String resourceUrl,
|
||||
String etag, String modified, boolean offlineUsage);
|
||||
|
||||
/**
|
||||
* Cancels the request.
|
||||
*/
|
||||
void cancelRequest();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class HttpRequestUrl {
|
||||
|
||||
private HttpRequestUrl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a resource request url based on the host, query size, and offline requirement.
|
||||
* MapLibre resources downloaded for offline use are subject to separate Vector Tile and
|
||||
* Raster Tile API pricing and are not included in the Maps SDK’s “unlimited” requests.
|
||||
* See <a href="https://www.mapbox.com/pricing">our pricing page</a> for more information.
|
||||
*
|
||||
* @param host the host used as endpoint
|
||||
* @param resourceUrl the resource to download
|
||||
* @param querySize the query size of the resource request
|
||||
* @param offline the type of resource, either offline or online
|
||||
* @return the adapted resource url
|
||||
*/
|
||||
public static String buildResourceUrl(@NonNull String host, String resourceUrl, int querySize, boolean offline) {
|
||||
if (isValidMapboxEndpoint(host)) {
|
||||
if (querySize == 0) {
|
||||
resourceUrl = resourceUrl + "?";
|
||||
} else {
|
||||
resourceUrl = resourceUrl + "&";
|
||||
}
|
||||
// Only add SKU token to requests not tagged as "offline" usage.
|
||||
if (offline) {
|
||||
resourceUrl = resourceUrl + "offline=true";
|
||||
}
|
||||
}
|
||||
return resourceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the host used as endpoint is a valid MapLibre endpoint.
|
||||
*
|
||||
* @param host the host used as endpoint
|
||||
* @return true if a valid MapLibre endpoint
|
||||
*/
|
||||
private static boolean isValidMapboxEndpoint(String host) {
|
||||
return host.equals("mapbox.com")
|
||||
|| host.endsWith(".mapbox.com")
|
||||
|| host.equals("mapbox.cn")
|
||||
|| host.endsWith(".mapbox.cn");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when either a response was returned for a requested resource or
|
||||
* when an error occurred when requesting the resource.
|
||||
*/
|
||||
public interface HttpResponder {
|
||||
|
||||
/**
|
||||
* Invoked when a resource has finished.
|
||||
*
|
||||
* @param responseCode http response code
|
||||
* @param eTag http header, identifier for a specific version of a resource
|
||||
* @param lastModified http header, used to determine if a resource hasn't been modified since
|
||||
* @param cacheControl http header, used to determine cache strategy of a resource
|
||||
* @param expires http header, used to determine when a resource is stale
|
||||
* @param retryAfter http header, used to indicate when the service is expected to be unavailable to the client
|
||||
* @param xRateLimitReset http header, used to determine the remaining window before the rate limit resets
|
||||
* @param body http response body, in an array of bytes representation
|
||||
*/
|
||||
void onResponse(int responseCode, String eTag, String lastModified, String cacheControl, String expires,
|
||||
String retryAfter, String xRateLimitReset, byte[] body);
|
||||
|
||||
/**
|
||||
* Invoked when a resource failed to be retrieved.
|
||||
*
|
||||
* @param type the error type, either one of {@link HttpRequest#CONNECTION_ERROR},
|
||||
* {@link HttpRequest#TEMPORARY_ERROR} or {@link HttpRequest#PERMANENT_ERROR}
|
||||
* @param errorMessage the error message associated with the failure
|
||||
*/
|
||||
void handleFailure(int type, String errorMessage);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.MapStrictMode;
|
||||
import org.maplibre.android.MapLibre;
|
||||
import org.maplibre.android.log.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class LocalRequestTask extends AsyncTask<String, Void, byte[]> {
|
||||
|
||||
private static final String TAG = "Mbgl-LocalRequestTask";
|
||||
|
||||
private OnLocalRequestResponse requestResponse;
|
||||
|
||||
LocalRequestTask(OnLocalRequestResponse requestResponse) {
|
||||
this.requestResponse = requestResponse;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected byte[] doInBackground(String... strings) {
|
||||
return loadFile(MapLibre.getApplicationContext().getAssets(),
|
||||
"integration/" + strings[0]
|
||||
.substring(8)
|
||||
.replaceAll("%20", " ")
|
||||
.replaceAll("%2c", ","));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable byte[] bytes) {
|
||||
super.onPostExecute(bytes);
|
||||
if (bytes != null && requestResponse != null) {
|
||||
requestResponse.onResponse(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] loadFile(AssetManager assets, @NonNull String path) {
|
||||
byte[] buffer = null;
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = assets.open(path);
|
||||
int size = input.available();
|
||||
buffer = new byte[size];
|
||||
input.read(buffer);
|
||||
} catch (IOException exception) {
|
||||
logFileError(exception);
|
||||
} finally {
|
||||
if (input != null) {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException exception) {
|
||||
logFileError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void logFileError(Exception exception) {
|
||||
String message = "Load file failed";
|
||||
Logger.e(TAG, message, exception);
|
||||
MapStrictMode.strictModeViolation(message, exception);
|
||||
}
|
||||
|
||||
public interface OnLocalRequestResponse {
|
||||
void onResponse(byte[] bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.maplibre.android.http;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.MapLibre;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Keep
|
||||
public class NativeHttpRequest implements HttpResponder {
|
||||
|
||||
private final HttpRequest httpRequest = MapLibre.getModuleProvider().createHttpRequest();
|
||||
|
||||
// Reentrancy is not needed, but "Lock" is an abstract class.
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
@Keep
|
||||
private long nativePtr;
|
||||
|
||||
@Keep
|
||||
private NativeHttpRequest(long nativePtr, String resourceUrl, String etag, String modified, boolean offlineUsage) {
|
||||
this.nativePtr = nativePtr;
|
||||
|
||||
if (resourceUrl.startsWith("local://")) {
|
||||
// used by render test to serve files from assets
|
||||
executeLocalRequest(resourceUrl);
|
||||
return;
|
||||
}
|
||||
httpRequest.executeRequest(this, nativePtr, resourceUrl, etag, modified, offlineUsage);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
httpRequest.cancelRequest();
|
||||
|
||||
// TODO: We need a lock here because we can try
|
||||
// to cancel at the same time the request is getting
|
||||
// answered on the OkHTTP thread. We could get rid of
|
||||
// this lock by using Runnable when we move Android
|
||||
// implementation of mbgl::RunLoop to Looper.
|
||||
lock.lock();
|
||||
nativePtr = 0;
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
public void onResponse(int responseCode, String etag, String lastModified, String cacheControl, String expires,
|
||||
String retryAfter, String xRateLimitReset, byte[] body) {
|
||||
lock.lock();
|
||||
if (nativePtr != 0) {
|
||||
nativeOnResponse(responseCode,
|
||||
etag,
|
||||
lastModified,
|
||||
cacheControl,
|
||||
expires,
|
||||
retryAfter,
|
||||
xRateLimitReset,
|
||||
body);
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
private void executeLocalRequest(String resourceUrl) {
|
||||
new LocalRequestTask(new LocalRequestTask.OnLocalRequestResponse() {
|
||||
@Override
|
||||
public void onResponse(@Nullable byte[] bytes) {
|
||||
if (bytes != null) {
|
||||
lock.lock();
|
||||
if (nativePtr != 0) {
|
||||
NativeHttpRequest.this.nativeOnResponse(200, null, null, null, null, null, null, bytes);
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}).execute(resourceUrl);
|
||||
}
|
||||
|
||||
public void handleFailure(int type, String errorMessage) {
|
||||
lock.lock();
|
||||
if (nativePtr != 0) {
|
||||
nativeOnFailure(type, errorMessage);
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@Keep
|
||||
private native void nativeOnFailure(int type, String message);
|
||||
|
||||
@Keep
|
||||
private native void nativeOnResponse(int code, String etag, String modified, String cacheControl, String expires,
|
||||
String retryAfter, String xRateLimitReset, byte[] body);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Do not use this package. Internal use only.
|
||||
*/
|
||||
package org.maplibre.android.http;
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
class AnimatorListenerHolder {
|
||||
@MapLibreAnimator.Type
|
||||
private final int animatorType;
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener listener;
|
||||
|
||||
AnimatorListenerHolder(@MapLibreAnimator.Type int animatorType,
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener) {
|
||||
this.animatorType = animatorType;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@MapLibreAnimator.Type
|
||||
public int getAnimatorType() {
|
||||
return animatorType;
|
||||
}
|
||||
|
||||
public MapLibreAnimator.AnimationsValueChangeListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AnimatorListenerHolder that = (AnimatorListenerHolder) o;
|
||||
|
||||
if (animatorType != that.animatorType) {
|
||||
return false;
|
||||
}
|
||||
return listener != null ? listener.equals(that.listener) : that.listener == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = animatorType;
|
||||
result = 31 * result + (listener != null ? listener.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.location.modes.CameraMode;
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
|
||||
/**
|
||||
* Interface defining the source of compass heading data that is
|
||||
* consumed by the {@link LocationComponent} when in compass related
|
||||
* {@link RenderMode} or
|
||||
* {@link CameraMode}s.
|
||||
*/
|
||||
public interface CompassEngine {
|
||||
|
||||
/**
|
||||
* Adds a {@link CompassListener} that can be used to
|
||||
* receive heading and state changes.
|
||||
*
|
||||
* @param compassListener to be added
|
||||
*/
|
||||
void addCompassListener(@NonNull CompassListener compassListener);
|
||||
|
||||
/**
|
||||
* Removes a {@link CompassListener} that can be used to
|
||||
* receive heading and state changes.
|
||||
*
|
||||
* @param compassListener to be removed
|
||||
*/
|
||||
void removeCompassListener(@NonNull CompassListener compassListener);
|
||||
|
||||
/**
|
||||
* Returns the last heading value produced and pushed via
|
||||
* a compass listener.
|
||||
*
|
||||
* @return last heading value
|
||||
*/
|
||||
float getLastHeading();
|
||||
|
||||
/**
|
||||
* Provides the last know accuracy status from the sensor manager.
|
||||
* <p>
|
||||
* An integer value which is identical to the {@code SensorManager} class constants:
|
||||
* <ul>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return last accuracy status
|
||||
*/
|
||||
int getLastAccuracySensorStatus();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* Callbacks related to the compass
|
||||
*/
|
||||
public interface CompassListener {
|
||||
|
||||
/**
|
||||
* Callback's invoked when a new compass update occurs. You can listen into the compass updates
|
||||
* using {@link CompassEngine#addCompassListener(CompassListener)} and implementing these
|
||||
* callbacks. Note that this interface is also used internally to to update the UI chevron/arrow.
|
||||
*
|
||||
* @param userHeading the new compass heading
|
||||
*/
|
||||
void onCompassChanged(float userHeading);
|
||||
|
||||
/**
|
||||
* This gets invoked when the compass accuracy status changes from one value to another. It
|
||||
* provides an integer value which is identical to the {@code SensorManager} class constants:
|
||||
* <ul>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
|
||||
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param compassStatus the new accuracy of this sensor, one of
|
||||
* {@code SensorManager.SENSOR_STATUS_*}
|
||||
*/
|
||||
void onCompassAccuracyChange(int compassStatus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
import org.maplibre.android.utils.BitmapUtils;
|
||||
import org.maplibre.android.utils.ColorUtils;
|
||||
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.SHADOW_ICON;
|
||||
import static org.maplibre.android.style.expressions.Expression.rgba;
|
||||
import static org.maplibre.android.style.layers.Property.NONE;
|
||||
import static org.maplibre.android.style.layers.Property.VISIBLE;
|
||||
|
||||
|
||||
class IndicatorLocationLayerRenderer implements LocationLayerRenderer {
|
||||
|
||||
private Style style;
|
||||
private final LayerSourceProvider layerSourceProvider;
|
||||
private Layer layer;
|
||||
|
||||
@Nullable
|
||||
private LatLng lastLatLng;
|
||||
private double lastBearing = 0;
|
||||
private float lastAccuracy = 0;
|
||||
|
||||
IndicatorLocationLayerRenderer(LayerSourceProvider layerSourceProvider) {
|
||||
this.layerSourceProvider = layerSourceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeComponents(Style style) {
|
||||
this.style = style;
|
||||
layer = layerSourceProvider.generateLocationComponentLayer();
|
||||
if (lastLatLng != null) {
|
||||
setLatLng(lastLatLng);
|
||||
}
|
||||
setLayerBearing(lastBearing);
|
||||
setAccuracyRadius(lastAccuracy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLayers(LocationComponentPositionManager positionManager) {
|
||||
positionManager.addLayerToMap(layer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLayers() {
|
||||
style.removeLayer(layer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
setLayerVisibility(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cameraTiltUpdated(double tilt) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cameraBearingUpdated(double bearing) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@RenderMode.Mode int renderMode, boolean isStale) {
|
||||
setImages(renderMode, isStale);
|
||||
setLayerVisibility(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void styleAccuracy(float accuracyAlpha, int accuracyColor) {
|
||||
float[] colorArray = ColorUtils.colorToRgbaArray(accuracyColor);
|
||||
colorArray[3] = accuracyAlpha;
|
||||
Expression rgbaExpression = rgba(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.accuracyRadiusColor(rgbaExpression),
|
||||
LocationPropertyFactory.accuracyRadiusBorderColor(rgbaExpression)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLatLng(LatLng latLng) {
|
||||
setLayerLocation(latLng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGpsBearing(Float gpsBearing) {
|
||||
setLayerBearing(gpsBearing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCompassBearing(Float compassBearing) {
|
||||
setLayerBearing(compassBearing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccuracyRadius(Float accuracy) {
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.accuracyRadius(accuracy)
|
||||
);
|
||||
lastAccuracy = accuracy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void styleScaling(Expression scaleExpression) {
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.shadowImageSize(scaleExpression),
|
||||
LocationPropertyFactory.bearingImageSize(scaleExpression),
|
||||
LocationPropertyFactory.topImageSize(scaleExpression)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocationStale(boolean isStale, int renderMode) {
|
||||
setImages(renderMode, isStale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIconIds(String foregroundIconString, String foregroundStaleIconString, String backgroundIconString,
|
||||
String backgroundStaleIconString, String bearingIconString) {
|
||||
// not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBitmaps(@RenderMode.Mode int renderMode, @Nullable Bitmap shadowBitmap, Bitmap backgroundBitmap,
|
||||
Bitmap backgroundStaleBitmap, Bitmap bearingBitmap,
|
||||
Bitmap foregroundBitmap, Bitmap foregroundStaleBitmap) {
|
||||
if (shadowBitmap != null) {
|
||||
style.addImage(SHADOW_ICON, shadowBitmap);
|
||||
} else {
|
||||
style.removeImage(SHADOW_ICON);
|
||||
}
|
||||
style.addImage(FOREGROUND_ICON, foregroundBitmap);
|
||||
style.addImage(FOREGROUND_STALE_ICON, foregroundStaleBitmap);
|
||||
|
||||
if (renderMode == RenderMode.COMPASS) {
|
||||
float leftOffset = (bearingBitmap.getWidth() - backgroundBitmap.getWidth()) / 2f;
|
||||
float topOffset = (bearingBitmap.getHeight() - backgroundBitmap.getHeight()) / 2f;
|
||||
style.addImage(BEARING_ICON, BitmapUtils.mergeBitmap(bearingBitmap, backgroundBitmap, leftOffset, topOffset));
|
||||
|
||||
float staleLeftOffset = (bearingBitmap.getWidth() - backgroundStaleBitmap.getWidth()) / 2f;
|
||||
float staleTopOffset = (bearingBitmap.getHeight() - backgroundStaleBitmap.getHeight()) / 2f;
|
||||
style.addImage(BEARING_STALE_ICON,
|
||||
BitmapUtils.mergeBitmap(bearingBitmap, backgroundStaleBitmap, staleLeftOffset, staleTopOffset));
|
||||
} else {
|
||||
style.addImage(BACKGROUND_ICON, backgroundBitmap);
|
||||
style.addImage(BACKGROUND_STALE_ICON, backgroundStaleBitmap);
|
||||
style.addImage(BEARING_ICON, bearingBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLayerVisibility(boolean visible) {
|
||||
layer.setProperties(LocationPropertyFactory.visibility(visible ? VISIBLE : NONE));
|
||||
}
|
||||
|
||||
private void setLayerLocation(LatLng latLng) {
|
||||
Double[] values = new Double[] {latLng.getLatitude(), latLng.getLongitude(), 0d};
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.location(values)
|
||||
);
|
||||
lastLatLng = latLng;
|
||||
}
|
||||
|
||||
private void setLayerBearing(double bearing) {
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.bearing(bearing)
|
||||
);
|
||||
lastBearing = bearing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the visibility of the pulsing LocationComponent circle.
|
||||
*/
|
||||
@Override
|
||||
public void adjustPulsingCircleLayerVisibility(boolean visible) {
|
||||
// not supported at this time
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the the pulsing LocationComponent circle based on the set options.
|
||||
*/
|
||||
@Override
|
||||
public void stylePulsingCircle(LocationComponentOptions options) {
|
||||
// not supported at this time
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the visual appearance of the pulsing LocationComponent circle.
|
||||
*/
|
||||
@Override
|
||||
public void updatePulsingUi(float radius, @Nullable Float opacity) {
|
||||
// not supported at this time
|
||||
}
|
||||
|
||||
private void setImages(@RenderMode.Mode int renderMode, boolean isStale) {
|
||||
String topImage = "";
|
||||
String bearingImage = "";
|
||||
String shadowImage = "";
|
||||
|
||||
switch (renderMode) {
|
||||
case RenderMode.COMPASS:
|
||||
topImage = isStale ? FOREGROUND_STALE_ICON : FOREGROUND_ICON;
|
||||
bearingImage = isStale ? BEARING_STALE_ICON : BEARING_ICON;
|
||||
shadowImage = SHADOW_ICON;
|
||||
break;
|
||||
case RenderMode.GPS:
|
||||
topImage = "";
|
||||
bearingImage = isStale ? FOREGROUND_STALE_ICON : FOREGROUND_ICON;
|
||||
shadowImage = isStale ? BACKGROUND_STALE_ICON : BACKGROUND_ICON;
|
||||
setAccuracyRadius(0f);
|
||||
break;
|
||||
case RenderMode.NORMAL:
|
||||
topImage = isStale ? FOREGROUND_STALE_ICON : FOREGROUND_ICON;
|
||||
bearingImage = isStale ? BACKGROUND_STALE_ICON : BACKGROUND_ICON;
|
||||
shadowImage = SHADOW_ICON;
|
||||
break;
|
||||
}
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.topImage(topImage),
|
||||
LocationPropertyFactory.bearingImage(bearingImage),
|
||||
LocationPropertyFactory.shadowImage(shadowImage)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.TypeEvaluator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
class LatLngEvaluator implements TypeEvaluator<LatLng> {
|
||||
|
||||
private final LatLng latLng = new LatLng();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LatLng evaluate(float fraction, @NonNull LatLng startValue, @NonNull LatLng endValue) {
|
||||
latLng.setLatitude(startValue.getLatitude()
|
||||
+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
|
||||
latLng.setLongitude(startValue.getLongitude()
|
||||
+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
|
||||
return latLng;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.R;
|
||||
import org.maplibre.android.utils.BitmapUtils;
|
||||
|
||||
class LayerBitmapProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
LayerBitmapProvider(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
Bitmap generateBitmap(@DrawableRes int drawableRes, @ColorInt Integer tintColor) {
|
||||
Drawable drawable = BitmapUtils.getDrawableFromRes(context, drawableRes, tintColor);
|
||||
return BitmapUtils.getBitmapFromDrawable(drawable);
|
||||
}
|
||||
|
||||
Bitmap generateShadowBitmap(@NonNull LocationComponentOptions options) {
|
||||
Drawable shadowDrawable = BitmapUtils.getDrawableFromRes(context, R.drawable.maplibre_user_icon_shadow);
|
||||
return Utils.generateShadow(shadowDrawable, options.elevation());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.mapbox.geojson.Feature;
|
||||
import com.mapbox.geojson.Point;
|
||||
|
||||
class LayerFeatureProvider {
|
||||
|
||||
@NonNull
|
||||
Feature generateLocationFeature(@Nullable Feature locationFeature, boolean isStale) {
|
||||
if (locationFeature != null) {
|
||||
return locationFeature;
|
||||
}
|
||||
locationFeature = Feature.fromGeometry(Point.fromLngLat(0.0, 0.0));
|
||||
locationFeature.addNumberProperty(LocationComponentConstants.PROPERTY_GPS_BEARING, 0f);
|
||||
locationFeature.addNumberProperty(LocationComponentConstants.PROPERTY_COMPASS_BEARING, 0f);
|
||||
locationFeature.addBooleanProperty(LocationComponentConstants.PROPERTY_LOCATION_STALE, isStale);
|
||||
return locationFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.mapbox.geojson.Feature;
|
||||
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.style.layers.CircleLayer;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
import org.maplibre.android.style.layers.Property;
|
||||
import org.maplibre.android.style.layers.PropertyFactory;
|
||||
import org.maplibre.android.style.layers.SymbolLayer;
|
||||
import org.maplibre.android.style.layers.TransitionOptions;
|
||||
import org.maplibre.android.style.sources.GeoJsonOptions;
|
||||
import org.maplibre.android.style.sources.GeoJsonSource;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class LayerSourceProvider {
|
||||
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
@NonNull
|
||||
GeoJsonSource generateSource(Feature locationFeature) {
|
||||
return new GeoJsonSource(
|
||||
LocationComponentConstants.LOCATION_SOURCE,
|
||||
locationFeature,
|
||||
new GeoJsonOptions().withMaxZoom(16)
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Layer generateLayer(@NonNull String layerId) {
|
||||
SymbolLayer layer = new SymbolLayer(layerId, LocationComponentConstants.LOCATION_SOURCE);
|
||||
layer.setProperties(
|
||||
PropertyFactory.iconAllowOverlap(true),
|
||||
PropertyFactory.iconIgnorePlacement(true),
|
||||
PropertyFactory.iconRotationAlignment(Property.ICON_ROTATION_ALIGNMENT_MAP),
|
||||
PropertyFactory.iconRotate(
|
||||
Expression.match(Expression.literal(layerId), Expression.literal(0f),
|
||||
Expression.stop(LocationComponentConstants.FOREGROUND_LAYER,
|
||||
Expression.get(LocationComponentConstants.PROPERTY_GPS_BEARING)),
|
||||
Expression.stop(LocationComponentConstants.BACKGROUND_LAYER,
|
||||
Expression.get(LocationComponentConstants.PROPERTY_GPS_BEARING)),
|
||||
Expression.stop(LocationComponentConstants.SHADOW_LAYER,
|
||||
Expression.get(LocationComponentConstants.PROPERTY_GPS_BEARING)),
|
||||
Expression.stop(LocationComponentConstants.BEARING_LAYER,
|
||||
Expression.get(LocationComponentConstants.PROPERTY_COMPASS_BEARING))
|
||||
)
|
||||
),
|
||||
PropertyFactory.iconImage(
|
||||
Expression.match(Expression.literal(layerId), Expression.literal(EMPTY_STRING),
|
||||
Expression.stop(LocationComponentConstants.FOREGROUND_LAYER, Expression.switchCase(
|
||||
Expression.get(LocationComponentConstants.PROPERTY_LOCATION_STALE),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_FOREGROUND_ICON))),
|
||||
Expression.stop(LocationComponentConstants.BACKGROUND_LAYER, Expression.switchCase(
|
||||
Expression.get(LocationComponentConstants.PROPERTY_LOCATION_STALE),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_BACKGROUND_ICON))),
|
||||
Expression.stop(LocationComponentConstants.SHADOW_LAYER,
|
||||
Expression.literal(LocationComponentConstants.SHADOW_ICON)),
|
||||
Expression.stop(LocationComponentConstants.BEARING_LAYER,
|
||||
Expression.get(LocationComponentConstants.PROPERTY_BEARING_ICON))
|
||||
)
|
||||
),
|
||||
PropertyFactory.iconOffset(
|
||||
Expression.match(Expression.literal(layerId), Expression.literal(new Float[] {0f, 0f}),
|
||||
Expression.stop(Expression.literal(LocationComponentConstants.FOREGROUND_LAYER),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET)),
|
||||
Expression.stop(Expression.literal(LocationComponentConstants.SHADOW_LAYER),
|
||||
Expression.get(LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET))
|
||||
)
|
||||
)
|
||||
);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Layer generateAccuracyLayer() {
|
||||
return new CircleLayer(LocationComponentConstants.ACCURACY_LAYER, LocationComponentConstants.LOCATION_SOURCE)
|
||||
.withProperties(
|
||||
PropertyFactory.circleRadius(Expression.get(LocationComponentConstants.PROPERTY_ACCURACY_RADIUS)),
|
||||
PropertyFactory.circleColor(Expression.get(LocationComponentConstants.PROPERTY_ACCURACY_COLOR)),
|
||||
PropertyFactory.circleOpacity(Expression.get(LocationComponentConstants.PROPERTY_ACCURACY_ALPHA)),
|
||||
PropertyFactory.circleStrokeColor(Expression.get(LocationComponentConstants.PROPERTY_ACCURACY_COLOR)),
|
||||
PropertyFactory.circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP)
|
||||
);
|
||||
}
|
||||
|
||||
Set<String> getEmptyLayerSet() {
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
LocationLayerRenderer getSymbolLocationLayerRenderer(LayerFeatureProvider featureProvider,
|
||||
boolean isStale) {
|
||||
return new SymbolLocationLayerRenderer(this, featureProvider, isStale);
|
||||
}
|
||||
|
||||
LocationLayerRenderer getIndicatorLocationLayerRenderer() {
|
||||
return new IndicatorLocationLayerRenderer(this);
|
||||
}
|
||||
|
||||
Layer generateLocationComponentLayer() {
|
||||
LocationIndicatorLayer layer = new LocationIndicatorLayer(LocationComponentConstants.FOREGROUND_LAYER);
|
||||
layer.setLocationTransition(new TransitionOptions(0, 0));
|
||||
layer.setProperties(
|
||||
LocationPropertyFactory.perspectiveCompensation(0.9f),
|
||||
LocationPropertyFactory.imageTiltDisplacement(4f)
|
||||
);
|
||||
return layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link CircleLayer} to the map to support the {@link LocationComponent} pulsing UI functionality.
|
||||
*
|
||||
* @return a {@link CircleLayer} with the correct data-driven styling. Tilting the map will keep the pulsing
|
||||
* layer aligned with the map plane.
|
||||
*/
|
||||
@NonNull
|
||||
Layer generatePulsingCircleLayer() {
|
||||
return new CircleLayer(LocationComponentConstants.PULSING_CIRCLE_LAYER, LocationComponentConstants.LOCATION_SOURCE)
|
||||
.withProperties(
|
||||
PropertyFactory.circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.location.Location;
|
||||
import android.os.SystemClock;
|
||||
import android.util.SparseArray;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Size;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.maplibre.android.log.Logger;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
import org.maplibre.android.maps.Projection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.maplibre.android.location.LocationComponentConstants.ACCURACY_RADIUS_ANIMATION_DURATION;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.COMPASS_UPDATE_RATE_MS;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.MAX_ANIMATION_DURATION_MS;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_CAMERA_COMPASS_BEARING;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_CAMERA_GPS_BEARING;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_CAMERA_LATLNG;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_ACCURACY;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_COMPASS_BEARING;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_GPS_BEARING;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_LATLNG;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_PADDING;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_PULSING_CIRCLE;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_TILT;
|
||||
import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_ZOOM;
|
||||
import static org.maplibre.android.location.Utils.immediateAnimation;
|
||||
import static org.maplibre.android.location.Utils.normalize;
|
||||
import static org.maplibre.android.location.Utils.shortestRotation;
|
||||
|
||||
import org.maplibre.android.camera.CameraPosition;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
final class LocationAnimatorCoordinator {
|
||||
|
||||
private static final String TAG = "Mbgl-LocationAnimatorCoordinator";
|
||||
|
||||
@VisibleForTesting
|
||||
final SparseArray<MapLibreAnimator> animatorArray = new SparseArray<>();
|
||||
|
||||
private final Projection projection;
|
||||
private Location previousLocation;
|
||||
private float previousAccuracyRadius = -1;
|
||||
private float previousCompassBearing = -1;
|
||||
private long locationUpdateTimestamp = -1;
|
||||
private float durationMultiplier;
|
||||
private final MapLibreAnimatorProvider animatorProvider;
|
||||
private final MapLibreAnimatorSetProvider animatorSetProvider;
|
||||
private boolean compassAnimationEnabled;
|
||||
private boolean accuracyAnimationEnabled;
|
||||
|
||||
@VisibleForTesting
|
||||
int maxAnimationFps = Integer.MAX_VALUE;
|
||||
|
||||
@VisibleForTesting
|
||||
final SparseArray<MapLibreAnimator.AnimationsValueChangeListener> listeners = new SparseArray<>();
|
||||
|
||||
LocationAnimatorCoordinator(@NonNull Projection projection, @NonNull MapLibreAnimatorSetProvider animatorSetProvider,
|
||||
@NonNull MapLibreAnimatorProvider animatorProvider) {
|
||||
this.projection = projection;
|
||||
this.animatorProvider = animatorProvider;
|
||||
this.animatorSetProvider = animatorSetProvider;
|
||||
}
|
||||
|
||||
void updateAnimatorListenerHolders(@NonNull Set<AnimatorListenerHolder> listenerHolders) {
|
||||
listeners.clear();
|
||||
for (AnimatorListenerHolder holder : listenerHolders) {
|
||||
listeners.append(holder.getAnimatorType(), holder.getListener());
|
||||
}
|
||||
|
||||
for (int i = 0; i < animatorArray.size(); i++) {
|
||||
@MapLibreAnimator.Type int animatorType = animatorArray.keyAt(i);
|
||||
if (listeners.get(animatorType) == null) {
|
||||
MapLibreAnimator animator = animatorArray.get(animatorType);
|
||||
if (animator != null) {
|
||||
animator.makeInvalid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition currentCameraPosition,
|
||||
boolean isGpsNorth) {
|
||||
feedNewLocation(new Location[] {newLocation}, currentCameraPosition, isGpsNorth, false);
|
||||
}
|
||||
|
||||
void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations,
|
||||
@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth, boolean lookAheadUpdate) {
|
||||
Location newLocation = newLocations[newLocations.length - 1];
|
||||
if (previousLocation == null) {
|
||||
previousLocation = newLocation;
|
||||
locationUpdateTimestamp = SystemClock.elapsedRealtime() - TRANSITION_ANIMATION_DURATION_MS;
|
||||
}
|
||||
|
||||
LatLng previousLayerLatLng = getPreviousLayerLatLng();
|
||||
float previousLayerBearing = getPreviousLayerGpsBearing();
|
||||
LatLng previousCameraLatLng = currentCameraPosition.target;
|
||||
float previousCameraBearing = normalize((float) currentCameraPosition.bearing);
|
||||
|
||||
// generate targets for layer
|
||||
LatLng[] latLngValues = getLatLngValues(previousLayerLatLng, newLocations);
|
||||
Float[] bearingValues = getBearingValues(previousLayerBearing, newLocations);
|
||||
updateLayerAnimators(latLngValues, bearingValues);
|
||||
|
||||
// replace the animation start with the camera's previous value
|
||||
latLngValues[0] = previousCameraLatLng;
|
||||
if (isGpsNorth) {
|
||||
bearingValues = new Float[] {previousCameraBearing, shortestRotation(0f, previousCameraBearing)};
|
||||
} else {
|
||||
bearingValues = getBearingValues(previousCameraBearing, newLocations);
|
||||
}
|
||||
updateCameraAnimators(latLngValues, bearingValues);
|
||||
|
||||
LatLng targetLatLng = new LatLng(newLocation);
|
||||
boolean snap = immediateAnimation(projection, previousCameraLatLng, targetLatLng)
|
||||
|| immediateAnimation(projection, previousLayerLatLng, targetLatLng);
|
||||
|
||||
long animationDuration = 0;
|
||||
if (!snap) {
|
||||
long previousUpdateTimeStamp = locationUpdateTimestamp;
|
||||
locationUpdateTimestamp = SystemClock.elapsedRealtime();
|
||||
|
||||
if (previousUpdateTimeStamp == 0) {
|
||||
animationDuration = 0;
|
||||
} else if (lookAheadUpdate) {
|
||||
long currentTimestamp = System.currentTimeMillis();
|
||||
if (currentTimestamp > newLocation.getTime()) {
|
||||
animationDuration = 0;
|
||||
Logger.e("LocationAnimatorCoordinator",
|
||||
"Lookahead enabled, but the target location's timestamp is smaller than current timestamp");
|
||||
} else {
|
||||
animationDuration = newLocation.getTime() - currentTimestamp;
|
||||
}
|
||||
} else {
|
||||
animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier)
|
||||
/* make animation slightly longer with durationMultiplier, defaults to 1.1f */;
|
||||
}
|
||||
|
||||
animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS);
|
||||
}
|
||||
|
||||
playAnimators(animationDuration,
|
||||
ANIMATOR_LAYER_LATLNG,
|
||||
ANIMATOR_LAYER_GPS_BEARING,
|
||||
ANIMATOR_CAMERA_LATLNG,
|
||||
ANIMATOR_CAMERA_GPS_BEARING);
|
||||
|
||||
previousLocation = newLocation;
|
||||
}
|
||||
|
||||
void feedNewCompassBearing(float targetCompassBearing, @NonNull CameraPosition currentCameraPosition) {
|
||||
if (previousCompassBearing < 0) {
|
||||
previousCompassBearing = targetCompassBearing;
|
||||
}
|
||||
|
||||
float previousLayerBearing = getPreviousLayerCompassBearing();
|
||||
float previousCameraBearing = (float) currentCameraPosition.bearing;
|
||||
|
||||
updateCompassAnimators(targetCompassBearing, previousLayerBearing, previousCameraBearing);
|
||||
playAnimators(
|
||||
compassAnimationEnabled ? COMPASS_UPDATE_RATE_MS : 0,
|
||||
ANIMATOR_LAYER_COMPASS_BEARING,
|
||||
ANIMATOR_CAMERA_COMPASS_BEARING);
|
||||
|
||||
previousCompassBearing = targetCompassBearing;
|
||||
}
|
||||
|
||||
void feedNewAccuracyRadius(float targetAccuracyRadius, boolean noAnimation) {
|
||||
if (previousAccuracyRadius < 0) {
|
||||
previousAccuracyRadius = targetAccuracyRadius;
|
||||
}
|
||||
|
||||
float previousAccuracyRadius = getPreviousAccuracyRadius();
|
||||
updateAccuracyAnimators(targetAccuracyRadius, previousAccuracyRadius);
|
||||
playAnimators(
|
||||
noAnimation || !accuracyAnimationEnabled ? 0 : ACCURACY_RADIUS_ANIMATION_DURATION,
|
||||
ANIMATOR_LAYER_ACCURACY);
|
||||
|
||||
this.previousAccuracyRadius = targetAccuracyRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the {@link PulsingLocationCircleAnimator}, which is a type of {@link MapLibreAnimator}.
|
||||
* This method also adds the animator to this class' animator array.
|
||||
*
|
||||
* @param options the {@link LocationComponentOptions} passed to this class upstream from the
|
||||
* {@link LocationComponent}.
|
||||
*/
|
||||
void startLocationComponentCirclePulsing(LocationComponentOptions options) {
|
||||
cancelAnimator(ANIMATOR_PULSING_CIRCLE);
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(ANIMATOR_PULSING_CIRCLE);
|
||||
if (listener != null) {
|
||||
PulsingLocationCircleAnimator pulsingLocationCircleAnimator = animatorProvider.pulsingCircleAnimator(
|
||||
listener,
|
||||
maxAnimationFps,
|
||||
options.pulseSingleDuration(),
|
||||
options.pulseMaxRadius(),
|
||||
options.pulseInterpolator() == null ? new DecelerateInterpolator() : options.pulseInterpolator());
|
||||
animatorArray.put(ANIMATOR_PULSING_CIRCLE, pulsingLocationCircleAnimator);
|
||||
playPulsingAnimator();
|
||||
}
|
||||
}
|
||||
|
||||
void feedNewZoomLevel(double targetZoomLevel, @NonNull CameraPosition currentCameraPosition, long animationDuration,
|
||||
@Nullable MapLibreMap.CancelableCallback callback) {
|
||||
updateZoomAnimator((float) targetZoomLevel, (float) currentCameraPosition.zoom, callback);
|
||||
playAnimators(animationDuration, ANIMATOR_ZOOM);
|
||||
}
|
||||
|
||||
void feedNewPadding(double[] padding, @NonNull CameraPosition currentCameraPosition, long animationDuration,
|
||||
@Nullable MapLibreMap.CancelableCallback callback) {
|
||||
updatePaddingAnimator(padding, currentCameraPosition.padding, callback);
|
||||
playAnimators(animationDuration, ANIMATOR_PADDING);
|
||||
}
|
||||
|
||||
void feedNewTilt(double targetTilt, @NonNull CameraPosition currentCameraPosition, long animationDuration,
|
||||
@Nullable MapLibreMap.CancelableCallback callback) {
|
||||
updateTiltAnimator((float) targetTilt, (float) currentCameraPosition.tilt, callback);
|
||||
playAnimators(animationDuration, ANIMATOR_TILT);
|
||||
}
|
||||
|
||||
private LatLng getPreviousLayerLatLng() {
|
||||
LatLng previousLatLng;
|
||||
MapLibreAnimator latLngAnimator = animatorArray.get(ANIMATOR_LAYER_LATLNG);
|
||||
if (latLngAnimator != null) {
|
||||
previousLatLng = (LatLng) latLngAnimator.getAnimatedValue();
|
||||
} else {
|
||||
previousLatLng = new LatLng(previousLocation);
|
||||
}
|
||||
return previousLatLng;
|
||||
}
|
||||
|
||||
private float getPreviousLayerGpsBearing() {
|
||||
MapLibreFloatAnimator animator = (MapLibreFloatAnimator) animatorArray.get(ANIMATOR_LAYER_GPS_BEARING);
|
||||
float previousBearing;
|
||||
if (animator != null) {
|
||||
previousBearing = (float) animator.getAnimatedValue();
|
||||
} else {
|
||||
previousBearing = previousLocation.getBearing();
|
||||
}
|
||||
return previousBearing;
|
||||
}
|
||||
|
||||
private float getPreviousLayerCompassBearing() {
|
||||
MapLibreFloatAnimator animator = (MapLibreFloatAnimator) animatorArray.get(ANIMATOR_LAYER_COMPASS_BEARING);
|
||||
|
||||
float previousBearing;
|
||||
if (animator != null) {
|
||||
previousBearing = (float) animator.getAnimatedValue();
|
||||
} else {
|
||||
previousBearing = previousCompassBearing;
|
||||
}
|
||||
return previousBearing;
|
||||
}
|
||||
|
||||
private float getPreviousAccuracyRadius() {
|
||||
MapLibreAnimator animator = animatorArray.get(ANIMATOR_LAYER_ACCURACY);
|
||||
float previousRadius;
|
||||
if (animator != null) {
|
||||
previousRadius = (float) animator.getAnimatedValue();
|
||||
} else {
|
||||
previousRadius = previousAccuracyRadius;
|
||||
}
|
||||
return previousRadius;
|
||||
}
|
||||
|
||||
private LatLng[] getLatLngValues(LatLng previousLatLng, Location[] targetLocations) {
|
||||
LatLng[] latLngs = new LatLng[targetLocations.length + 1];
|
||||
latLngs[0] = previousLatLng;
|
||||
for (int i = 1; i < latLngs.length; i++) {
|
||||
latLngs[i] = new LatLng(targetLocations[i - 1]);
|
||||
}
|
||||
return latLngs;
|
||||
}
|
||||
|
||||
private Float[] getBearingValues(Float previousBearing, Location[] targetLocations) {
|
||||
Float[] bearings = new Float[targetLocations.length + 1];
|
||||
|
||||
// Because Location bearing values are normalized to [0, 360]
|
||||
// we need to do the same for the previous bearing value to determine the shortest path
|
||||
bearings[0] = normalize(previousBearing);
|
||||
for (int i = 1; i < bearings.length; i++) {
|
||||
bearings[i] = shortestRotation(targetLocations[i - 1].getBearing(), bearings[i - 1]);
|
||||
}
|
||||
return bearings;
|
||||
}
|
||||
|
||||
private void updateLayerAnimators(LatLng[] latLngValues, Float[] bearingValues) {
|
||||
createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, latLngValues);
|
||||
createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, bearingValues);
|
||||
}
|
||||
|
||||
private void updateCameraAnimators(LatLng[] latLngValues, Float[] bearingValues) {
|
||||
createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, latLngValues);
|
||||
createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, bearingValues);
|
||||
}
|
||||
|
||||
private void updateCompassAnimators(float targetCompassBearing, float previousLayerBearing,
|
||||
float previousCameraBearing) {
|
||||
float normalizedLayerBearing = Utils.shortestRotation(targetCompassBearing, previousLayerBearing);
|
||||
createNewFloatAnimator(ANIMATOR_LAYER_COMPASS_BEARING, previousLayerBearing, normalizedLayerBearing);
|
||||
|
||||
float normalizedCameraBearing = Utils.shortestRotation(targetCompassBearing, previousCameraBearing);
|
||||
createNewFloatAnimator(ANIMATOR_CAMERA_COMPASS_BEARING, previousCameraBearing, normalizedCameraBearing);
|
||||
}
|
||||
|
||||
private void updateAccuracyAnimators(float targetAccuracyRadius, float previousAccuracyRadius) {
|
||||
createNewFloatAnimator(ANIMATOR_LAYER_ACCURACY, previousAccuracyRadius, targetAccuracyRadius);
|
||||
}
|
||||
|
||||
private void updateZoomAnimator(float targetZoomLevel, float previousZoomLevel,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
createNewCameraAdapterAnimator(ANIMATOR_ZOOM, new Float[] {previousZoomLevel, targetZoomLevel}, cancelableCallback);
|
||||
}
|
||||
|
||||
private void updatePaddingAnimator(double[] targetPadding, double[] previousPadding,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
createNewPaddingAnimator(ANIMATOR_PADDING, new double[][] {previousPadding, targetPadding}, cancelableCallback);
|
||||
}
|
||||
|
||||
private void updateTiltAnimator(float targetTilt, float previousTiltLevel,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
createNewCameraAdapterAnimator(ANIMATOR_TILT, new Float[] {previousTiltLevel, targetTilt}, cancelableCallback);
|
||||
}
|
||||
|
||||
private void createNewLatLngAnimator(@MapLibreAnimator.Type int animatorType, LatLng previous, LatLng target) {
|
||||
createNewLatLngAnimator(animatorType, new LatLng[] {previous, target});
|
||||
}
|
||||
|
||||
private void createNewLatLngAnimator(@MapLibreAnimator.Type int animatorType, LatLng[] values) {
|
||||
cancelAnimator(animatorType);
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
|
||||
if (listener != null) {
|
||||
animatorArray.put(animatorType, animatorProvider.latLngAnimator(values, listener, maxAnimationFps));
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewFloatAnimator(@MapLibreAnimator.Type int animatorType, float previous, float target) {
|
||||
createNewFloatAnimator(animatorType, new Float[] {previous, target});
|
||||
}
|
||||
|
||||
private void createNewFloatAnimator(@MapLibreAnimator.Type int animatorType, @NonNull @Size(min = 2) Float[] values) {
|
||||
cancelAnimator(animatorType);
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
|
||||
if (listener != null) {
|
||||
animatorArray.put(animatorType, animatorProvider.floatAnimator(values, listener, maxAnimationFps));
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewCameraAdapterAnimator(@MapLibreAnimator.Type int animatorType,
|
||||
@NonNull @Size(min = 2) Float[] values,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
cancelAnimator(animatorType);
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
|
||||
if (listener != null) {
|
||||
animatorArray.put(animatorType, animatorProvider.cameraAnimator(values, listener, cancelableCallback));
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewPaddingAnimator(@MapLibreAnimator.Type int animatorType,
|
||||
@NonNull @Size(min = 2) double[][] values,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
cancelAnimator(animatorType);
|
||||
MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
|
||||
if (listener != null) {
|
||||
animatorArray.put(animatorType, animatorProvider.paddingAnimator(values, listener, cancelableCallback));
|
||||
}
|
||||
}
|
||||
|
||||
private float checkGpsNorth(boolean isGpsNorth, float targetCameraBearing) {
|
||||
if (isGpsNorth) {
|
||||
targetCameraBearing = 0;
|
||||
}
|
||||
return targetCameraBearing;
|
||||
}
|
||||
|
||||
private void playAnimators(long duration, @MapLibreAnimator.Type int... animatorTypes) {
|
||||
List<Animator> animators = new ArrayList<>();
|
||||
for (@MapLibreAnimator.Type int animatorType : animatorTypes) {
|
||||
Animator animator = animatorArray.get(animatorType);
|
||||
if (animator != null) {
|
||||
animators.add(animator);
|
||||
}
|
||||
}
|
||||
animatorSetProvider.startAnimation(animators, new LinearInterpolator(), duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the {@link PulsingLocationCircleAnimator} in the animator array. This method is separate
|
||||
* from {@link #playAnimators(long, int...)} because the MapboxAnimatorSetProvider has many more
|
||||
* customizable animation parameters than the other {@link MapLibreAnimator}s.
|
||||
*/
|
||||
private void playPulsingAnimator() {
|
||||
Animator animator = animatorArray.get(ANIMATOR_PULSING_CIRCLE);
|
||||
if (animator != null) {
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
|
||||
void resetAllCameraAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) {
|
||||
resetCameraCompassAnimation(currentCameraPosition);
|
||||
boolean snap = resetCameraLocationAnimations(currentCameraPosition, isGpsNorth);
|
||||
playAnimators(
|
||||
snap ? 0 : TRANSITION_ANIMATION_DURATION_MS,
|
||||
ANIMATOR_CAMERA_LATLNG,
|
||||
ANIMATOR_CAMERA_GPS_BEARING);
|
||||
}
|
||||
|
||||
private boolean resetCameraLocationAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) {
|
||||
resetCameraGpsBearingAnimation(currentCameraPosition, isGpsNorth);
|
||||
return resetCameraLatLngAnimation(currentCameraPosition);
|
||||
}
|
||||
|
||||
private boolean resetCameraLatLngAnimation(@NonNull CameraPosition currentCameraPosition) {
|
||||
MapLibreLatLngAnimator animator = (MapLibreLatLngAnimator) animatorArray.get(ANIMATOR_CAMERA_LATLNG);
|
||||
if (animator == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LatLng currentTarget = animator.getTarget();
|
||||
LatLng previousCameraTarget = currentCameraPosition.target;
|
||||
createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, previousCameraTarget, currentTarget);
|
||||
|
||||
return immediateAnimation(projection, previousCameraTarget, currentTarget);
|
||||
}
|
||||
|
||||
private void resetCameraGpsBearingAnimation(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) {
|
||||
MapLibreFloatAnimator animator = (MapLibreFloatAnimator) animatorArray.get(ANIMATOR_CAMERA_GPS_BEARING);
|
||||
if (animator == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float currentTargetBearing = animator.getTarget();
|
||||
currentTargetBearing = checkGpsNorth(isGpsNorth, currentTargetBearing);
|
||||
float previousCameraBearing = (float) currentCameraPosition.bearing;
|
||||
float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing);
|
||||
createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, previousCameraBearing, normalizedCameraBearing);
|
||||
}
|
||||
|
||||
private void resetCameraCompassAnimation(@NonNull CameraPosition currentCameraPosition) {
|
||||
MapLibreFloatAnimator animator =
|
||||
(MapLibreFloatAnimator) animatorArray.get(ANIMATOR_CAMERA_COMPASS_BEARING);
|
||||
if (animator == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float currentTargetBearing = animator.getTarget();
|
||||
float previousCameraBearing = (float) currentCameraPosition.bearing;
|
||||
float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing);
|
||||
createNewFloatAnimator(ANIMATOR_CAMERA_COMPASS_BEARING, previousCameraBearing, normalizedCameraBearing);
|
||||
}
|
||||
|
||||
void resetAllLayerAnimations() {
|
||||
MapLibreLatLngAnimator latLngAnimator = (MapLibreLatLngAnimator) animatorArray.get(ANIMATOR_LAYER_LATLNG);
|
||||
MapLibreFloatAnimator gpsBearingAnimator = (MapLibreFloatAnimator) animatorArray.get(ANIMATOR_LAYER_GPS_BEARING);
|
||||
MapLibreFloatAnimator compassBearingAnimator =
|
||||
(MapLibreFloatAnimator) animatorArray.get(ANIMATOR_LAYER_COMPASS_BEARING);
|
||||
MapLibreFloatAnimator accuracyAnimator =
|
||||
(MapLibreFloatAnimator) animatorArray.get(ANIMATOR_LAYER_ACCURACY);
|
||||
|
||||
if (latLngAnimator != null && gpsBearingAnimator != null) {
|
||||
LatLng currentLatLng = (LatLng) latLngAnimator.getAnimatedValue();
|
||||
LatLng currentLatLngTarget = latLngAnimator.getTarget();
|
||||
createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, currentLatLng, currentLatLngTarget);
|
||||
|
||||
float currentGpsBearing = (float) gpsBearingAnimator.getAnimatedValue();
|
||||
float currentGpsBearingTarget = gpsBearingAnimator.getTarget();
|
||||
createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, currentGpsBearing, currentGpsBearingTarget);
|
||||
|
||||
long duration = latLngAnimator.getDuration() - latLngAnimator.getCurrentPlayTime();
|
||||
|
||||
playAnimators(duration, ANIMATOR_LAYER_LATLNG, ANIMATOR_LAYER_GPS_BEARING);
|
||||
}
|
||||
|
||||
if (compassBearingAnimator != null) {
|
||||
float currentLayerBearing = getPreviousLayerCompassBearing();
|
||||
float currentLayerBearingTarget = compassBearingAnimator.getTarget();
|
||||
createNewFloatAnimator(ANIMATOR_LAYER_COMPASS_BEARING, currentLayerBearing, currentLayerBearingTarget);
|
||||
playAnimators(
|
||||
compassAnimationEnabled ? COMPASS_UPDATE_RATE_MS : 0,
|
||||
ANIMATOR_LAYER_COMPASS_BEARING);
|
||||
}
|
||||
|
||||
if (accuracyAnimator != null) {
|
||||
feedNewAccuracyRadius(previousAccuracyRadius, false);
|
||||
}
|
||||
}
|
||||
|
||||
void cancelZoomAnimation() {
|
||||
cancelAnimator(ANIMATOR_ZOOM);
|
||||
}
|
||||
|
||||
void cancelPaddingAnimation() {
|
||||
cancelAnimator(ANIMATOR_PADDING);
|
||||
}
|
||||
|
||||
void cancelTiltAnimation() {
|
||||
cancelAnimator(ANIMATOR_TILT);
|
||||
}
|
||||
|
||||
void cancelAndRemoveGpsBearingAnimation() {
|
||||
cancelAnimator(ANIMATOR_LAYER_GPS_BEARING);
|
||||
animatorArray.remove(ANIMATOR_LAYER_GPS_BEARING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the pulsing circle location animator.
|
||||
*/
|
||||
void stopPulsingCircleAnimation() {
|
||||
cancelAnimator(ANIMATOR_PULSING_CIRCLE);
|
||||
}
|
||||
|
||||
void cancelAllAnimations() {
|
||||
for (int i = 0; i < animatorArray.size(); i++) {
|
||||
@MapLibreAnimator.Type int animatorType = animatorArray.keyAt(i);
|
||||
cancelAnimator(animatorType);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelAnimator(@MapLibreAnimator.Type int animatorType) {
|
||||
MapLibreAnimator animator = animatorArray.get(animatorType);
|
||||
if (animator != null) {
|
||||
animator.cancel();
|
||||
animator.removeAllUpdateListeners();
|
||||
animator.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void setTrackingAnimationDurationMultiplier(float trackingAnimationDurationMultiplier) {
|
||||
this.durationMultiplier = trackingAnimationDurationMultiplier;
|
||||
}
|
||||
|
||||
void setCompassAnimationEnabled(boolean compassAnimationEnabled) {
|
||||
this.compassAnimationEnabled = compassAnimationEnabled;
|
||||
}
|
||||
|
||||
void setAccuracyAnimationEnabled(boolean accuracyAnimationEnabled) {
|
||||
this.accuracyAnimationEnabled = accuracyAnimationEnabled;
|
||||
}
|
||||
|
||||
void setMaxAnimationFps(int maxAnimationFps) {
|
||||
if (maxAnimationFps <= 0) {
|
||||
Logger.e(TAG, "Max animation FPS cannot be less or equal to 0.");
|
||||
return;
|
||||
}
|
||||
this.maxAnimationFps = maxAnimationFps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.location.Location;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.mapbox.android.gestures.AndroidGesturesManager;
|
||||
import com.mapbox.android.gestures.MoveGestureDetector;
|
||||
import com.mapbox.android.gestures.RotateGestureDetector;
|
||||
import org.maplibre.android.location.modes.CameraMode;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
import org.maplibre.android.maps.Transform;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.maplibre.android.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS;
|
||||
|
||||
import org.maplibre.android.camera.CameraPosition;
|
||||
import org.maplibre.android.camera.CameraUpdate;
|
||||
import org.maplibre.android.camera.CameraUpdateFactory;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
final class LocationCameraController {
|
||||
|
||||
@CameraMode.Mode
|
||||
private int cameraMode;
|
||||
|
||||
private final MapLibreMap maplibreMap;
|
||||
private final Transform transform;
|
||||
private final OnCameraTrackingChangedListener internalCameraTrackingChangedListener;
|
||||
private LocationComponentOptions options;
|
||||
|
||||
private final MoveGestureDetector moveGestureDetector;
|
||||
private final OnCameraMoveInvalidateListener onCameraMoveInvalidateListener;
|
||||
|
||||
private final AndroidGesturesManager initialGesturesManager;
|
||||
private final AndroidGesturesManager internalGesturesManager;
|
||||
|
||||
private boolean isTransitioning;
|
||||
private LatLng lastLocation;
|
||||
private boolean isEnabled;
|
||||
|
||||
LocationCameraController(
|
||||
Context context,
|
||||
MapLibreMap maplibreMap,
|
||||
Transform transform,
|
||||
OnCameraTrackingChangedListener internalCameraTrackingChangedListener,
|
||||
@NonNull LocationComponentOptions options,
|
||||
OnCameraMoveInvalidateListener onCameraMoveInvalidateListener) {
|
||||
this.maplibreMap = maplibreMap;
|
||||
this.transform = transform;
|
||||
|
||||
initialGesturesManager = maplibreMap.getGesturesManager();
|
||||
internalGesturesManager = new LocationGesturesManager(context);
|
||||
moveGestureDetector = internalGesturesManager.getMoveGestureDetector();
|
||||
maplibreMap.addOnRotateListener(onRotateListener);
|
||||
maplibreMap.addOnFlingListener(onFlingListener);
|
||||
maplibreMap.addOnMoveListener(onMoveListener);
|
||||
maplibreMap.addOnCameraMoveListener(onCameraMoveListener);
|
||||
this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener;
|
||||
this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener;
|
||||
initializeOptions(options);
|
||||
}
|
||||
|
||||
// Package private for testing purposes
|
||||
LocationCameraController(MapLibreMap maplibreMap,
|
||||
Transform transform,
|
||||
MoveGestureDetector moveGestureDetector,
|
||||
OnCameraTrackingChangedListener internalCameraTrackingChangedListener,
|
||||
OnCameraMoveInvalidateListener onCameraMoveInvalidateListener,
|
||||
AndroidGesturesManager initialGesturesManager,
|
||||
AndroidGesturesManager internalGesturesManager) {
|
||||
this.maplibreMap = maplibreMap;
|
||||
maplibreMap.addOnCameraMoveListener(onCameraMoveListener);
|
||||
this.transform = transform;
|
||||
this.moveGestureDetector = moveGestureDetector;
|
||||
this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener;
|
||||
this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener;
|
||||
this.internalGesturesManager = internalGesturesManager;
|
||||
this.initialGesturesManager = initialGesturesManager;
|
||||
}
|
||||
|
||||
void initializeOptions(LocationComponentOptions options) {
|
||||
this.options = options;
|
||||
if (options.trackingGesturesManagement()) {
|
||||
if (maplibreMap.getGesturesManager() != internalGesturesManager) {
|
||||
maplibreMap.setGesturesManager(internalGesturesManager, true, true);
|
||||
}
|
||||
adjustGesturesThresholds();
|
||||
} else if (maplibreMap.getGesturesManager() != initialGesturesManager) {
|
||||
maplibreMap.setGesturesManager(initialGesturesManager, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
void setCameraMode(@CameraMode.Mode int cameraMode) {
|
||||
setCameraMode(cameraMode, null, TRANSITION_ANIMATION_DURATION_MS, null, null, null, null);
|
||||
}
|
||||
|
||||
void setCameraMode(@CameraMode.Mode final int cameraMode, @Nullable Location lastLocation,
|
||||
long transitionDuration,
|
||||
@Nullable Double zoom, @Nullable Double bearing, @Nullable Double tilt,
|
||||
@Nullable OnLocationCameraTransitionListener internalTransitionListener) {
|
||||
if (this.cameraMode == cameraMode) {
|
||||
if (internalTransitionListener != null) {
|
||||
internalTransitionListener.onLocationCameraTransitionFinished(cameraMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean wasTracking = isLocationTracking();
|
||||
this.cameraMode = cameraMode;
|
||||
|
||||
if (cameraMode != CameraMode.NONE) {
|
||||
maplibreMap.cancelTransitions();
|
||||
}
|
||||
|
||||
adjustGesturesThresholds();
|
||||
notifyCameraTrackingChangeListener(wasTracking);
|
||||
transitionToCurrentLocation(
|
||||
wasTracking, lastLocation, transitionDuration, zoom, bearing, tilt, internalTransitionListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a camera animation to the current location if location tracking was engaged.
|
||||
* Notifies an internal listener when the transition's finished to invalidate animators and notify external listeners.
|
||||
*/
|
||||
private void transitionToCurrentLocation(boolean wasTracking, Location lastLocation,
|
||||
long transitionDuration,
|
||||
Double zoom, Double bearing, Double tilt,
|
||||
final OnLocationCameraTransitionListener internalTransitionListener) {
|
||||
if (!wasTracking && isLocationTracking() && lastLocation != null && isEnabled) {
|
||||
isTransitioning = true;
|
||||
LatLng target = new LatLng(lastLocation);
|
||||
|
||||
CameraPosition.Builder builder = new CameraPosition.Builder().target(target);
|
||||
if (zoom != null) {
|
||||
builder.zoom(zoom);
|
||||
}
|
||||
if (tilt != null) {
|
||||
builder.tilt(tilt);
|
||||
}
|
||||
if (bearing != null) {
|
||||
builder.bearing(bearing);
|
||||
} else {
|
||||
if (isLocationBearingTracking()) {
|
||||
builder.bearing(cameraMode == CameraMode.TRACKING_GPS_NORTH ? 0 : lastLocation.getBearing());
|
||||
}
|
||||
}
|
||||
|
||||
CameraUpdate update = CameraUpdateFactory.newCameraPosition(builder.build());
|
||||
MapLibreMap.CancelableCallback callback = new MapLibreMap.CancelableCallback() {
|
||||
@Override
|
||||
public void onCancel() {
|
||||
isTransitioning = false;
|
||||
if (internalTransitionListener != null) {
|
||||
internalTransitionListener.onLocationCameraTransitionCanceled(cameraMode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
isTransitioning = false;
|
||||
if (internalTransitionListener != null) {
|
||||
internalTransitionListener.onLocationCameraTransitionFinished(cameraMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CameraPosition currentPosition = maplibreMap.getCameraPosition();
|
||||
if (Utils.immediateAnimation(maplibreMap.getProjection(), currentPosition.target, target)) {
|
||||
transform.moveCamera(
|
||||
maplibreMap,
|
||||
update,
|
||||
callback);
|
||||
} else {
|
||||
transform.animateCamera(
|
||||
maplibreMap,
|
||||
update,
|
||||
(int) transitionDuration,
|
||||
callback);
|
||||
}
|
||||
} else {
|
||||
if (internalTransitionListener != null) {
|
||||
internalTransitionListener.onLocationCameraTransitionFinished(cameraMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getCameraMode() {
|
||||
return cameraMode;
|
||||
}
|
||||
|
||||
private void setBearing(float bearing) {
|
||||
if (isTransitioning) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.moveCamera(maplibreMap, CameraUpdateFactory.bearingTo(bearing), null);
|
||||
onCameraMoveInvalidateListener.onInvalidateCameraMove();
|
||||
}
|
||||
|
||||
private void setLatLng(@NonNull LatLng latLng) {
|
||||
if (isTransitioning) {
|
||||
return;
|
||||
}
|
||||
lastLocation = latLng;
|
||||
transform.moveCamera(maplibreMap, CameraUpdateFactory.newLatLng(latLng), null);
|
||||
onCameraMoveInvalidateListener.onInvalidateCameraMove();
|
||||
}
|
||||
|
||||
private void setZoom(float zoom) {
|
||||
if (isTransitioning) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.moveCamera(maplibreMap, CameraUpdateFactory.zoomTo(zoom), null);
|
||||
onCameraMoveInvalidateListener.onInvalidateCameraMove();
|
||||
}
|
||||
|
||||
private void setPadding(double[] padding) {
|
||||
if (isTransitioning) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.moveCamera(maplibreMap, CameraUpdateFactory.paddingTo(padding), null);
|
||||
onCameraMoveInvalidateListener.onInvalidateCameraMove();
|
||||
}
|
||||
|
||||
private void setTilt(float tilt) {
|
||||
if (isTransitioning) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.moveCamera(maplibreMap, CameraUpdateFactory.tiltTo(tilt), null);
|
||||
onCameraMoveInvalidateListener.onInvalidateCameraMove();
|
||||
}
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<LatLng> latLngValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<LatLng>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(LatLng value) {
|
||||
setLatLng(value);
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> gpsBearingValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float value) {
|
||||
boolean trackingNorth = cameraMode == CameraMode.TRACKING_GPS_NORTH
|
||||
&& maplibreMap.getCameraPosition().bearing == 0;
|
||||
|
||||
if (!trackingNorth) {
|
||||
setBearing(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> compassBearingValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float value) {
|
||||
if (cameraMode == CameraMode.TRACKING_COMPASS
|
||||
|| cameraMode == CameraMode.NONE_COMPASS) {
|
||||
setBearing(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> zoomValueListener =
|
||||
value -> setZoom(value);
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<double[]> paddingValueListener =
|
||||
value -> setPadding(value);
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> tiltValueListener =
|
||||
value -> setTilt(value);
|
||||
|
||||
Set<AnimatorListenerHolder> getAnimationListeners() {
|
||||
Set<AnimatorListenerHolder> holders = new HashSet<>();
|
||||
if (isLocationTracking()) {
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_CAMERA_LATLNG, latLngValueListener));
|
||||
}
|
||||
|
||||
if (isLocationBearingTracking()) {
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_CAMERA_GPS_BEARING, gpsBearingValueListener));
|
||||
}
|
||||
|
||||
if (isConsumingCompass()) {
|
||||
holders.add(new AnimatorListenerHolder(
|
||||
MapLibreAnimator.ANIMATOR_CAMERA_COMPASS_BEARING,
|
||||
compassBearingValueListener));
|
||||
}
|
||||
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_ZOOM, zoomValueListener));
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_TILT, tiltValueListener));
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_PADDING, paddingValueListener));
|
||||
return holders;
|
||||
}
|
||||
|
||||
boolean isTransitioning() {
|
||||
return isTransitioning;
|
||||
}
|
||||
|
||||
private void adjustGesturesThresholds() {
|
||||
if (options.trackingGesturesManagement()) {
|
||||
if (isLocationTracking()) {
|
||||
moveGestureDetector.setMoveThreshold(options.trackingInitialMoveThreshold());
|
||||
} else {
|
||||
moveGestureDetector.setMoveThreshold(0f);
|
||||
moveGestureDetector.setMoveThresholdRect(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isConsumingCompass() {
|
||||
return cameraMode == CameraMode.TRACKING_COMPASS
|
||||
|| cameraMode == CameraMode.NONE_COMPASS;
|
||||
}
|
||||
|
||||
void setEnabled(boolean enabled) {
|
||||
isEnabled = enabled;
|
||||
}
|
||||
|
||||
private boolean isLocationTracking() {
|
||||
return cameraMode == CameraMode.TRACKING
|
||||
|| cameraMode == CameraMode.TRACKING_COMPASS
|
||||
|| cameraMode == CameraMode.TRACKING_GPS
|
||||
|| cameraMode == CameraMode.TRACKING_GPS_NORTH;
|
||||
}
|
||||
|
||||
private boolean isBearingTracking() {
|
||||
return cameraMode == CameraMode.NONE_COMPASS
|
||||
|| cameraMode == CameraMode.TRACKING_COMPASS
|
||||
|| cameraMode == CameraMode.NONE_GPS
|
||||
|| cameraMode == CameraMode.TRACKING_GPS
|
||||
|| cameraMode == CameraMode.TRACKING_GPS_NORTH;
|
||||
}
|
||||
|
||||
private boolean isLocationBearingTracking() {
|
||||
return cameraMode == CameraMode.TRACKING_GPS
|
||||
|| cameraMode == CameraMode.TRACKING_GPS_NORTH
|
||||
|| cameraMode == CameraMode.NONE_GPS;
|
||||
}
|
||||
|
||||
private void notifyCameraTrackingChangeListener(boolean wasTracking) {
|
||||
internalCameraTrackingChangedListener.onCameraTrackingChanged(cameraMode);
|
||||
if (wasTracking && !isLocationTracking()) {
|
||||
maplibreMap.getUiSettings().setFocalPoint(null);
|
||||
internalCameraTrackingChangedListener.onCameraTrackingDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
private MapLibreMap.OnCameraMoveListener onCameraMoveListener = new MapLibreMap.OnCameraMoveListener() {
|
||||
|
||||
@Override
|
||||
public void onCameraMove() {
|
||||
if (isLocationTracking() && lastLocation != null && options.trackingGesturesManagement()) {
|
||||
PointF focalPoint = maplibreMap.getProjection().toScreenLocation(lastLocation);
|
||||
maplibreMap.getUiSettings().setFocalPoint(focalPoint);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
@VisibleForTesting
|
||||
MapLibreMap.OnMoveListener onMoveListener = new MapLibreMap.OnMoveListener() {
|
||||
private boolean interrupt;
|
||||
|
||||
@Override
|
||||
public void onMoveBegin(@NonNull MoveGestureDetector detector) {
|
||||
if (options.trackingGesturesManagement() && isLocationTracking()) {
|
||||
if (detector.getPointersCount() > 1) {
|
||||
applyMultiFingerThresholdArea(detector);
|
||||
applyMultiFingerMoveThreshold(detector);
|
||||
} else {
|
||||
applySingleFingerMoveThreshold(detector);
|
||||
}
|
||||
} else {
|
||||
setCameraMode(CameraMode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMultiFingerThresholdArea(@NonNull MoveGestureDetector detector) {
|
||||
RectF currentRect = detector.getMoveThresholdRect();
|
||||
if (currentRect != null && !currentRect.equals(options.trackingMultiFingerProtectedMoveArea())) {
|
||||
detector.setMoveThresholdRect(options.trackingMultiFingerProtectedMoveArea());
|
||||
interrupt = true;
|
||||
} else if (currentRect == null && options.trackingMultiFingerProtectedMoveArea() != null) {
|
||||
detector.setMoveThresholdRect(options.trackingMultiFingerProtectedMoveArea());
|
||||
interrupt = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMultiFingerMoveThreshold(@NonNull MoveGestureDetector detector) {
|
||||
if (detector.getMoveThreshold() != options.trackingMultiFingerMoveThreshold()) {
|
||||
detector.setMoveThreshold(options.trackingMultiFingerMoveThreshold());
|
||||
interrupt = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void applySingleFingerMoveThreshold(@NonNull MoveGestureDetector detector) {
|
||||
if (detector.getMoveThreshold() != options.trackingInitialMoveThreshold()) {
|
||||
detector.setMoveThreshold(options.trackingInitialMoveThreshold());
|
||||
interrupt = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMove(@NonNull MoveGestureDetector detector) {
|
||||
if (interrupt) {
|
||||
detector.interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLocationTracking() || isBearingTracking()) {
|
||||
setCameraMode(CameraMode.NONE);
|
||||
detector.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoveEnd(@NonNull MoveGestureDetector detector) {
|
||||
if (options.trackingGesturesManagement() && !interrupt && isLocationTracking()) {
|
||||
detector.setMoveThreshold(options.trackingInitialMoveThreshold());
|
||||
detector.setMoveThresholdRect(null);
|
||||
}
|
||||
interrupt = false;
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
private MapLibreMap.OnRotateListener onRotateListener = new MapLibreMap.OnRotateListener() {
|
||||
@Override
|
||||
public void onRotateBegin(@NonNull RotateGestureDetector detector) {
|
||||
if (isBearingTracking()) {
|
||||
setCameraMode(CameraMode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotate(@NonNull RotateGestureDetector detector) {
|
||||
// no implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotateEnd(@NonNull RotateGestureDetector detector) {
|
||||
// no implementation
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
private MapLibreMap.OnFlingListener onFlingListener = new MapLibreMap.OnFlingListener() {
|
||||
@Override
|
||||
public void onFling() {
|
||||
setCameraMode(CameraMode.NONE);
|
||||
}
|
||||
};
|
||||
|
||||
private class LocationGesturesManager extends AndroidGesturesManager {
|
||||
|
||||
LocationGesturesManager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(@Nullable MotionEvent motionEvent) {
|
||||
if (motionEvent != null) {
|
||||
int action = motionEvent.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
adjustGesturesThresholds();
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(motionEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.location.engine.LocationEngine;
|
||||
import org.maplibre.android.location.engine.LocationEngineRequest;
|
||||
import org.maplibre.android.maps.Style;
|
||||
|
||||
/**
|
||||
* A class which holds the various options for activating the Maps SDK's {@link LocationComponent} to eventually
|
||||
* show the device location on the map.
|
||||
*/
|
||||
public class LocationComponentActivationOptions {
|
||||
|
||||
private final Context context;
|
||||
private final Style style;
|
||||
private final LocationEngine locationEngine;
|
||||
private final LocationEngineRequest locationEngineRequest;
|
||||
private final LocationComponentOptions locationComponentOptions;
|
||||
private final int styleRes;
|
||||
private final boolean useDefaultLocationEngine;
|
||||
private final boolean useSpecializedLocationLayer;
|
||||
|
||||
private LocationComponentActivationOptions(@NonNull Context context, @NonNull Style style,
|
||||
@Nullable LocationEngine locationEngine,
|
||||
@Nullable LocationEngineRequest locationEngineRequest,
|
||||
@Nullable LocationComponentOptions locationComponentOptions,
|
||||
int styleRes,
|
||||
boolean useDefaultLocationEngine,
|
||||
boolean useSpecializedLocationLayer) {
|
||||
this.context = context;
|
||||
this.style = style;
|
||||
this.locationEngine = locationEngine;
|
||||
this.locationEngineRequest = locationEngineRequest;
|
||||
this.locationComponentOptions = locationComponentOptions;
|
||||
this.styleRes = styleRes;
|
||||
this.useDefaultLocationEngine = useDefaultLocationEngine;
|
||||
this.useSpecializedLocationLayer = useSpecializedLocationLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to retrieve a {@link LocationComponentActivationOptions} object which is ready to build with
|
||||
*
|
||||
* @return a builder object
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Context context, @NonNull Style fullyLoadedMapStyle) {
|
||||
return new LocationComponentActivationOptions.Builder(context, fullyLoadedMapStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The application's current context
|
||||
*
|
||||
* @return the application's current context
|
||||
*/
|
||||
@NonNull
|
||||
public Context context() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* The proxy object for current map style. More info at {@link Style}
|
||||
*
|
||||
* @return the map's fully loaded Style object
|
||||
*/
|
||||
@NonNull
|
||||
public Style style() {
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link LocationEngine} which the {@link LocationComponent} should use
|
||||
*
|
||||
* @return the engine
|
||||
*/
|
||||
@Nullable
|
||||
public LocationEngine locationEngine() {
|
||||
return locationEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* The location request which the {@link LocationComponent} should use
|
||||
*
|
||||
* @return the LocationEngineRequest object
|
||||
*/
|
||||
@Nullable
|
||||
public LocationEngineRequest locationEngineRequest() {
|
||||
return locationEngineRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* A built {@link LocationComponentOptions} object, which holds the various {@link LocationComponent} styling options
|
||||
*
|
||||
* @return the options for styling the actual LocationComponent
|
||||
*/
|
||||
@Nullable
|
||||
public LocationComponentOptions locationComponentOptions() {
|
||||
return locationComponentOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The LocationComponent style resource (e.g. R.style.style_name)
|
||||
*
|
||||
* @return the style resource.
|
||||
*/
|
||||
public int styleRes() {
|
||||
return styleRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if you want to initialize and use the built-in location engine or false if there should be no
|
||||
* location engine initialized
|
||||
*
|
||||
* @return whether the default LocationEngine is used
|
||||
*/
|
||||
public boolean useDefaultLocationEngine() {
|
||||
return useDefaultLocationEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if you want to initialize and use the specialized location layer to render the location puck.
|
||||
* <p>
|
||||
* The specialized layer is not based on runtime styling. This brings significant performance improvements,
|
||||
* but since it's not based on the runtime styling,
|
||||
* it's not fully compatible with the traditional implementation. The incompatibilities are:
|
||||
* <ul>
|
||||
* <li> Constants like {@link LocationComponentConstants#BACKGROUND_LAYER},
|
||||
* {@link LocationComponentConstants#ACCURACY_LAYER}
|
||||
* or {@link LocationComponentConstants#LOCATION_SOURCE} are ignored.
|
||||
* The only usable and valid for relative positioning layer ID is
|
||||
* {@link LocationComponentConstants#FOREGROUND_LAYER}.
|
||||
*
|
||||
* <li> All options that alter the image ID, like {@link LocationComponentOptions#foregroundName()}, are ignored.
|
||||
* Use {@link LocationComponentOptions#foregroundDrawable()} to alter the image rendered as the puck.
|
||||
* <li> The LocationComponent's pulsing effect. Any of the {@link LocationComponentOptions}'
|
||||
* pulsing methods such as {@link LocationComponentOptions#pulseEnabled()} or
|
||||
* {@link LocationComponentOptions#pulseColor()}, are useless when using the
|
||||
* the specialized location layer.
|
||||
* </ul>
|
||||
*
|
||||
* @return whether the default specialized location layer is used
|
||||
*/
|
||||
public boolean useSpecializedLocationLayer() {
|
||||
return useSpecializedLocationLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for constructing a new instance of {@link LocationComponentActivationOptions}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Context context;
|
||||
private final Style style;
|
||||
private LocationEngine locationEngine;
|
||||
private LocationEngineRequest locationEngineRequest;
|
||||
private LocationComponentOptions locationComponentOptions;
|
||||
private int styleRes;
|
||||
|
||||
/**
|
||||
* Set to true as the default in case a true/false value isn't declared via the builder's
|
||||
* {@link LocationComponentActivationOptions#useDefaultLocationEngine()} method.
|
||||
* <p>
|
||||
* Please be aware that this activation boolean is ignored when a non-null
|
||||
* {@link LocationEngine} is set via the builder's `locationEngine()` method.
|
||||
*/
|
||||
private boolean useDefaultLocationEngine = true;
|
||||
|
||||
private boolean useSpecializedLocationLayer = false;
|
||||
|
||||
/**
|
||||
* Constructor for the {@link LocationComponentActivationOptions} builder class.
|
||||
* While other activation options are optional, the activation process always requires
|
||||
* context and a fully-loaded map {@link Style} object, which is why the two are in this
|
||||
* constructor.
|
||||
*/
|
||||
public Builder(@NonNull Context context, @NonNull Style style) {
|
||||
this.context = context;
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver your own {@link LocationEngine} to the LocationComponent.
|
||||
* <p>
|
||||
* The true/false
|
||||
* {@link LocationComponentActivationOptions.Builder#useDefaultLocationEngine()}
|
||||
* activation option is ignored when a non-null {@link LocationEngine} is set via
|
||||
* this `locationEngine()` method.
|
||||
*
|
||||
* @param locationEngine a {@link LocationEngine} object
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
@NonNull
|
||||
public Builder locationEngine(@Nullable LocationEngine locationEngine) {
|
||||
this.locationEngine = locationEngine;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param locationEngineRequest the location request which the
|
||||
* {@link LocationComponent} should use
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
public Builder locationEngineRequest(LocationEngineRequest locationEngineRequest) {
|
||||
this.locationEngineRequest = locationEngineRequest;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param locationComponentOptions a built {@link LocationComponentOptions} object,
|
||||
* which holds the various {@link LocationComponent}
|
||||
* styling options
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
public Builder locationComponentOptions(LocationComponentOptions locationComponentOptions) {
|
||||
this.locationComponentOptions = locationComponentOptions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param styleRes the LocationComponent style resource (e.g. R.style.style_name)
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
public Builder styleRes(int styleRes) {
|
||||
this.styleRes = styleRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param useDefaultLocationEngine true if you want to initialize and use the
|
||||
* built-in location engine or false if there
|
||||
* should be no location engine initialized
|
||||
* This is ignored when null is set as the parameter
|
||||
* for {@link LocationComponentActivationOptions#builder
|
||||
* (Context, Style)#locationEngine()}.
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
public Builder useDefaultLocationEngine(boolean useDefaultLocationEngine) {
|
||||
this.useDefaultLocationEngine = useDefaultLocationEngine;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if you want to initialize and use the specialized location layer to render the location puck.
|
||||
* <p>
|
||||
* The specialized layer is not based on runtime styling. This brings significant performance improvements,
|
||||
* but since it's not based on the runtime styling,
|
||||
* it's not fully compatible with the traditional implementation. The incompatibilities are:
|
||||
* <ul>
|
||||
* <li> Constants like {@link LocationComponentConstants#BACKGROUND_LAYER},
|
||||
* {@link LocationComponentConstants#ACCURACY_LAYER}
|
||||
* or {@link LocationComponentConstants#LOCATION_SOURCE} are ignored.
|
||||
* The only usable and valid for relative positioning layer ID is
|
||||
* {@link LocationComponentConstants#FOREGROUND_LAYER}.
|
||||
*
|
||||
* <li> All options that alter the image ID, like {@link LocationComponentOptions#foregroundName()}, are ignored.
|
||||
* Use {@link LocationComponentOptions#foregroundDrawable()} to alter the image rendered as the puck.
|
||||
* <li> The LocationComponent's pulsing effect. Any of the {@link LocationComponentOptions}'
|
||||
* pulsing methods such as {@link LocationComponentOptions#pulseEnabled()} or
|
||||
* {@link LocationComponentOptions#pulseColor()}, are useless when using the
|
||||
* the specialized location layer.
|
||||
* </ul>
|
||||
*
|
||||
* @param useSpecializedLocationLayer true if you want to initialize and use the
|
||||
* specialized location layer. Defaults to false.
|
||||
* @return the {@link Builder} object being constructed
|
||||
*/
|
||||
public Builder useSpecializedLocationLayer(boolean useSpecializedLocationLayer) {
|
||||
this.useSpecializedLocationLayer = useSpecializedLocationLayer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method which actually builds the {@link LocationComponentActivationOptions} object while
|
||||
* taking the various options into account.
|
||||
*
|
||||
* @return a built {@link LocationComponentActivationOptions} object
|
||||
*/
|
||||
public LocationComponentActivationOptions build() {
|
||||
if (styleRes != 0 && locationComponentOptions != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"You've provided both a style resource and a LocationComponentOptions object to the "
|
||||
+ "LocationComponentActivationOptions builder. You can't use both and "
|
||||
+ "you must choose one of the two to style the LocationComponent.");
|
||||
}
|
||||
if (context == null) {
|
||||
throw new NullPointerException(
|
||||
"Context in LocationComponentActivationOptions is null.");
|
||||
}
|
||||
if (style == null) {
|
||||
throw new NullPointerException(
|
||||
"Style in LocationComponentActivationOptions is null. Make sure the "
|
||||
+ "Style object isn't null. Wait for the map to fully load before passing "
|
||||
+ "the Style object to LocationComponentActivationOptions.");
|
||||
}
|
||||
if (!style.isFullyLoaded()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Style in LocationComponentActivationOptions isn't fully loaded. Wait for the "
|
||||
+ "map to fully load before passing the Style object to "
|
||||
+ "LocationComponentActivationOptions.");
|
||||
}
|
||||
return new LocationComponentActivationOptions(context, style, locationEngine,
|
||||
locationEngineRequest, locationComponentOptions, styleRes, useDefaultLocationEngine,
|
||||
useSpecializedLocationLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.log.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This manager class handles compass events such as starting the tracking of device bearing, or
|
||||
* when a new compass update occurs.
|
||||
*/
|
||||
class LocationComponentCompassEngine implements CompassEngine, SensorEventListener {
|
||||
|
||||
private static final String TAG = "Mbgl-LocationComponentCompassEngine";
|
||||
|
||||
// The rate sensor events will be delivered at. As the Android documentation states, this is only
|
||||
// a hint to the system and the events might actually be received faster or slower then this
|
||||
// specified rate. Since the minimum Android API levels about 9, we are able to set this value
|
||||
// ourselves rather than using one of the provided constants which deliver updates too quickly for
|
||||
// our use case. The default is set to 100ms
|
||||
static final int SENSOR_DELAY_MICROS = 100 * 1000;
|
||||
// Filtering coefficient 0 < ALPHA < 1
|
||||
private static final float ALPHA = 0.45f;
|
||||
|
||||
@NonNull
|
||||
private final WindowManager windowManager;
|
||||
@NonNull
|
||||
private final SensorManager sensorManager;
|
||||
private final List<CompassListener> compassListeners = new ArrayList<>();
|
||||
|
||||
// Not all devices have a compassSensor
|
||||
@Nullable
|
||||
private Sensor compassSensor;
|
||||
@Nullable
|
||||
private Sensor gravitySensor;
|
||||
@Nullable
|
||||
private Sensor magneticFieldSensor;
|
||||
|
||||
@NonNull
|
||||
private float[] truncatedRotationVectorValue = new float[4];
|
||||
@NonNull
|
||||
private float[] rotationMatrix = new float[9];
|
||||
private float[] rotationVectorValue;
|
||||
private float lastHeading;
|
||||
private int lastAccuracySensorStatus;
|
||||
|
||||
private long compassUpdateNextTimestamp;
|
||||
@Nullable
|
||||
private float[] gravityValues = new float[3];
|
||||
@Nullable
|
||||
private float[] magneticValues = new float[3];
|
||||
|
||||
/**
|
||||
* Construct a new instance of the this class. A internal compass listeners needed to separate it
|
||||
* from the cleared list of public listeners.
|
||||
*/
|
||||
LocationComponentCompassEngine(@NonNull WindowManager windowManager, @NonNull SensorManager sensorManager) {
|
||||
this.windowManager = windowManager;
|
||||
this.sensorManager = sensorManager;
|
||||
compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
|
||||
if (compassSensor == null) {
|
||||
Logger.d(TAG, "Rotation vector sensor not supported on device, "
|
||||
+ "falling back to accelerometer and magnetic field.");
|
||||
gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||
magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCompassListener(@NonNull CompassListener compassListener) {
|
||||
if (compassListeners.isEmpty()) {
|
||||
registerSensorListeners();
|
||||
}
|
||||
compassListeners.add(compassListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCompassListener(@NonNull CompassListener compassListener) {
|
||||
compassListeners.remove(compassListener);
|
||||
if (compassListeners.isEmpty()) {
|
||||
unregisterSensorListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastAccuracySensorStatus() {
|
||||
return lastAccuracySensorStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getLastHeading() {
|
||||
return lastHeading;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(@NonNull SensorEvent event) {
|
||||
if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) {
|
||||
Logger.d(TAG, "Compass sensor is unreliable, device calibration is needed.");
|
||||
// Update the heading, even if the sensor is unreliable.
|
||||
// This makes it possible to use a different indicator for the unreliable case,
|
||||
// instead of just changing the RenderMode to NORMAL.
|
||||
}
|
||||
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
|
||||
rotationVectorValue = getRotationVectorFromSensorEvent(event);
|
||||
updateOrientation();
|
||||
} else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues);
|
||||
updateOrientation();
|
||||
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
|
||||
magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues);
|
||||
updateOrientation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
if (lastAccuracySensorStatus != accuracy) {
|
||||
for (CompassListener compassListener : compassListeners) {
|
||||
compassListener.onCompassAccuracyChange(accuracy);
|
||||
}
|
||||
lastAccuracySensorStatus = accuracy;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void updateOrientation() {
|
||||
// check when the last time the compass was updated, return if too soon.
|
||||
long currentTime = SystemClock.elapsedRealtime();
|
||||
if (currentTime < compassUpdateNextTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rotationVectorValue != null) {
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue);
|
||||
} else {
|
||||
// Get rotation matrix given the gravity and geomagnetic matrices
|
||||
SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues);
|
||||
}
|
||||
|
||||
int worldAxisForDeviceAxisX;
|
||||
int worldAxisForDeviceAxisY;
|
||||
|
||||
// Assume the device screen was parallel to the ground,
|
||||
// and adjust the rotation matrix for the device orientation.
|
||||
switch (windowManager.getDefaultDisplay().getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_Y;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Y;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Y;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_Y;
|
||||
break;
|
||||
}
|
||||
|
||||
float[] adjustedRotationMatrix = new float[9];
|
||||
SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
|
||||
worldAxisForDeviceAxisY, adjustedRotationMatrix);
|
||||
|
||||
// Transform rotation matrix into azimuth/pitch/roll
|
||||
float[] orientation = new float[3];
|
||||
SensorManager.getOrientation(adjustedRotationMatrix, orientation);
|
||||
|
||||
if (orientation[1] < -Math.PI / 4) {
|
||||
// The pitch is less than -45 degrees.
|
||||
// Remap the axes as if the device screen was the instrument panel.
|
||||
switch (windowManager.getDefaultDisplay().getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
|
||||
break;
|
||||
}
|
||||
} else if (orientation[1] > Math.PI / 4) {
|
||||
// The pitch is larger than 45 degrees.
|
||||
// Remap the axes as if the device screen was upside down and facing back.
|
||||
switch (windowManager.getDefaultDisplay().getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
|
||||
break;
|
||||
}
|
||||
} else if (Math.abs(orientation[2]) > Math.PI / 2) {
|
||||
// The roll is less than -90 degrees, or is larger than 90 degrees.
|
||||
// Remap the axes as if the device screen was face down.
|
||||
switch (windowManager.getDefaultDisplay().getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Y;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_Y;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_Y;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
|
||||
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
|
||||
worldAxisForDeviceAxisY, adjustedRotationMatrix);
|
||||
|
||||
// Transform rotation matrix into azimuth/pitch/roll
|
||||
SensorManager.getOrientation(adjustedRotationMatrix, orientation);
|
||||
|
||||
// The x-axis is all we care about here.
|
||||
notifyCompassChangeListeners((float) Math.toDegrees(orientation[0]));
|
||||
|
||||
// Update the compassUpdateNextTimestamp
|
||||
compassUpdateNextTimestamp = currentTime + LocationComponentConstants.COMPASS_UPDATE_RATE_MS;
|
||||
}
|
||||
|
||||
private void notifyCompassChangeListeners(float heading) {
|
||||
for (CompassListener compassListener : compassListeners) {
|
||||
compassListener.onCompassChanged(heading);
|
||||
}
|
||||
lastHeading = heading;
|
||||
}
|
||||
|
||||
private void registerSensorListeners() {
|
||||
if (isCompassSensorAvailable()) {
|
||||
// Does nothing if the sensors already registered.
|
||||
sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS);
|
||||
} else {
|
||||
sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS);
|
||||
sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS);
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterSensorListeners() {
|
||||
if (isCompassSensorAvailable()) {
|
||||
sensorManager.unregisterListener(this, compassSensor);
|
||||
} else {
|
||||
sensorManager.unregisterListener(this, gravitySensor);
|
||||
sensorManager.unregisterListener(this, magneticFieldSensor);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCompassSensorAvailable() {
|
||||
return compassSensor != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, that filters newValues, considering previous values
|
||||
*
|
||||
* @param newValues array of float, that contains new data
|
||||
* @param smoothedValues array of float, that contains previous state
|
||||
* @return float filtered array of float
|
||||
*/
|
||||
@NonNull
|
||||
private float[] lowPassFilter(@NonNull float[] newValues, @Nullable float[] smoothedValues) {
|
||||
if (smoothedValues == null) {
|
||||
return newValues;
|
||||
}
|
||||
for (int i = 0; i < newValues.length; i++) {
|
||||
smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]);
|
||||
}
|
||||
return smoothedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls out the rotation vector from a SensorEvent, with a maximum length
|
||||
* vector of four elements to avoid potential compatibility issues.
|
||||
*
|
||||
* @param event the sensor event
|
||||
* @return the events rotation vector, potentially truncated
|
||||
*/
|
||||
@NonNull
|
||||
private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) {
|
||||
if (event.values.length > 4) {
|
||||
// On some Samsung devices SensorManager.getRotationMatrixFromVector
|
||||
// appears to throw an exception if rotation vector has length > 4.
|
||||
// For the purposes of this class the first 4 values of the
|
||||
// rotation vector are sufficient (see crbug.com/335298 for details).
|
||||
// Only affects Android 4.3
|
||||
System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4);
|
||||
return truncatedRotationVectorValue;
|
||||
} else {
|
||||
return event.values;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* Contains all the constants being used for the {@link LocationComponent}.
|
||||
*/
|
||||
public final class LocationComponentConstants {
|
||||
|
||||
// Controls the compass update rate in milliseconds
|
||||
static final long COMPASS_UPDATE_RATE_MS = 500;
|
||||
|
||||
// Sets the transition animation duration when switching camera modes.
|
||||
static final long TRANSITION_ANIMATION_DURATION_MS = 750;
|
||||
|
||||
// Sets the max allowed time for the location icon animation from one LatLng to another.
|
||||
static final long MAX_ANIMATION_DURATION_MS = 2000;
|
||||
|
||||
// Sets the duration of change of accuracy radius when a different value is provided.
|
||||
static final long ACCURACY_RADIUS_ANIMATION_DURATION = 250;
|
||||
|
||||
// Default animation duration for zooming while tracking.
|
||||
static final long DEFAULT_TRACKING_ZOOM_ANIM_DURATION = 750;
|
||||
|
||||
// Default animation duration for updating padding while tracking.
|
||||
static final long DEFAULT_TRACKING_PADDING_ANIM_DURATION = 750;
|
||||
|
||||
// Default animation duration for tilting while tracking.
|
||||
static final long DEFAULT_TRACKING_TILT_ANIM_DURATION = 1250;
|
||||
|
||||
// Threshold value to perform immediate camera/layer position update.
|
||||
static final double INSTANT_LOCATION_TRANSITION_THRESHOLD = 50_000;
|
||||
|
||||
// Default interval between location updates
|
||||
static final long DEFAULT_INTERVAL_MILLIS = 1000;
|
||||
|
||||
// Default fastest acceptable interval between location updates
|
||||
static final long DEFAULT_FASTEST_INTERVAL_MILLIS = 1000;
|
||||
|
||||
// Sources
|
||||
|
||||
/**
|
||||
* Source ID of the location's GeoJsonSource.
|
||||
*/
|
||||
public static final String LOCATION_SOURCE = "mapbox-location-source";
|
||||
|
||||
static final String PROPERTY_GPS_BEARING = "mapbox-property-gps-bearing";
|
||||
static final String PROPERTY_COMPASS_BEARING = "mapbox-property-compass-bearing";
|
||||
static final String PROPERTY_LOCATION_STALE = "mapbox-property-location-stale";
|
||||
static final String PROPERTY_ACCURACY_RADIUS = "mapbox-property-accuracy-radius";
|
||||
static final String PROPERTY_ACCURACY_COLOR = "mapbox-property-accuracy-color";
|
||||
static final String PROPERTY_ACCURACY_ALPHA = "mapbox-property-accuracy-alpha";
|
||||
static final String PROPERTY_FOREGROUND_ICON_OFFSET = "mapbox-property-foreground-icon-offset";
|
||||
static final String PROPERTY_SHADOW_ICON_OFFSET = "mapbox-property-shadow-icon-offset";
|
||||
static final String PROPERTY_FOREGROUND_ICON = "mapbox-property-foreground-icon";
|
||||
static final String PROPERTY_BACKGROUND_ICON = "mapbox-property-background-icon";
|
||||
static final String PROPERTY_FOREGROUND_STALE_ICON = "mapbox-property-foreground-stale-icon";
|
||||
static final String PROPERTY_BACKGROUND_STALE_ICON = "mapbox-property-background-stale-icon";
|
||||
static final String PROPERTY_BEARING_ICON = "mapbox-property-shadow-icon";
|
||||
static final String PROPERTY_PULSING_RADIUS = "mapbox-property-pulsing-circle-radius";
|
||||
static final String PROPERTY_PULSING_OPACITY = "mapbox-property-pulsing-circle-opacity";
|
||||
|
||||
// Layers
|
||||
|
||||
/**
|
||||
* Layer ID of the location shadow.
|
||||
*/
|
||||
public static final String SHADOW_LAYER = "mapbox-location-shadow-layer";
|
||||
|
||||
/**
|
||||
* Layer ID of the location foreground icon or the only runtime layer added if
|
||||
* {@link LocationComponentActivationOptions#useSpecializedLocationLayer()} is used.
|
||||
*/
|
||||
public static final String FOREGROUND_LAYER = "mapbox-location-foreground-layer";
|
||||
|
||||
/**
|
||||
* Layer ID of the location background icon.
|
||||
*/
|
||||
public static final String BACKGROUND_LAYER = "mapbox-location-background-layer";
|
||||
|
||||
/**
|
||||
* Layer ID of the location accuracy.
|
||||
*/
|
||||
public static final String ACCURACY_LAYER = "mapbox-location-accuracy-layer";
|
||||
|
||||
/**
|
||||
* Layer ID of the location bearing icon.
|
||||
*/
|
||||
public static final String BEARING_LAYER = "mapbox-location-bearing-layer";
|
||||
|
||||
/**
|
||||
* Layer ID of the location pulsing circle.
|
||||
*/
|
||||
public static final String PULSING_CIRCLE_LAYER = "mapbox-location-pulsing-circle-layer";
|
||||
|
||||
// Icons
|
||||
static final String FOREGROUND_ICON = "mapbox-location-icon";
|
||||
static final String BACKGROUND_ICON = "mapbox-location-stroke-icon";
|
||||
static final String FOREGROUND_STALE_ICON = "mapbox-location-stale-icon";
|
||||
static final String BACKGROUND_STALE_ICON = "mapbox-location-background-stale-icon";
|
||||
static final String SHADOW_ICON = "mapbox-location-shadow-icon";
|
||||
static final String BEARING_ICON = "mapbox-location-bearing-icon";
|
||||
static final String BEARING_STALE_ICON = "mapbox-location-bearing-stale-icon";
|
||||
|
||||
private LocationComponentConstants() {
|
||||
// Class should not be initialized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
final class LocationComponentNotInitializedException extends RuntimeException {
|
||||
LocationComponentNotInitializedException() {
|
||||
super("The LocationComponent has to be activated with one of the LocationComponent#activateLocationComponent"
|
||||
+ " overloads before any other methods are invoked.");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
|
||||
class LocationComponentPositionManager {
|
||||
|
||||
@NonNull
|
||||
private final Style style;
|
||||
|
||||
@Nullable
|
||||
private String layerAbove;
|
||||
|
||||
@Nullable
|
||||
private String layerBelow;
|
||||
|
||||
LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove, @Nullable String layerBelow) {
|
||||
this.style = style;
|
||||
this.layerAbove = layerAbove;
|
||||
this.layerBelow = layerBelow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true whenever layer above/below configuration has changed and requires re-layout.
|
||||
*/
|
||||
boolean update(@Nullable String layerAbove, @Nullable String layerBelow) {
|
||||
boolean requiresUpdate =
|
||||
!(this.layerAbove == layerAbove || (this.layerAbove != null && this.layerAbove.equals(layerAbove)))
|
||||
|| !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow)));
|
||||
|
||||
this.layerAbove = layerAbove;
|
||||
this.layerBelow = layerBelow;
|
||||
return requiresUpdate;
|
||||
}
|
||||
|
||||
void addLayerToMap(@NonNull Layer layer) {
|
||||
if (layerAbove != null) {
|
||||
style.addLayerAbove(layer, layerAbove);
|
||||
} else if (layerBelow != null) {
|
||||
style.addLayerBelow(layer, layerBelow);
|
||||
} else {
|
||||
style.addLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
// This file is generated. Edit scripts/generate-style-code.js, then run `make style-code`.
|
||||
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import static org.maplibre.android.utils.ColorUtils.rgbaToColor;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.style.layers.TransitionOptions;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
import org.maplibre.android.style.layers.PropertyValue;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @see <a href="https://maplibre.org/maplibre-style-spec/#layers-location-indicator">The online documentation</a>
|
||||
*/
|
||||
@UiThread
|
||||
class LocationIndicatorLayer extends Layer {
|
||||
|
||||
/**
|
||||
* Creates a LocationIndicatorLayer.
|
||||
*
|
||||
* @param nativePtr pointer used by core
|
||||
*/
|
||||
@Keep
|
||||
LocationIndicatorLayer(long nativePtr) {
|
||||
super(nativePtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LocationIndicatorLayer.
|
||||
*
|
||||
* @param layerId the id of the layer
|
||||
*/
|
||||
public LocationIndicatorLayer(String layerId) {
|
||||
super();
|
||||
initialize(layerId);
|
||||
}
|
||||
|
||||
@Keep
|
||||
protected native void initialize(String layerId);
|
||||
|
||||
/**
|
||||
* Set a property or properties.
|
||||
*
|
||||
* @param properties the var-args properties
|
||||
* @return This
|
||||
*/
|
||||
@NonNull
|
||||
public LocationIndicatorLayer withProperties(@NonNull PropertyValue<?>... properties) {
|
||||
setProperties(properties);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Property getters
|
||||
|
||||
/**
|
||||
* Get the TopImage property
|
||||
*
|
||||
* @return property wrapper value around String
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<String> getTopImage() {
|
||||
checkThread();
|
||||
return (PropertyValue<String>) new PropertyValue("top-image", nativeGetTopImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BearingImage property
|
||||
*
|
||||
* @return property wrapper value around String
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<String> getBearingImage() {
|
||||
checkThread();
|
||||
return (PropertyValue<String>) new PropertyValue("bearing-image", nativeGetBearingImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ShadowImage property
|
||||
*
|
||||
* @return property wrapper value around String
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<String> getShadowImage() {
|
||||
checkThread();
|
||||
return (PropertyValue<String>) new PropertyValue("shadow-image", nativeGetShadowImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PerspectiveCompensation property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getPerspectiveCompensation() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("perspective-compensation", nativeGetPerspectiveCompensation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ImageTiltDisplacement property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getImageTiltDisplacement() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("image-tilt-displacement", nativeGetImageTiltDisplacement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bearing property
|
||||
*
|
||||
* @return property wrapper value around Double
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Double> getBearing() {
|
||||
checkThread();
|
||||
return (PropertyValue<Double>) new PropertyValue("bearing", nativeGetBearing());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Location property
|
||||
*
|
||||
* @return property wrapper value around Double[]
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Double[]> getLocation() {
|
||||
checkThread();
|
||||
return (PropertyValue<Double[]>) new PropertyValue("location", nativeGetLocation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Location property transition options
|
||||
*
|
||||
* @return transition options for Double[]
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getLocationTransition() {
|
||||
checkThread();
|
||||
return nativeGetLocationTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Location property transition options
|
||||
*
|
||||
* @param options transition options for Double[]
|
||||
*/
|
||||
public void setLocationTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetLocationTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadius property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getAccuracyRadius() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("accuracy-radius", nativeGetAccuracyRadius());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadius property transition options
|
||||
*
|
||||
* @return transition options for Float
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getAccuracyRadiusTransition() {
|
||||
checkThread();
|
||||
return nativeGetAccuracyRadiusTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the AccuracyRadius property transition options
|
||||
*
|
||||
* @param options transition options for Float
|
||||
*/
|
||||
public void setAccuracyRadiusTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetAccuracyRadiusTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TopImageSize property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getTopImageSize() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("top-image-size", nativeGetTopImageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TopImageSize property transition options
|
||||
*
|
||||
* @return transition options for Float
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getTopImageSizeTransition() {
|
||||
checkThread();
|
||||
return nativeGetTopImageSizeTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the TopImageSize property transition options
|
||||
*
|
||||
* @param options transition options for Float
|
||||
*/
|
||||
public void setTopImageSizeTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetTopImageSizeTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BearingImageSize property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getBearingImageSize() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("bearing-image-size", nativeGetBearingImageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BearingImageSize property transition options
|
||||
*
|
||||
* @return transition options for Float
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getBearingImageSizeTransition() {
|
||||
checkThread();
|
||||
return nativeGetBearingImageSizeTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the BearingImageSize property transition options
|
||||
*
|
||||
* @param options transition options for Float
|
||||
*/
|
||||
public void setBearingImageSizeTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetBearingImageSizeTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ShadowImageSize property
|
||||
*
|
||||
* @return property wrapper value around Float
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<Float> getShadowImageSize() {
|
||||
checkThread();
|
||||
return (PropertyValue<Float>) new PropertyValue("shadow-image-size", nativeGetShadowImageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ShadowImageSize property transition options
|
||||
*
|
||||
* @return transition options for Float
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getShadowImageSizeTransition() {
|
||||
checkThread();
|
||||
return nativeGetShadowImageSizeTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ShadowImageSize property transition options
|
||||
*
|
||||
* @param options transition options for Float
|
||||
*/
|
||||
public void setShadowImageSizeTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetShadowImageSizeTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadiusColor property
|
||||
*
|
||||
* @return property wrapper value around String
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<String> getAccuracyRadiusColor() {
|
||||
checkThread();
|
||||
return (PropertyValue<String>) new PropertyValue("accuracy-radius-color", nativeGetAccuracyRadiusColor());
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius, as a circle. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @return int representation of a rgba string color
|
||||
* @throws RuntimeException thrown if property isn't a value
|
||||
*/
|
||||
@ColorInt
|
||||
public int getAccuracyRadiusColorAsInt() {
|
||||
checkThread();
|
||||
PropertyValue<String> value = getAccuracyRadiusColor();
|
||||
if (value.isValue()) {
|
||||
return rgbaToColor(value.getValue());
|
||||
} else {
|
||||
throw new RuntimeException("accuracy-radius-color was set as a Function");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadiusColor property transition options
|
||||
*
|
||||
* @return transition options for String
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getAccuracyRadiusColorTransition() {
|
||||
checkThread();
|
||||
return nativeGetAccuracyRadiusColorTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the AccuracyRadiusColor property transition options
|
||||
*
|
||||
* @param options transition options for String
|
||||
*/
|
||||
public void setAccuracyRadiusColorTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetAccuracyRadiusColorTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadiusBorderColor property
|
||||
*
|
||||
* @return property wrapper value around String
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertyValue<String> getAccuracyRadiusBorderColor() {
|
||||
checkThread();
|
||||
return (PropertyValue<String>) new PropertyValue("accuracy-radius-border-color", nativeGetAccuracyRadiusBorderColor());
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius border. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @return int representation of a rgba string color
|
||||
* @throws RuntimeException thrown if property isn't a value
|
||||
*/
|
||||
@ColorInt
|
||||
public int getAccuracyRadiusBorderColorAsInt() {
|
||||
checkThread();
|
||||
PropertyValue<String> value = getAccuracyRadiusBorderColor();
|
||||
if (value.isValue()) {
|
||||
return rgbaToColor(value.getValue());
|
||||
} else {
|
||||
throw new RuntimeException("accuracy-radius-border-color was set as a Function");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AccuracyRadiusBorderColor property transition options
|
||||
*
|
||||
* @return transition options for String
|
||||
*/
|
||||
@NonNull
|
||||
public TransitionOptions getAccuracyRadiusBorderColorTransition() {
|
||||
checkThread();
|
||||
return nativeGetAccuracyRadiusBorderColorTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the AccuracyRadiusBorderColor property transition options
|
||||
*
|
||||
* @param options transition options for String
|
||||
*/
|
||||
public void setAccuracyRadiusBorderColorTransition(@NonNull TransitionOptions options) {
|
||||
checkThread();
|
||||
nativeSetAccuracyRadiusBorderColorTransition(options.getDuration(), options.getDelay());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetTopImage();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetBearingImage();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetShadowImage();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetPerspectiveCompensation();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetImageTiltDisplacement();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetBearing();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetLocation();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetLocationTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetLocationTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetAccuracyRadius();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetAccuracyRadiusTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetAccuracyRadiusTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetTopImageSize();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetTopImageSizeTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetTopImageSizeTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetBearingImageSize();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetBearingImageSizeTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetBearingImageSizeTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetShadowImageSize();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetShadowImageSizeTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetShadowImageSizeTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetAccuracyRadiusColor();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetAccuracyRadiusColorTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetAccuracyRadiusColorTransition(long duration, long delay);
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native Object nativeGetAccuracyRadiusBorderColor();
|
||||
|
||||
@NonNull
|
||||
@Keep
|
||||
private native TransitionOptions nativeGetAccuracyRadiusBorderColorTransition();
|
||||
|
||||
@Keep
|
||||
private native void nativeSetAccuracyRadiusBorderColorTransition(long duration, long delay);
|
||||
|
||||
@Override
|
||||
@Keep
|
||||
protected native void finalize() throws Throwable;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.mapbox.geojson.Feature;
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
import org.maplibre.android.log.Logger;
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.style.expressions.Expression.interpolate;
|
||||
import static org.maplibre.android.style.expressions.Expression.linear;
|
||||
import static org.maplibre.android.style.expressions.Expression.stop;
|
||||
import static org.maplibre.android.style.expressions.Expression.zoom;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
final class LocationLayerController {
|
||||
|
||||
private static final String TAG = "Mbgl-LocationLayerController";
|
||||
|
||||
@RenderMode.Mode
|
||||
private int renderMode;
|
||||
|
||||
private final MapLibreMap maplibreMap;
|
||||
private final LayerBitmapProvider bitmapProvider;
|
||||
private LocationComponentOptions options;
|
||||
private final OnRenderModeChangedListener internalRenderModeChangedListener;
|
||||
private final boolean useSpecializedLocationLayer;
|
||||
|
||||
private boolean isHidden = true;
|
||||
private boolean isStale;
|
||||
|
||||
private LocationComponentPositionManager positionManager;
|
||||
|
||||
private LocationLayerRenderer locationLayerRenderer;
|
||||
|
||||
LocationLayerController(MapLibreMap maplibreMap, Style style,
|
||||
LayerSourceProvider layerSourceProvider,
|
||||
LayerFeatureProvider featureProvider,
|
||||
LayerBitmapProvider bitmapProvider,
|
||||
@NonNull LocationComponentOptions options,
|
||||
@NonNull OnRenderModeChangedListener internalRenderModeChangedListener,
|
||||
boolean useSpecializedLocationLayer) {
|
||||
this.maplibreMap = maplibreMap;
|
||||
this.bitmapProvider = bitmapProvider;
|
||||
this.internalRenderModeChangedListener = internalRenderModeChangedListener;
|
||||
this.useSpecializedLocationLayer = useSpecializedLocationLayer;
|
||||
this.isStale = options.enableStaleState();
|
||||
if (useSpecializedLocationLayer) {
|
||||
locationLayerRenderer = layerSourceProvider.getIndicatorLocationLayerRenderer();
|
||||
} else {
|
||||
locationLayerRenderer =
|
||||
layerSourceProvider.getSymbolLocationLayerRenderer(featureProvider, isStale);
|
||||
}
|
||||
initializeComponents(style, options);
|
||||
}
|
||||
|
||||
void initializeComponents(Style style, LocationComponentOptions options) {
|
||||
this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(), options.layerBelow());
|
||||
locationLayerRenderer.initializeComponents(style);
|
||||
locationLayerRenderer.addLayers(positionManager);
|
||||
applyStyle(options);
|
||||
|
||||
if (isHidden) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void applyStyle(@NonNull LocationComponentOptions options) {
|
||||
if (positionManager.update(options.layerAbove(), options.layerBelow())) {
|
||||
locationLayerRenderer.removeLayers();
|
||||
locationLayerRenderer.addLayers(positionManager);
|
||||
if (isHidden) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
styleBitmaps(options);
|
||||
locationLayerRenderer.styleAccuracy(options.accuracyAlpha(), options.accuracyColor());
|
||||
styleScaling(options);
|
||||
locationLayerRenderer.stylePulsingCircle(options);
|
||||
determineIconsSource(options);
|
||||
|
||||
if (!isHidden) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void setGpsBearing(float gpsBearing) {
|
||||
locationLayerRenderer.setGpsBearing(gpsBearing);
|
||||
}
|
||||
|
||||
void setRenderMode(@RenderMode.Mode int renderMode) {
|
||||
if (this.renderMode == renderMode) {
|
||||
return;
|
||||
}
|
||||
this.renderMode = renderMode;
|
||||
|
||||
styleBitmaps(options);
|
||||
determineIconsSource(options);
|
||||
if (!isHidden) {
|
||||
show();
|
||||
}
|
||||
internalRenderModeChangedListener.onRenderModeChanged(renderMode);
|
||||
}
|
||||
|
||||
int getRenderMode() {
|
||||
return renderMode;
|
||||
}
|
||||
|
||||
//
|
||||
// Layer action
|
||||
//
|
||||
|
||||
void show() {
|
||||
isHidden = false;
|
||||
locationLayerRenderer.show(renderMode, isStale);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
isHidden = true;
|
||||
locationLayerRenderer.hide();
|
||||
}
|
||||
|
||||
boolean isHidden() {
|
||||
return isHidden;
|
||||
}
|
||||
|
||||
boolean isConsumingCompass() {
|
||||
return renderMode == RenderMode.COMPASS;
|
||||
}
|
||||
|
||||
//
|
||||
// Styling
|
||||
//
|
||||
|
||||
private void styleBitmaps(LocationComponentOptions options) {
|
||||
// shadow
|
||||
Bitmap shadowBitmap = null;
|
||||
if (options.elevation() > 0) {
|
||||
// Only add icon elevation if the values greater than 0.
|
||||
shadowBitmap = bitmapProvider.generateShadowBitmap(options);
|
||||
}
|
||||
|
||||
// background
|
||||
Bitmap backgroundBitmap = bitmapProvider.generateBitmap(
|
||||
options.backgroundDrawable(), options.backgroundTintColor()
|
||||
);
|
||||
Bitmap backgroundStaleBitmap = bitmapProvider.generateBitmap(
|
||||
options.backgroundDrawableStale(), options.backgroundStaleTintColor()
|
||||
);
|
||||
|
||||
// compass bearing
|
||||
Bitmap bearingBitmap = bitmapProvider.generateBitmap(options.bearingDrawable(), options.bearingTintColor());
|
||||
|
||||
// foreground
|
||||
Bitmap foregroundBitmap = bitmapProvider.generateBitmap(
|
||||
options.foregroundDrawable(), options.foregroundTintColor()
|
||||
);
|
||||
Bitmap foregroundStaleBitmap = bitmapProvider.generateBitmap(
|
||||
options.foregroundDrawableStale(), options.foregroundStaleTintColor()
|
||||
);
|
||||
if (renderMode == RenderMode.GPS) {
|
||||
foregroundBitmap = bitmapProvider.generateBitmap(
|
||||
options.gpsDrawable(), options.foregroundTintColor()
|
||||
);
|
||||
foregroundStaleBitmap = bitmapProvider.generateBitmap(
|
||||
options.gpsDrawable(), options.foregroundStaleTintColor()
|
||||
);
|
||||
}
|
||||
|
||||
locationLayerRenderer.addBitmaps(
|
||||
renderMode,
|
||||
shadowBitmap,
|
||||
backgroundBitmap,
|
||||
backgroundStaleBitmap,
|
||||
bearingBitmap,
|
||||
foregroundBitmap,
|
||||
foregroundStaleBitmap
|
||||
);
|
||||
}
|
||||
|
||||
private void styleScaling(@NonNull LocationComponentOptions options) {
|
||||
Expression scaleExpression = interpolate(linear(), zoom(),
|
||||
stop(maplibreMap.getMinZoomLevel(), options.minZoomIconScale()),
|
||||
stop(maplibreMap.getMaxZoomLevel(), options.maxZoomIconScale())
|
||||
);
|
||||
|
||||
locationLayerRenderer.styleScaling(scaleExpression);
|
||||
}
|
||||
|
||||
private void determineIconsSource(LocationComponentOptions options) {
|
||||
String foregroundIconString = buildIconString(
|
||||
renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON);
|
||||
String foregroundStaleIconString = buildIconString(options.foregroundStaleName(), FOREGROUND_STALE_ICON);
|
||||
String backgroundIconString = buildIconString(options.backgroundName(), BACKGROUND_ICON);
|
||||
String backgroundStaleIconString = buildIconString(options.backgroundStaleName(), BACKGROUND_STALE_ICON);
|
||||
String bearingIconString = buildIconString(options.bearingName(), BEARING_ICON);
|
||||
|
||||
locationLayerRenderer.updateIconIds(
|
||||
foregroundIconString,
|
||||
foregroundStaleIconString,
|
||||
backgroundIconString,
|
||||
backgroundStaleIconString,
|
||||
bearingIconString
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String buildIconString(@Nullable String bitmapName, @NonNull String drawableName) {
|
||||
if (bitmapName != null) {
|
||||
if (useSpecializedLocationLayer) {
|
||||
Logger.e(TAG, bitmapName + " replacement ID provided for an unsupported specialized location layer");
|
||||
return drawableName;
|
||||
}
|
||||
return bitmapName;
|
||||
}
|
||||
return drawableName;
|
||||
}
|
||||
|
||||
void setLocationsStale(boolean isStale) {
|
||||
this.isStale = isStale;
|
||||
locationLayerRenderer.setLocationStale(isStale, renderMode);
|
||||
}
|
||||
|
||||
//
|
||||
// Map click event
|
||||
//
|
||||
|
||||
boolean onMapClick(@NonNull LatLng point) {
|
||||
PointF screenLoc = maplibreMap.getProjection().toScreenLocation(point);
|
||||
List<Feature> features = maplibreMap.queryRenderedFeatures(screenLoc,
|
||||
BACKGROUND_LAYER,
|
||||
FOREGROUND_LAYER,
|
||||
BEARING_LAYER
|
||||
);
|
||||
return !features.isEmpty();
|
||||
}
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<LatLng> latLngValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<LatLng>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(LatLng value) {
|
||||
locationLayerRenderer.setLatLng(value);
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> gpsBearingValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float value) {
|
||||
locationLayerRenderer.setGpsBearing(value);
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> compassBearingValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float value) {
|
||||
locationLayerRenderer.setCompassBearing(value);
|
||||
}
|
||||
};
|
||||
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> accuracyValueListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float value) {
|
||||
locationLayerRenderer.setAccuracyRadius(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The listener that handles the updating of the pulsing circle's radius and opacity.
|
||||
*/
|
||||
private final MapLibreAnimator.AnimationsValueChangeListener<Float> pulsingCircleRadiusListener =
|
||||
new MapLibreAnimator.AnimationsValueChangeListener<Float>() {
|
||||
@Override
|
||||
public void onNewAnimationValue(Float newPulseRadiusValue) {
|
||||
Float newPulseOpacityValue = null;
|
||||
if (options.pulseFadeEnabled()) {
|
||||
newPulseOpacityValue = (float) 1 - ((newPulseRadiusValue / 100) * 3);
|
||||
}
|
||||
locationLayerRenderer.updatePulsingUi(newPulseRadiusValue, newPulseOpacityValue);
|
||||
}
|
||||
};
|
||||
|
||||
Set<AnimatorListenerHolder> getAnimationListeners() {
|
||||
Set<AnimatorListenerHolder> holders = new HashSet<>();
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_LAYER_LATLNG, latLngValueListener));
|
||||
|
||||
if (renderMode == RenderMode.GPS) {
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_LAYER_GPS_BEARING, gpsBearingValueListener));
|
||||
} else if (renderMode == RenderMode.COMPASS) {
|
||||
holders.add(
|
||||
new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_LAYER_COMPASS_BEARING, compassBearingValueListener));
|
||||
}
|
||||
|
||||
if (renderMode == RenderMode.COMPASS || renderMode == RenderMode.NORMAL) {
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_LAYER_ACCURACY, accuracyValueListener));
|
||||
}
|
||||
|
||||
if (options.pulseEnabled()) {
|
||||
holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_PULSING_CIRCLE,
|
||||
pulsingCircleRadiusListener));
|
||||
}
|
||||
return holders;
|
||||
}
|
||||
|
||||
void cameraBearingUpdated(double bearing) {
|
||||
if (renderMode != RenderMode.GPS) {
|
||||
locationLayerRenderer.cameraBearingUpdated(bearing);
|
||||
}
|
||||
}
|
||||
|
||||
void cameraTiltUpdated(double tilt) {
|
||||
locationLayerRenderer.cameraTiltUpdated(tilt);
|
||||
}
|
||||
|
||||
void adjustPulsingCircleLayerVisibility(boolean visible) {
|
||||
locationLayerRenderer.adjustPulsingCircleLayerVisibility(visible);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
interface LocationLayerRenderer {
|
||||
void initializeComponents(Style style);
|
||||
|
||||
void addLayers(LocationComponentPositionManager positionManager);
|
||||
|
||||
void removeLayers();
|
||||
|
||||
void hide();
|
||||
|
||||
void cameraTiltUpdated(double tilt);
|
||||
|
||||
void cameraBearingUpdated(double bearing);
|
||||
|
||||
void show(@RenderMode.Mode int renderMode, boolean isStale);
|
||||
|
||||
void styleAccuracy(float accuracyAlpha, int accuracyColor);
|
||||
|
||||
void setLatLng(LatLng latLng);
|
||||
|
||||
void setGpsBearing(Float gpsBearing);
|
||||
|
||||
void setCompassBearing(Float compassBearing);
|
||||
|
||||
void setAccuracyRadius(Float accuracy);
|
||||
|
||||
void styleScaling(Expression scaleExpression);
|
||||
|
||||
void setLocationStale(boolean isStale, int renderMode);
|
||||
|
||||
void adjustPulsingCircleLayerVisibility(boolean visible);
|
||||
|
||||
void stylePulsingCircle(LocationComponentOptions options);
|
||||
|
||||
void updatePulsingUi(float radius, @Nullable Float opacity);
|
||||
|
||||
void updateIconIds(String foregroundIconString, String foregroundStaleIconString, String backgroundIconString,
|
||||
String backgroundStaleIconString, String bearingIconString);
|
||||
|
||||
void addBitmaps(@RenderMode.Mode int renderMode, @Nullable Bitmap shadowBitmap, Bitmap backgroundBitmap,
|
||||
Bitmap backgroundStaleBitmap, Bitmap bearingBitmap,
|
||||
Bitmap foregroundBitmap, Bitmap foregroundStaleBitmap);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// This file is generated. Edit scripts/generate-style-code.js, then run `make style-code`.
|
||||
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.style.types.Formatted;
|
||||
import static org.maplibre.android.utils.ColorUtils.colorToRgbaString;
|
||||
import org.maplibre.android.style.layers.LayoutPropertyValue;
|
||||
import org.maplibre.android.style.layers.PaintPropertyValue;
|
||||
import org.maplibre.android.style.layers.Property;
|
||||
import org.maplibre.android.style.layers.PropertyValue;
|
||||
|
||||
/**
|
||||
* Constructs paint/layout properties for Layers
|
||||
*
|
||||
* @see <a href="https://maplibre.org/maplibre-style-spec/#layers">Layer style documentation</a>
|
||||
*/
|
||||
class LocationPropertyFactory {
|
||||
|
||||
/**
|
||||
* Set the property visibility.
|
||||
*
|
||||
* @param value the visibility value
|
||||
* @return property wrapper around visibility
|
||||
*/
|
||||
public static PropertyValue<String> visibility(@Property.VISIBILITY String value) {
|
||||
return new LayoutPropertyValue<>("visibility", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of the perspective compensation, between 0 and 1. A value of 1 produces a location indicator of constant width across the screen. A value of 0 makes it scale naturally according to the viewing projection.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> perspectiveCompensation(Float value) {
|
||||
return new PaintPropertyValue<>("perspective-compensation", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of the perspective compensation, between 0 and 1. A value of 1 produces a location indicator of constant width across the screen. A value of 0 makes it scale naturally according to the viewing projection.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> perspectiveCompensation(Expression expression) {
|
||||
return new PaintPropertyValue<>("perspective-compensation", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The displacement off the center of the top image and the shadow image when the pitch of the map is greater than 0. This helps producing a three-dimensional appearence.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> imageTiltDisplacement(Float value) {
|
||||
return new PaintPropertyValue<>("image-tilt-displacement", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The displacement off the center of the top image and the shadow image when the pitch of the map is greater than 0. This helps producing a three-dimensional appearence.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> imageTiltDisplacement(Expression expression) {
|
||||
return new PaintPropertyValue<>("image-tilt-displacement", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The bearing of the location indicator.
|
||||
*
|
||||
* @param value a Double value
|
||||
* @return property wrapper around Double
|
||||
*/
|
||||
public static PropertyValue<Double> bearing(Double value) {
|
||||
return new PaintPropertyValue<>("bearing", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The bearing of the location indicator.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> bearing(Expression expression) {
|
||||
return new PaintPropertyValue<>("bearing", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of [latitude, longitude, altitude] position of the location indicator.
|
||||
*
|
||||
* @param value a Double[] value
|
||||
* @return property wrapper around Double[]
|
||||
*/
|
||||
public static PropertyValue<Double[]> location(Double[] value) {
|
||||
return new PaintPropertyValue<>("location", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of [latitude, longitude, altitude] position of the location indicator.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> location(Expression expression) {
|
||||
return new PaintPropertyValue<>("location", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The accuracy, in meters, of the position source used to retrieve the position of the location indicator.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> accuracyRadius(Float value) {
|
||||
return new PaintPropertyValue<>("accuracy-radius", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The accuracy, in meters, of the position source used to retrieve the position of the location indicator.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> accuracyRadius(Expression expression) {
|
||||
return new PaintPropertyValue<>("accuracy-radius", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the top image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> topImageSize(Float value) {
|
||||
return new PaintPropertyValue<>("top-image-size", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the top image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> topImageSize(Expression expression) {
|
||||
return new PaintPropertyValue<>("top-image-size", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the bearing image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> bearingImageSize(Float value) {
|
||||
return new PaintPropertyValue<>("bearing-image-size", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the bearing image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> bearingImageSize(Expression expression) {
|
||||
return new PaintPropertyValue<>("bearing-image-size", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the shadow image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param value a Float value
|
||||
* @return property wrapper around Float
|
||||
*/
|
||||
public static PropertyValue<Float> shadowImageSize(Float value) {
|
||||
return new PaintPropertyValue<>("shadow-image-size", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the shadow image, as a scale factor applied to the size of the specified image.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> shadowImageSize(Expression expression) {
|
||||
return new PaintPropertyValue<>("shadow-image-size", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius, as a circle. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param value a int color value
|
||||
* @return property wrapper around String color
|
||||
*/
|
||||
public static PropertyValue<String> accuracyRadiusColor(@ColorInt int value) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-color", colorToRgbaString(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius, as a circle. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<String> accuracyRadiusColor(String value) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-color", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius, as a circle. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> accuracyRadiusColor(Expression expression) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-color", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius border. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param value a int color value
|
||||
* @return property wrapper around String color
|
||||
*/
|
||||
public static PropertyValue<String> accuracyRadiusBorderColor(@ColorInt int value) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-border-color", colorToRgbaString(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius border. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<String> accuracyRadiusBorderColor(String value) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-border-color", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The color for drawing the accuracy radius border. To adjust transparency, set the alpha component of the color accordingly.
|
||||
*
|
||||
* @param expression an expression statement
|
||||
* @return property wrapper around an expression statement
|
||||
*/
|
||||
public static PropertyValue<Expression> accuracyRadiusBorderColor(Expression expression) {
|
||||
return new PaintPropertyValue<>("accuracy-radius-border-color", expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the top of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<String> topImage(String value) {
|
||||
return new LayoutPropertyValue<>("top-image", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the top of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<Expression> topImage(Expression value) {
|
||||
return new LayoutPropertyValue<>("top-image", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the middle of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<String> bearingImage(String value) {
|
||||
return new LayoutPropertyValue<>("bearing-image", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the middle of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<Expression> bearingImage(Expression value) {
|
||||
return new LayoutPropertyValue<>("bearing-image", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the background of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<String> shadowImage(String value) {
|
||||
return new LayoutPropertyValue<>("shadow-image", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of image in sprite to use as the background of the location indicator.
|
||||
*
|
||||
* @param value a String value
|
||||
* @return property wrapper around String
|
||||
*/
|
||||
public static PropertyValue<Expression> shadowImage(Expression value) {
|
||||
return new LayoutPropertyValue<>("shadow-image", value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Size;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Abstract class for all of the location component animators.
|
||||
*
|
||||
* @param <K> Data type that will be animated.
|
||||
*/
|
||||
public abstract class MapLibreAnimator<K> extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef( {
|
||||
ANIMATOR_LAYER_LATLNG,
|
||||
ANIMATOR_CAMERA_LATLNG,
|
||||
ANIMATOR_LAYER_GPS_BEARING,
|
||||
ANIMATOR_LAYER_COMPASS_BEARING,
|
||||
ANIMATOR_CAMERA_GPS_BEARING,
|
||||
ANIMATOR_CAMERA_COMPASS_BEARING,
|
||||
ANIMATOR_LAYER_ACCURACY,
|
||||
ANIMATOR_ZOOM,
|
||||
ANIMATOR_TILT,
|
||||
ANIMATOR_PULSING_CIRCLE,
|
||||
ANIMATOR_PADDING
|
||||
})
|
||||
@interface Type {
|
||||
}
|
||||
|
||||
static final int ANIMATOR_LAYER_LATLNG = 0;
|
||||
static final int ANIMATOR_CAMERA_LATLNG = 1;
|
||||
static final int ANIMATOR_LAYER_GPS_BEARING = 2;
|
||||
static final int ANIMATOR_LAYER_COMPASS_BEARING = 3;
|
||||
static final int ANIMATOR_CAMERA_GPS_BEARING = 4;
|
||||
static final int ANIMATOR_CAMERA_COMPASS_BEARING = 5;
|
||||
static final int ANIMATOR_LAYER_ACCURACY = 6;
|
||||
static final int ANIMATOR_ZOOM = 7;
|
||||
static final int ANIMATOR_TILT = 8;
|
||||
static final int ANIMATOR_PULSING_CIRCLE = 9;
|
||||
static final int ANIMATOR_PADDING = 10;
|
||||
|
||||
private final AnimationsValueChangeListener<K> updateListener;
|
||||
private final K target;
|
||||
private K animatedValue;
|
||||
|
||||
private final double minUpdateInterval;
|
||||
private long timeElapsed;
|
||||
|
||||
/**
|
||||
* Makes this animator invalid and prevents it from pushing any more updates to the listener.
|
||||
*
|
||||
* This can be used to prevent propagating final updates when the animator should be immediately canceled.
|
||||
*/
|
||||
private boolean invalid;
|
||||
|
||||
public MapLibreAnimator(@NonNull @Size(min = 2) K[] values, @NonNull AnimationsValueChangeListener<K> updateListener,
|
||||
int maxAnimationFps) {
|
||||
minUpdateInterval = 1E9 / maxAnimationFps;
|
||||
setObjectValues((Object[]) values);
|
||||
setEvaluator(provideEvaluator());
|
||||
this.updateListener = updateListener;
|
||||
this.target = values[values.length - 1];
|
||||
addUpdateListener(this);
|
||||
addListener(new AnimatorListener());
|
||||
}
|
||||
|
||||
public MapLibreAnimator(AnimationsValueChangeListener<K> updateListener, K target, K animatedValue,
|
||||
double minUpdateInterval, long timeElapsed) {
|
||||
this.updateListener = updateListener;
|
||||
this.target = target;
|
||||
this.animatedValue = animatedValue;
|
||||
this.minUpdateInterval = minUpdateInterval;
|
||||
this.timeElapsed = timeElapsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
animatedValue = (K) animation.getAnimatedValue();
|
||||
|
||||
long currentTime = System.nanoTime();
|
||||
if ((currentTime - timeElapsed) < minUpdateInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
postUpdates();
|
||||
timeElapsed = currentTime;
|
||||
}
|
||||
|
||||
private class AnimatorListener extends AnimatorListenerAdapter {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
postUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void postUpdates() {
|
||||
if (!invalid) {
|
||||
updateListener.onNewAnimationValue(animatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
K getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
abstract TypeEvaluator provideEvaluator();
|
||||
|
||||
interface AnimationsValueChangeListener<K> {
|
||||
void onNewAnimationValue(K value);
|
||||
}
|
||||
|
||||
public void makeInvalid() {
|
||||
invalid = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.maplibre.android.location
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
|
||||
internal class MapLibreAnimatorListener(cancelableCallback: MapLibreMap.CancelableCallback?) :
|
||||
AnimatorListenerAdapter() {
|
||||
private val cancelableCallback: MapLibreMap.CancelableCallback?
|
||||
|
||||
init {
|
||||
this.cancelableCallback = cancelableCallback
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
cancelableCallback?.onCancel()
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
cancelableCallback?.onFinish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
final class MapLibreAnimatorProvider {
|
||||
|
||||
private static MapLibreAnimatorProvider INSTANCE;
|
||||
|
||||
private MapLibreAnimatorProvider() {
|
||||
// private constructor
|
||||
}
|
||||
|
||||
public static MapLibreAnimatorProvider getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new MapLibreAnimatorProvider();
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
MapLibreLatLngAnimator latLngAnimator(LatLng[] values, MapLibreAnimator.AnimationsValueChangeListener updateListener,
|
||||
int maxAnimationFps) {
|
||||
return new MapLibreLatLngAnimator(values, updateListener, maxAnimationFps);
|
||||
}
|
||||
|
||||
MapLibreFloatAnimator floatAnimator(Float[] values, MapLibreAnimator.AnimationsValueChangeListener updateListener,
|
||||
int maxAnimationFps) {
|
||||
return new MapLibreFloatAnimator(values, updateListener, maxAnimationFps);
|
||||
}
|
||||
|
||||
MapLibreCameraAnimatorAdapter cameraAnimator(Float[] values,
|
||||
MapLibreAnimator.AnimationsValueChangeListener updateListener,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
return new MapLibreCameraAnimatorAdapter(values, updateListener, cancelableCallback);
|
||||
}
|
||||
|
||||
MapLibrePaddingAnimator paddingAnimator(double[][] values,
|
||||
MapLibreAnimator.AnimationsValueChangeListener<double[]> updateListener,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
return new MapLibrePaddingAnimator(values, updateListener, cancelableCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* This animator is for the LocationComponent pulsing circle.
|
||||
*
|
||||
* @param updateListener the listener that is found in the {@link LocationAnimatorCoordinator}'s
|
||||
* listener array.
|
||||
* @param maxAnimationFps the max frames per second of the pulsing animation
|
||||
* @param pulseSingleDuration the number of milliseconds it takes for the animator to create
|
||||
* a single pulse.
|
||||
* @param pulseMaxRadius the max radius when the circle is finished with a single pulse.
|
||||
* @param pulseInterpolator the type of Android-system interpolator to use for
|
||||
* the pulsing animation (linear, accelerate, bounce, etc.)
|
||||
* @return a built {@link PulsingLocationCircleAnimator} object.
|
||||
*/
|
||||
PulsingLocationCircleAnimator pulsingCircleAnimator(MapLibreAnimator.AnimationsValueChangeListener updateListener,
|
||||
int maxAnimationFps,
|
||||
float pulseSingleDuration,
|
||||
float pulseMaxRadius,
|
||||
Interpolator pulseInterpolator) {
|
||||
PulsingLocationCircleAnimator pulsingLocationCircleAnimator =
|
||||
new PulsingLocationCircleAnimator(updateListener, maxAnimationFps, pulseMaxRadius);
|
||||
pulsingLocationCircleAnimator.setDuration((long) pulseSingleDuration);
|
||||
pulsingLocationCircleAnimator.setRepeatMode(ValueAnimator.RESTART);
|
||||
pulsingLocationCircleAnimator.setRepeatCount(ValueAnimator.INFINITE);
|
||||
pulsingLocationCircleAnimator.setInterpolator(pulseInterpolator);
|
||||
return pulsingLocationCircleAnimator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class MapLibreAnimatorSetProvider {
|
||||
private static MapLibreAnimatorSetProvider instance;
|
||||
|
||||
private MapLibreAnimatorSetProvider() {
|
||||
// private constructor
|
||||
}
|
||||
|
||||
static MapLibreAnimatorSetProvider getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new MapLibreAnimatorSetProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
void startAnimation(@NonNull List<Animator> animators, @NonNull Interpolator interpolator,
|
||||
long duration) {
|
||||
AnimatorSet locationAnimatorSet = new AnimatorSet();
|
||||
locationAnimatorSet.playTogether(animators);
|
||||
locationAnimatorSet.setInterpolator(interpolator);
|
||||
locationAnimatorSet.setDuration(duration);
|
||||
locationAnimatorSet.start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Size;
|
||||
|
||||
import org.maplibre.android.maps.MapLibreMap;
|
||||
|
||||
class MapLibreCameraAnimatorAdapter extends MapLibreFloatAnimator {
|
||||
|
||||
MapLibreCameraAnimatorAdapter(@NonNull @Size(min = 2) Float[] values,
|
||||
AnimationsValueChangeListener updateListener,
|
||||
@Nullable MapLibreMap.CancelableCallback cancelableCallback) {
|
||||
super(values, updateListener, Integer.MAX_VALUE);
|
||||
addListener(new MapLibreAnimatorListener(cancelableCallback));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.FloatEvaluator;
|
||||
import android.animation.TypeEvaluator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Size;
|
||||
|
||||
class MapLibreFloatAnimator extends MapLibreAnimator<Float> {
|
||||
MapLibreFloatAnimator(@NonNull @Size(min = 2) Float[] values,
|
||||
@NonNull AnimationsValueChangeListener updateListener, int maxAnimationFps) {
|
||||
super(values, updateListener, maxAnimationFps);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
TypeEvaluator provideEvaluator() {
|
||||
return new FloatEvaluator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.animation.TypeEvaluator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
|
||||
class MapLibreLatLngAnimator extends MapLibreAnimator<LatLng> {
|
||||
|
||||
MapLibreLatLngAnimator(@NonNull LatLng[] values, @NonNull AnimationsValueChangeListener updateListener,
|
||||
int maxAnimationFps) {
|
||||
super(values, updateListener, maxAnimationFps);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
TypeEvaluator provideEvaluator() {
|
||||
return new LatLngEvaluator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.maplibre.android.location
|
||||
|
||||
import android.animation.TypeEvaluator
|
||||
import androidx.annotation.Size
|
||||
import org.maplibre.android.maps.MapLibreMap.CancelableCallback
|
||||
|
||||
class MapLibrePaddingAnimator internal constructor(
|
||||
@Size(min = 2) values: Array<DoubleArray>,
|
||||
updateListener: AnimationsValueChangeListener<DoubleArray>,
|
||||
cancelableCallback: CancelableCallback?
|
||||
) :
|
||||
MapLibreAnimator<DoubleArray>(values, updateListener, Int.MAX_VALUE) {
|
||||
init {
|
||||
addListener(MapLibreAnimatorListener(cancelableCallback))
|
||||
}
|
||||
|
||||
public override fun provideEvaluator(): TypeEvaluator<DoubleArray> {
|
||||
return PaddingEvaluator()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
interface OnCameraMoveInvalidateListener {
|
||||
|
||||
void onInvalidateCameraMove();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import org.maplibre.android.location.modes.CameraMode;
|
||||
|
||||
/**
|
||||
* Listener that gets invoked when camera tracking state changes.
|
||||
*/
|
||||
public interface OnCameraTrackingChangedListener {
|
||||
/**
|
||||
* Invoked whenever camera tracking is broken.
|
||||
* This callback gets invoked just after {@link #onCameraTrackingChanged(int)}, if needed.
|
||||
*/
|
||||
void onCameraTrackingDismissed();
|
||||
|
||||
/**
|
||||
* Invoked on every {@link CameraMode} change.
|
||||
*
|
||||
* @param currentMode current active {@link CameraMode}.
|
||||
*/
|
||||
void onCameraTrackingChanged(@CameraMode.Mode int currentMode);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import org.maplibre.android.location.modes.CameraMode;
|
||||
|
||||
/**
|
||||
* Callback for {@link CameraMode } transition state.
|
||||
*/
|
||||
public interface OnLocationCameraTransitionListener {
|
||||
/**
|
||||
* Invoked when the camera mode transition animation has been finished.
|
||||
*
|
||||
* @param cameraMode camera mode change that initiated the transition
|
||||
*/
|
||||
void onLocationCameraTransitionFinished(@CameraMode.Mode int cameraMode);
|
||||
|
||||
/**
|
||||
* Invoked when the camera mode transition animation has been canceled.
|
||||
* <p>
|
||||
* The camera mode is set regardless of the cancellation of the transition animation.
|
||||
*
|
||||
* @param cameraMode camera mode change that initiated the transition
|
||||
*/
|
||||
void onLocationCameraTransitionCanceled(@CameraMode.Mode int cameraMode);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* The Location Component exposes an API for listening to when the user clicks on the location
|
||||
* layer icon visible on the map. When this event occurs, the {@link #onLocationComponentClick()} method
|
||||
* gets invoked.
|
||||
*/
|
||||
public interface OnLocationClickListener {
|
||||
|
||||
/**
|
||||
* Called whenever user clicks on the location layer drawn on the map.
|
||||
*/
|
||||
void onLocationComponentClick();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* The Location Component exposes an API for listening to when the user long clicks on the location
|
||||
* layer icon visible on the map. when this event occurs, the {@link #onLocationComponentLongClick()} method
|
||||
* gets invoked.
|
||||
*/
|
||||
public interface OnLocationLongClickListener {
|
||||
|
||||
/**
|
||||
* Called whenever user long clicks on the location layer drawn on the map.
|
||||
*/
|
||||
void onLocationComponentLongClick();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* Listener that can be added as a callback when the last location update
|
||||
* is considered stale.
|
||||
* <p>
|
||||
* The time from the last location update that determines if a location update
|
||||
* is stale or not is provided by {@link LocationComponentOptions#staleStateTimeout()}.
|
||||
*/
|
||||
public interface OnLocationStaleListener {
|
||||
|
||||
/**
|
||||
* Called when the stale state changes.
|
||||
* @param isStale true if location is stale, false otherwise
|
||||
*/
|
||||
void onStaleStateChange(boolean isStale);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
|
||||
/**
|
||||
* Listener that gets invoked when layer render mode changes.
|
||||
*/
|
||||
public interface OnRenderModeChangedListener {
|
||||
|
||||
/**
|
||||
* Invoked on every {@link RenderMode} change.
|
||||
*
|
||||
* @param currentMode current active {@link RenderMode}.
|
||||
*/
|
||||
void onRenderModeChanged(@RenderMode.Mode int currentMode);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.maplibre.android.location
|
||||
|
||||
import android.animation.TypeEvaluator
|
||||
import androidx.annotation.Size
|
||||
|
||||
internal class PaddingEvaluator : TypeEvaluator<DoubleArray> {
|
||||
private val padding = DoubleArray(4)
|
||||
override fun evaluate(
|
||||
fraction: Float, @Size(min = 4) startValue: DoubleArray,
|
||||
@Size(min = 4) endValue: DoubleArray
|
||||
): DoubleArray {
|
||||
padding[0] = startValue[0] + (endValue[0] - startValue[0]) * fraction
|
||||
padding[1] = startValue[1] + (endValue[1] - startValue[1]) * fraction
|
||||
padding[2] = startValue[2] + (endValue[2] - startValue[2]) * fraction
|
||||
padding[3] = startValue[3] + (endValue[3] - startValue[3]) * fraction
|
||||
return padding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
/**
|
||||
* Manages the logic of the interpolated animation which is applied to the LocationComponent's pulsing circle
|
||||
*/
|
||||
public class PulsingLocationCircleAnimator extends MapLibreFloatAnimator {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param updateListener the {@link AnimationsValueChangeListener} associated with this animator.
|
||||
* @param maxAnimationFps the maximum frames per second that the animator should use. Default
|
||||
* is the {@link LocationAnimatorCoordinator#maxAnimationFps} variable.
|
||||
*/
|
||||
public PulsingLocationCircleAnimator(AnimationsValueChangeListener updateListener,
|
||||
int maxAnimationFps,
|
||||
float circleMaxRadius) {
|
||||
super(new Float[]{0f, circleMaxRadius}, updateListener, maxAnimationFps);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Class controls the location stale state when the {@link android.location.Location} hasn't
|
||||
* been updated in 'x' amount of time. {@link LocationComponentOptions#staleStateTimeout()} can be used to
|
||||
* control the amount of time before the location's considered stale.
|
||||
* {@link LocationComponentOptions#enableStaleState()} is available for disabling this behaviour.
|
||||
*/
|
||||
class StaleStateManager {
|
||||
|
||||
private boolean isEnabled;
|
||||
private final OnLocationStaleListener innerOnLocationStaleListeners;
|
||||
@NonNull
|
||||
private final StaleMessageHandler handler;
|
||||
private boolean isStale = true;
|
||||
private long delayTime;
|
||||
|
||||
private final int staleStateMessage = 1;
|
||||
|
||||
StaleStateManager(OnLocationStaleListener innerListener, LocationComponentOptions options) {
|
||||
innerOnLocationStaleListeners = innerListener;
|
||||
handler = new StaleMessageHandler(this);
|
||||
isEnabled = options.enableStaleState();
|
||||
delayTime = options.staleStateTimeout();
|
||||
}
|
||||
|
||||
void setEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
setState(isStale);
|
||||
} else if (isEnabled) {
|
||||
onStop();
|
||||
innerOnLocationStaleListeners.onStaleStateChange(false);
|
||||
}
|
||||
isEnabled = enabled;
|
||||
}
|
||||
|
||||
boolean isStale() {
|
||||
return isStale;
|
||||
}
|
||||
|
||||
void updateLatestLocationTime() {
|
||||
setState(false);
|
||||
postTheCallback();
|
||||
}
|
||||
|
||||
void setDelayTime(long delayTime) {
|
||||
this.delayTime = delayTime;
|
||||
if (handler.hasMessages(staleStateMessage)) {
|
||||
postTheCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void onStart() {
|
||||
if (!isStale) {
|
||||
postTheCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void onStop() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
private void postTheCallback() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handler.sendEmptyMessageDelayed(staleStateMessage, delayTime);
|
||||
}
|
||||
|
||||
private void setState(boolean stale) {
|
||||
if (stale != isStale) {
|
||||
isStale = stale;
|
||||
if (isEnabled) {
|
||||
innerOnLocationStaleListeners.onStaleStateChange(stale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class StaleMessageHandler extends Handler {
|
||||
|
||||
private final WeakReference<StaleStateManager> managerWeakReference;
|
||||
|
||||
private StaleMessageHandler(StaleStateManager staleStateManager) {
|
||||
this.managerWeakReference = new WeakReference<>(staleStateManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
StaleStateManager manager = managerWeakReference.get();
|
||||
if (manager != null) {
|
||||
manager.setState(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package org.maplibre.android.location;
|
||||
|
||||
import static org.maplibre.android.location.LocationComponentConstants.ACCURACY_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BACKGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.BEARING_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.FOREGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.LOCATION_SOURCE;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_BEARING_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_PULSING_OPACITY;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_PULSING_RADIUS;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.PULSING_CIRCLE_LAYER;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.SHADOW_ICON;
|
||||
import static org.maplibre.android.location.LocationComponentConstants.SHADOW_LAYER;
|
||||
import static org.maplibre.android.style.expressions.Expression.get;
|
||||
import static org.maplibre.android.style.layers.Property.NONE;
|
||||
import static org.maplibre.android.style.layers.Property.VISIBLE;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.circleColor;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.circleOpacity;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.circleRadius;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.circleStrokeColor;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.iconSize;
|
||||
import static org.maplibre.android.style.layers.PropertyFactory.visibility;
|
||||
import static org.maplibre.android.utils.ColorUtils.colorToRgbaString;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mapbox.geojson.Feature;
|
||||
import com.mapbox.geojson.Point;
|
||||
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.location.modes.RenderMode;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.expressions.Expression;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
import org.maplibre.android.style.layers.SymbolLayer;
|
||||
import org.maplibre.android.style.sources.GeoJsonSource;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
final class SymbolLocationLayerRenderer implements LocationLayerRenderer {
|
||||
private Style style;
|
||||
private final LayerSourceProvider layerSourceProvider;
|
||||
|
||||
private final Set<String> layerSet;
|
||||
private Feature locationFeature;
|
||||
private GeoJsonSource locationSource;
|
||||
|
||||
SymbolLocationLayerRenderer(LayerSourceProvider layerSourceProvider,
|
||||
LayerFeatureProvider featureProvider,
|
||||
boolean isStale) {
|
||||
this.layerSourceProvider = layerSourceProvider;
|
||||
this.layerSet = layerSourceProvider.getEmptyLayerSet();
|
||||
this.locationFeature = featureProvider.generateLocationFeature(locationFeature, isStale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeComponents(Style style) {
|
||||
this.style = style;
|
||||
addLocationSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLayers(LocationComponentPositionManager positionManager) {
|
||||
// positions the top-most reference layer
|
||||
Layer layer = layerSourceProvider.generateLayer(BEARING_LAYER);
|
||||
positionManager.addLayerToMap(layer);
|
||||
layerSet.add(layer.getId());
|
||||
|
||||
// adds remaining layers while keeping the order
|
||||
addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER);
|
||||
addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
|
||||
addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER);
|
||||
addAccuracyLayer();
|
||||
addPulsingCircleLayerToMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLayers() {
|
||||
for (String layerId : layerSet) {
|
||||
style.removeLayer(layerId);
|
||||
}
|
||||
layerSet.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
for (String layerId : layerSet) {
|
||||
setLayerVisibility(layerId, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cameraTiltUpdated(double tilt) {
|
||||
updateForegroundOffset(tilt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cameraBearingUpdated(double bearing) {
|
||||
updateForegroundBearing((float) bearing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@RenderMode.Mode int renderMode, boolean isStale) {
|
||||
switch (renderMode) {
|
||||
case RenderMode.NORMAL:
|
||||
setLayerVisibility(SHADOW_LAYER, true);
|
||||
setLayerVisibility(FOREGROUND_LAYER, true);
|
||||
setLayerVisibility(BACKGROUND_LAYER, true);
|
||||
setLayerVisibility(ACCURACY_LAYER, !isStale);
|
||||
setLayerVisibility(BEARING_LAYER, false);
|
||||
break;
|
||||
case RenderMode.COMPASS:
|
||||
setLayerVisibility(SHADOW_LAYER, true);
|
||||
setLayerVisibility(FOREGROUND_LAYER, true);
|
||||
setLayerVisibility(BACKGROUND_LAYER, true);
|
||||
setLayerVisibility(ACCURACY_LAYER, !isStale);
|
||||
setLayerVisibility(BEARING_LAYER, true);
|
||||
break;
|
||||
case RenderMode.GPS:
|
||||
setLayerVisibility(SHADOW_LAYER, false);
|
||||
setLayerVisibility(FOREGROUND_LAYER, true);
|
||||
setLayerVisibility(BACKGROUND_LAYER, true);
|
||||
setLayerVisibility(ACCURACY_LAYER, false);
|
||||
setLayerVisibility(BEARING_LAYER, false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void styleAccuracy(float accuracyAlpha, int accuracyColor) {
|
||||
locationFeature.addNumberProperty(PROPERTY_ACCURACY_ALPHA, accuracyAlpha);
|
||||
locationFeature.addStringProperty(PROPERTY_ACCURACY_COLOR, colorToRgbaString(accuracyColor));
|
||||
refreshSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLatLng(LatLng latLng) {
|
||||
Point point = Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude());
|
||||
setLocationPoint(point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGpsBearing(Float gpsBearing) {
|
||||
setBearingProperty(PROPERTY_GPS_BEARING, gpsBearing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCompassBearing(Float compassBearing) {
|
||||
setBearingProperty(PROPERTY_COMPASS_BEARING, compassBearing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccuracyRadius(Float accuracy) {
|
||||
updateAccuracyRadius(accuracy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void styleScaling(Expression scaleExpression) {
|
||||
for (String layerId : layerSet) {
|
||||
Layer layer = style.getLayer(layerId);
|
||||
if (layer instanceof SymbolLayer) {
|
||||
layer.setProperties(
|
||||
iconSize(scaleExpression)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocationStale(boolean isStale, int renderMode) {
|
||||
locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, isStale);
|
||||
refreshSource();
|
||||
if (renderMode != RenderMode.GPS) {
|
||||
setLayerVisibility(ACCURACY_LAYER, !isStale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIconIds(String foregroundIconString, String foregroundStaleIconString, String backgroundIconString,
|
||||
String backgroundStaleIconString, String bearingIconString) {
|
||||
locationFeature.addStringProperty(PROPERTY_FOREGROUND_ICON, foregroundIconString);
|
||||
locationFeature.addStringProperty(PROPERTY_BACKGROUND_ICON, backgroundIconString);
|
||||
locationFeature.addStringProperty(PROPERTY_FOREGROUND_STALE_ICON, foregroundStaleIconString);
|
||||
locationFeature.addStringProperty(PROPERTY_BACKGROUND_STALE_ICON, backgroundStaleIconString);
|
||||
locationFeature.addStringProperty(PROPERTY_BEARING_ICON, bearingIconString);
|
||||
refreshSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBitmaps(@RenderMode.Mode int renderMode, @Nullable Bitmap shadowBitmap, Bitmap backgroundBitmap,
|
||||
Bitmap backgroundStaleBitmap, Bitmap bearingBitmap,
|
||||
Bitmap foregroundBitmap, Bitmap foregroundBitmapStale) {
|
||||
if (shadowBitmap != null) {
|
||||
style.addImage(SHADOW_ICON, shadowBitmap);
|
||||
} else {
|
||||
style.removeImage(SHADOW_ICON);
|
||||
}
|
||||
style.addImage(BACKGROUND_ICON, backgroundBitmap);
|
||||
style.addImage(BACKGROUND_STALE_ICON, backgroundStaleBitmap);
|
||||
style.addImage(BEARING_ICON, bearingBitmap);
|
||||
style.addImage(FOREGROUND_ICON, foregroundBitmap);
|
||||
style.addImage(FOREGROUND_STALE_ICON, foregroundBitmapStale);
|
||||
}
|
||||
|
||||
private void updateForegroundOffset(double tilt) {
|
||||
JsonArray foregroundJsonArray = new JsonArray();
|
||||
foregroundJsonArray.add(0f);
|
||||
foregroundJsonArray.add((float) (-0.05 * tilt));
|
||||
locationFeature.addProperty(PROPERTY_FOREGROUND_ICON_OFFSET, foregroundJsonArray);
|
||||
|
||||
JsonArray backgroundJsonArray = new JsonArray();
|
||||
backgroundJsonArray.add(0f);
|
||||
backgroundJsonArray.add((float) (0.05 * tilt));
|
||||
locationFeature.addProperty(PROPERTY_SHADOW_ICON_OFFSET, backgroundJsonArray);
|
||||
|
||||
refreshSource();
|
||||
}
|
||||
|
||||
private void updateForegroundBearing(float bearing) {
|
||||
setBearingProperty(PROPERTY_GPS_BEARING, bearing);
|
||||
}
|
||||
|
||||
private void setLayerVisibility(@NonNull String layerId, boolean visible) {
|
||||
Layer layer = style.getLayer(layerId);
|
||||
if (layer != null) {
|
||||
String targetVisibility = visible ? VISIBLE : NONE;
|
||||
if (!layer.getVisibility().value.equals(targetVisibility)) {
|
||||
layer.setProperties(visibility(visible ? VISIBLE : NONE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the visibility of the pulsing LocationComponent circle.
|
||||
*/
|
||||
@Override
|
||||
public void adjustPulsingCircleLayerVisibility(boolean visible) {
|
||||
setLayerVisibility(PULSING_CIRCLE_LAYER, visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the the pulsing LocationComponent circle based on the set options.
|
||||
*/
|
||||
@Override
|
||||
public void stylePulsingCircle(LocationComponentOptions options) {
|
||||
if (style.getLayer(PULSING_CIRCLE_LAYER) != null) {
|
||||
setLayerVisibility(PULSING_CIRCLE_LAYER, true);
|
||||
style.getLayer(PULSING_CIRCLE_LAYER).setProperties(
|
||||
circleRadius(get(PROPERTY_PULSING_RADIUS)),
|
||||
circleColor(options.pulseColor()),
|
||||
circleStrokeColor(options.pulseColor()),
|
||||
circleOpacity(get(PROPERTY_PULSING_OPACITY))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the visual appearance of the pulsing LocationComponent circle.
|
||||
*/
|
||||
@Override
|
||||
public void updatePulsingUi(float radius, @Nullable Float opacity) {
|
||||
locationFeature.addNumberProperty(PROPERTY_PULSING_RADIUS, radius);
|
||||
if (opacity != null) {
|
||||
locationFeature.addNumberProperty(PROPERTY_PULSING_OPACITY, opacity);
|
||||
}
|
||||
refreshSource();
|
||||
}
|
||||
|
||||
private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) {
|
||||
Layer layer = layerSourceProvider.generateLayer(layerId);
|
||||
addLayerToMap(layer, beforeLayerId);
|
||||
}
|
||||
|
||||
private void addAccuracyLayer() {
|
||||
Layer accuracyLayer = layerSourceProvider.generateAccuracyLayer();
|
||||
addLayerToMap(accuracyLayer, BACKGROUND_LAYER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the pulsing LocationComponent circle to the map for future use, if need be.
|
||||
*/
|
||||
private void addPulsingCircleLayerToMap() {
|
||||
Layer pulsingCircleLayer = layerSourceProvider.generatePulsingCircleLayer();
|
||||
addLayerToMap(pulsingCircleLayer, ACCURACY_LAYER);
|
||||
}
|
||||
|
||||
private void addLayerToMap(Layer layer, @NonNull String idBelowLayer) {
|
||||
style.addLayerBelow(layer, idBelowLayer);
|
||||
layerSet.add(layer.getId());
|
||||
}
|
||||
|
||||
private void addLocationSource() {
|
||||
locationSource = layerSourceProvider.generateSource(locationFeature);
|
||||
style.addSource(locationSource);
|
||||
}
|
||||
|
||||
private void refreshSource() {
|
||||
GeoJsonSource source = style.getSourceAs(LOCATION_SOURCE);
|
||||
if (source != null) {
|
||||
locationSource.setGeoJson(locationFeature);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLocationPoint(Point locationPoint) {
|
||||
JsonObject properties = locationFeature.properties();
|
||||
if (properties != null) {
|
||||
locationFeature = Feature.fromGeometry(locationPoint, properties);
|
||||
refreshSource();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBearingProperty(@NonNull String propertyId, float bearing) {
|
||||
locationFeature.addNumberProperty(propertyId, bearing);
|
||||
refreshSource();
|
||||
}
|
||||
|
||||
private void updateAccuracyRadius(float accuracy) {
|
||||
locationFeature.addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracy);
|
||||
refreshSource();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user