diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java
index 83b6db8a2..76a639b42 100644
--- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java
+++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java
@@ -133,6 +133,7 @@ public class LocationComponentOptions implements Parcelable {
private RectF trackingMultiFingerProtectedMoveArea;
private String layerAbove;
private String layerBelow;
+ private boolean bearingOnTop;
private float trackingAnimationDurationMultiplier;
private boolean compassAnimationEnabled;
private boolean accuracyAnimationEnabled;
@@ -177,6 +178,7 @@ public class LocationComponentOptions implements Parcelable {
RectF trackingMultiFingerProtectedMoveArea,
String layerAbove,
String layerBelow,
+ boolean bearingOnTop,
float trackingAnimationDurationMultiplier,
boolean compassAnimationEnabled,
boolean accuracyAnimationEnabled,
@@ -221,6 +223,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = trackingMultiFingerProtectedMoveArea;
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
+ this.bearingOnTop = bearingOnTop;
this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier;
this.compassAnimationEnabled = compassAnimationEnabled;
this.accuracyAnimationEnabled = accuracyAnimationEnabled;
@@ -327,6 +330,9 @@ public class LocationComponentOptions implements Parcelable {
builder.layerBelow(
typedArray.getString(R.styleable.maplibre_LocationComponent_maplibre_layer_below));
+ builder.bearingOnTop(
+ typedArray.getBoolean(R.styleable.maplibre_LocationComponent_maplibre_bearing_on_top, true));
+
float minScale = typedArray.getFloat(
R.styleable.maplibre_LocationComponent_maplibre_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT);
float maxScale = typedArray.getFloat(
@@ -798,6 +804,15 @@ public class LocationComponentOptions implements Parcelable {
return layerBelow;
}
+ /**
+ * Whether the bearing icon is rendered on top of the foreground icon in the location layer stack.
+ *
+ * @return true if the bearing icon is rendered above the foreground icon, false otherwise.
+ */
+ public boolean bearingOnTop() {
+ return bearingOnTop;
+ }
+
/**
* Get the tracking animation duration multiplier.
*
@@ -927,6 +942,7 @@ public class LocationComponentOptions implements Parcelable {
+ "trackingMultiFingerProtectedMoveArea=" + trackingMultiFingerProtectedMoveArea + ", "
+ "layerAbove=" + layerAbove
+ "layerBelow=" + layerBelow
+ + "bearingOnTop=" + bearingOnTop
+ "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier
+ "pulseEnabled=" + pulseEnabled
+ "pulseFadeEnabled=" + pulseFadeEnabled
@@ -1057,6 +1073,10 @@ public class LocationComponentOptions implements Parcelable {
return false;
}
+ if (bearingOnTop != options.bearingOnTop) {
+ return false;
+ }
+
if (pulseEnabled != options.pulseEnabled) {
return false;
}
@@ -1120,6 +1140,7 @@ public class LocationComponentOptions implements Parcelable {
? trackingMultiFingerProtectedMoveArea.hashCode() : 0);
result = 31 * result + (layerAbove != null ? layerAbove.hashCode() : 0);
result = 31 * result + (layerBelow != null ? layerBelow.hashCode() : 0);
+ result = 31 * result + (bearingOnTop ? 1 : 0);
result = 31 * result + (trackingAnimationDurationMultiplier != +0.0f
? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0);
result = 31 * result + (compassAnimationEnabled ? 1 : 0);
@@ -1171,6 +1192,7 @@ public class LocationComponentOptions implements Parcelable {
dest.writeParcelable(this.trackingMultiFingerProtectedMoveArea, flags);
dest.writeString(this.layerAbove);
dest.writeString(this.layerBelow);
+ dest.writeByte(this.bearingOnTop ? (byte) 1 : (byte) 0);
dest.writeFloat(this.trackingAnimationDurationMultiplier);
dest.writeByte(this.compassAnimationEnabled ? (byte) 1 : (byte) 0);
dest.writeByte(this.accuracyAnimationEnabled ? (byte) 1 : (byte) 0);
@@ -1214,6 +1236,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = in.readParcelable(RectF.class.getClassLoader());
this.layerAbove = in.readString();
this.layerBelow = in.readString();
+ this.bearingOnTop = in.readByte() != 0;
this.trackingAnimationDurationMultiplier = in.readFloat();
this.compassAnimationEnabled = in.readByte() != 0;
this.accuracyAnimationEnabled = in.readByte() != 0;
@@ -1338,6 +1361,7 @@ public class LocationComponentOptions implements Parcelable {
private RectF trackingMultiFingerProtectedMoveArea;
private String layerAbove;
private String layerBelow;
+ private boolean bearingOnTop;
private Float trackingAnimationDurationMultiplier;
private Boolean compassAnimationEnabled;
private Boolean accuracyAnimationEnabled;
@@ -1385,6 +1409,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = source.trackingMultiFingerProtectedMoveArea();
this.layerAbove = source.layerAbove();
this.layerBelow = source.layerBelow();
+ this.bearingOnTop = source.bearingOnTop();
this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier();
this.compassAnimationEnabled = source.compassAnimationEnabled();
this.accuracyAnimationEnabled = source.accuracyAnimationEnabled();
@@ -1866,6 +1891,19 @@ public class LocationComponentOptions implements Parcelable {
return this;
}
+ /**
+ * Sets whether the bearing icon is rendered on top of the foreground icon in the location
+ * layer stack. When true, the bearing layer is placed above the foreground layer; when false,
+ * it is rendered below.
+ *
+ * @param bearingOnTop true to render the bearing icon above the foreground icon, false for below.
+ */
+ @NonNull
+ public LocationComponentOptions.Builder bearingOnTop(boolean bearingOnTop) {
+ this.bearingOnTop = bearingOnTop;
+ return this;
+ }
+
/**
* Sets the tracking animation duration multiplier.
*
@@ -2065,6 +2103,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea,
this.layerAbove,
this.layerBelow,
+ this.bearingOnTop,
this.trackingAnimationDurationMultiplier,
this.compassAnimationEnabled,
this.accuracyAnimationEnabled,
diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentPositionManager.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentPositionManager.java
index f0a3185dd..dd5f25349 100644
--- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentPositionManager.java
+++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentPositionManager.java
@@ -17,22 +17,28 @@ class LocationComponentPositionManager {
@Nullable
private String layerBelow;
- LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove, @Nullable String layerBelow) {
+ public boolean bearingOnTop;
+
+ LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove,
+ @Nullable String layerBelow, Boolean bearingOnTop) {
this.style = style;
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
+ this.bearingOnTop = bearingOnTop;
}
/**
* Returns true whenever layer above/below configuration has changed and requires re-layout.
*/
- boolean update(@Nullable String layerAbove, @Nullable String layerBelow) {
+ boolean update(@Nullable String layerAbove, @Nullable String layerBelow, boolean bearingOnTop) {
boolean requiresUpdate =
!(this.layerAbove == layerAbove || (this.layerAbove != null && this.layerAbove.equals(layerAbove)))
- || !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow)));
+ || !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow)))
+ || (this.bearingOnTop != bearingOnTop);
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
+ this.bearingOnTop = bearingOnTop;
return requiresUpdate;
}
diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationLayerController.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationLayerController.java
index a1039c12e..a17fd617b 100644
--- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationLayerController.java
+++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationLayerController.java
@@ -74,7 +74,8 @@ final class LocationLayerController {
}
void initializeComponents(Style style, LocationComponentOptions options) {
- this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(), options.layerBelow());
+ this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(),
+ options.layerBelow(), options.bearingOnTop());
locationLayerRenderer.initializeComponents(style);
locationLayerRenderer.addLayers(positionManager);
applyStyle(options);
@@ -87,7 +88,7 @@ final class LocationLayerController {
}
void applyStyle(@NonNull LocationComponentOptions options) {
- if (positionManager.update(options.layerAbove(), options.layerBelow())) {
+ if (positionManager.update(options.layerAbove(), options.layerBelow(), options.bearingOnTop())) {
locationLayerRenderer.removeLayers();
locationLayerRenderer.addLayers(positionManager);
if (isHidden) {
diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java
index a647b7534..8ec6601cd 100644
--- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java
+++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java
@@ -83,15 +83,29 @@ final class SymbolLocationLayerRenderer implements LocationLayerRenderer {
@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);
+ if (positionManager.bearingOnTop) {
+ // 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);
+ } else {
+ // positions the top-most reference layer
+ Layer layer = layerSourceProvider.generateLayer(FOREGROUND_LAYER);
+ positionManager.addLayerToMap(layer);
+ layerSet.add(layer.getId());
+
+ // adds remaining layers while keeping the order
+ addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
+ addSymbolLayer(BEARING_LAYER, BACKGROUND_LAYER);
+ addSymbolLayer(SHADOW_LAYER, BEARING_LAYER);
+ }
+
addAccuracyLayer();
addPulsingCircleLayerToMap();
}
diff --git a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml
index 622e18640..78a822286 100644
--- a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml
+++ b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml
@@ -181,6 +181,7 @@
+
diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationComponentPositionManagerTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationComponentPositionManagerTest.kt
index 1b18d9dcb..9b023a247 100644
--- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationComponentPositionManagerTest.kt
+++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationComponentPositionManagerTest.kt
@@ -26,70 +26,91 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun update_noChange_null() {
- val positionManager = LocationComponentPositionManager(style, null, null)
- val requiresUpdate = positionManager.update(null, null)
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ val requiresUpdate = positionManager.update(null, null, false)
assertFalse(requiresUpdate)
}
@Test
fun update_noChange_above() {
- val positionManager = LocationComponentPositionManager(style, "above", null)
- val requiresUpdate = positionManager.update("above", null)
+ val positionManager = LocationComponentPositionManager(style, "above", null, false)
+ val requiresUpdate = positionManager.update("above", null, false)
assertFalse(requiresUpdate)
}
@Test
fun update_noChange_below() {
- val positionManager = LocationComponentPositionManager(style, null, "below")
- val requiresUpdate = positionManager.update(null, "below")
+ val positionManager = LocationComponentPositionManager(style, null, "below", false)
+ val requiresUpdate = positionManager.update(null, "below", false)
+ assertFalse(requiresUpdate)
+ }
+
+ @Test
+ fun update_noChange_bearingOnTop() {
+ val positionManager = LocationComponentPositionManager(style, null, null, true)
+ val requiresUpdate = positionManager.update(null, null, true)
assertFalse(requiresUpdate)
}
@Test
fun update_fromNull_above() {
- val positionManager = LocationComponentPositionManager(style, null, null)
- val requiresUpdate = positionManager.update("above", null)
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ val requiresUpdate = positionManager.update("above", null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromNull_below() {
- val positionManager = LocationComponentPositionManager(style, null, null)
- val requiresUpdate = positionManager.update(null, "below")
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ val requiresUpdate = positionManager.update(null, "below", false)
assertTrue(requiresUpdate)
}
@Test
fun update_toNull_above() {
- val positionManager = LocationComponentPositionManager(style, "above", null)
- val requiresUpdate = positionManager.update(null, null)
+ val positionManager = LocationComponentPositionManager(style, "above", null, false)
+ val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_toNull_below() {
- val positionManager = LocationComponentPositionManager(style, null, "below")
- val requiresUpdate = positionManager.update(null, null)
+ val positionManager = LocationComponentPositionManager(style, null, "below", false)
+ val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromValue_above() {
- val positionManager = LocationComponentPositionManager(style, "above1", null)
- val requiresUpdate = positionManager.update("above2", null)
+ val positionManager = LocationComponentPositionManager(style, "above1", null, false)
+ val requiresUpdate = positionManager.update("above2", null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromValue_below() {
- val positionManager = LocationComponentPositionManager(style, null, "below1")
- val requiresUpdate = positionManager.update(null, "below2")
+ val positionManager = LocationComponentPositionManager(style, null, "below1", false)
+ val requiresUpdate = positionManager.update(null, "below2", false)
+ assertTrue(requiresUpdate)
+ }
+
+ @Test
+ fun update_bearingOnTop_changed() {
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ val requiresUpdate = positionManager.update(null, null, true)
+ assertTrue(requiresUpdate)
+ }
+
+ @Test
+ fun update_bearingOnTop_changedToFalse() {
+ val positionManager = LocationComponentPositionManager(style, null, null, true)
+ val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun addLayer_noModifier() {
- val positionManager = LocationComponentPositionManager(style, null, null)
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayer(layer) }
@@ -97,7 +118,7 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_above() {
- val positionManager = LocationComponentPositionManager(style, "above", null)
+ val positionManager = LocationComponentPositionManager(style, "above", null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayerAbove(layer, "above") }
@@ -105,7 +126,7 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_below() {
- val positionManager = LocationComponentPositionManager(style, null, "below")
+ val positionManager = LocationComponentPositionManager(style, null, "below", false)
positionManager.addLayerToMap(layer)
verify { style.addLayerBelow(layer, "below") }
@@ -113,8 +134,8 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_afterUpdate_above() {
- val positionManager = LocationComponentPositionManager(style, null, null)
- positionManager.update("above", null)
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ positionManager.update("above", null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayerAbove(layer, "above") }
@@ -122,8 +143,8 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_afterUpdate_below() {
- val positionManager = LocationComponentPositionManager(style, null, null)
- positionManager.update(null, "below")
+ val positionManager = LocationComponentPositionManager(style, null, null, false)
+ positionManager.update(null, "below", false)
positionManager.addLayerToMap(layer)
verify { style.addLayerBelow(layer, "below") }
diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationLayerControllerTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationLayerControllerTest.kt
index 229e1325e..e77f73d20 100644
--- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationLayerControllerTest.kt
+++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/location/LocationLayerControllerTest.kt
@@ -102,7 +102,7 @@ class LocationLayerControllerTest : BaseTest() {
false
)
Mockito.verify(style)
- .addLayerBelow(shadowLayer, LocationComponentConstants.BACKGROUND_LAYER)
+ .addLayerBelow(shadowLayer, LocationComponentConstants.BEARING_LAYER)
}
@Test
@@ -181,8 +181,7 @@ class LocationLayerControllerTest : BaseTest() {
internalRenderModeChangedListener,
false
)
- Mockito.verify(style)
- .addLayerBelow(foregroundLayer, LocationComponentConstants.BEARING_LAYER)
+ Mockito.verify(style).addLayer(foregroundLayer)
}
@Test
@@ -223,7 +222,8 @@ class LocationLayerControllerTest : BaseTest() {
internalRenderModeChangedListener,
false
)
- Mockito.verify(style).addLayerBelow(bearingLayer, layerBelow)
+ Mockito.verify(style)
+ .addLayerBelow(bearingLayer, LocationComponentConstants.BACKGROUND_LAYER)
}
@Test
@@ -757,13 +757,13 @@ class LocationLayerControllerTest : BaseTest() {
Mockito.verify(style).removeLayer(LocationComponentConstants.BACKGROUND_LAYER)
Mockito.verify(style).removeLayer(LocationComponentConstants.SHADOW_LAYER)
Mockito.verify(style).removeLayer(LocationComponentConstants.ACCURACY_LAYER)
- Mockito.verify(style).addLayerBelow(bearingLayer2, layerBelow)
- Mockito.verify(style)
- .addLayerBelow(foregroundLayer2, LocationComponentConstants.BEARING_LAYER)
+ Mockito.verify(style).addLayerBelow(foregroundLayer2, layerBelow)
Mockito.verify(style)
.addLayerBelow(backgroundLayer2, LocationComponentConstants.FOREGROUND_LAYER)
Mockito.verify(style)
- .addLayerBelow(shadowLayer2, LocationComponentConstants.BACKGROUND_LAYER)
+ .addLayerBelow(bearingLayer2, LocationComponentConstants.BACKGROUND_LAYER)
+ Mockito.verify(style)
+ .addLayerBelow(shadowLayer2, LocationComponentConstants.BEARING_LAYER)
Mockito.verify(style)
.addLayerBelow(accuracyLayer2, LocationComponentConstants.BACKGROUND_LAYER)
}