Rename Gradle targets Android (#2226)

This commit is contained in:
Bart Louwers
2024-04-02 23:15:50 +02:00
committed by GitHub
parent 37170ed61c
commit 9f8ae7a98b
1081 changed files with 132 additions and 370 deletions

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,7 @@
package org.maplibre.android;
public enum WellKnownTileServer {
Mapbox,
MapTiler,
MapLibre;
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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() + "]]";
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Annotation API classes.
*/
package org.maplibre.android.annotations;

View File

@@ -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;
}
}

View File

@@ -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
+ '}';
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Attribution API classes.
*/
package org.maplibre.android.attribution;

View File

@@ -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)
}
}
}
}

View File

@@ -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?
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Camera API classes.
*/
package org.maplibre.android.camera

View File

@@ -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.
}
}

View File

@@ -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";
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Constant API classes.
*/
package org.maplibre.android.constants;

View File

@@ -0,0 +1,8 @@
package org.maplibre.android.exceptions;
public class CalledFromWorkerThreadException extends RuntimeException {
public CalledFromWorkerThreadException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View File

@@ -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.");
}
}

View File

@@ -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");
}
}

View File

@@ -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.");
}
}

View File

@@ -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);
}
}

View File

@@ -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.");
}
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Exception API classes.
*/
package org.maplibre.android.exceptions;

View File

@@ -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
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Contains the MapLibre Maps Android Geometry API classes.
*/
package org.maplibre.android.geometry

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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 "";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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");
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,4 @@
/**
* Do not use this package. Internal use only.
*/
package org.maplibre.android.http;

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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)
);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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)
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}

View File

@@ -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.");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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()
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,7 @@
package org.maplibre.android.location;
interface OnCameraMoveInvalidateListener {
void onInvalidateCameraMove();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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