Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/android-for-mapbox.api
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public final class com.mapconductor.mapbox.MapboxMapViewHolder implements com.ma
public java.lang.Object getMap();
}
public final class com.mapconductor.mapbox.MapboxMapViewKt {
public static final void MapboxMapView(com.mapconductor.mapbox.MapboxViewState, androidx.compose.ui.Modifier, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.CameraRestriction, kotlin.jvm.functions.Function2<? super android.content.Context, ? super kotlin.coroutines.Continuation<? super java.lang.Boolean>, ? extends java.lang.Object>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapViewStateInterface<?>, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, com.mapconductor.core.map.MapProjection, kotlin.jvm.functions.Function3<? super com.mapconductor.mapbox.MapboxMapViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final void MapboxMapView(com.mapconductor.mapbox.MapboxViewState, androidx.compose.ui.Modifier, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.CameraRestriction, kotlin.jvm.functions.Function2<? super android.content.Context, ? super kotlin.coroutines.Continuation<? super java.lang.Boolean>, ? extends java.lang.Object>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapViewStateInterface<?>, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, com.mapconductor.core.map.MapProjection, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1<? super java.util.List<java.lang.String>, kotlin.Unit>, kotlin.jvm.functions.Function3<? super com.mapconductor.mapbox.MapboxMapViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final com.mapconductor.mapbox.MapboxMapViewController createMapboxViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.MapProjection, com.mapconductor.core.map.MutableMapServiceRegistry);
public static final com.mapconductor.mapbox.polygon.MapboxPolygonConductor getPolygonController(com.mapconductor.mapbox.MapboxMapViewHolder);
public static final com.mapconductor.mapbox.circle.MapboxCircleController getCircleController(com.mapconductor.mapbox.MapboxMapViewHolder);
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/mapconductor/mapbox/MapboxMapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ import com.mapconductor.core.circle.CircleManager
import com.mapconductor.core.map.CameraRestriction
import com.mapconductor.core.map.MapCameraPositionInterface
import com.mapconductor.core.map.MapProjection
import com.mapconductor.core.map.MapViewStyle
import com.mapconductor.core.map.MutableMapServiceRegistry
import com.mapconductor.core.map.StyleMutationStore
import com.mapconductor.core.map.VectorStyleAsDesign
import com.mapconductor.core.map.VectorStyleMutationSupportKey
import com.mapconductor.core.map.VectorStyleSupportKey
import com.mapconductor.core.map.applyStyleMutations
import com.mapconductor.core.marker.MarkerEventControllerInterface
import com.mapconductor.core.marker.MarkerManager
import com.mapconductor.core.marker.MarkerOverlayRendererInterface
Expand Down Expand Up @@ -71,6 +75,15 @@ fun MapboxMapView(
onCameraMove: OnCameraMoveHandler? = null,
onCameraMoveEnd: OnCameraMoveHandler? = null,
projection: MapProjection = MapProjection.Mercator,
/**
* How the map looks, when the app states it rather than naming a design.
*
* A vector style *is* the basemap. `com.mapconductor:vectorstyle` builds
* one; what happens underneath depends on this backend and the app does
* not have to know.
*/
style: MapViewStyle? = null,
onStyleDiagnostics: ((List<String>) -> Unit)? = null,
content: (@Composable MapboxMapViewScope.() -> Unit)? = null,
) {
val holderRef = remember { Ref<MapboxMapViewHolder>() }
Expand Down Expand Up @@ -146,6 +159,29 @@ fun MapboxMapView(
VectorStyleSupportKey,
VectorStyleAsDesign(state) { url, rules -> MapboxMapDesign.StyleUri(url, rules) },
)
// Mapbox can also be told to change a loaded style in place,
// so adjusting one need not hand it a new document -- which
// would drop its tiles and rebuild every overlay the app
// added. `VectorStyle` looks this up and takes the cheap road
// when it is here.
//
// The style is read through a lambda rather than captured:
// every design change replaces it, and a target holding the
// old one would patch a style the map has already dropped.
val mutations =
StyleMutationStore { list ->
applyStyleMutations(
MapboxStyleMutationTarget(
style = { holder.map.style },
ready = { mapController.isShowingRequestedStyle() },
),
list,
)
}
state.serviceRegistry.put(VectorStyleMutationSupportKey, mutations)
// Whatever was applied is lost the moment the map loads a
// style again; this is where it goes back on.
mapController.styleLoadedListener = { mutations.onStyleLoaded() }

holderRef.value = holder
controllerRef.value = mapController
Expand All @@ -168,6 +204,8 @@ fun MapboxMapView(
// Pass content if it needs to be rendered within the overlay providers in MapViewBase,
// or handle it here if it's specific to MapboxMapView structure before calling MapViewBase.
// For now, assuming content relates to overlay definitions.
style = style,
onStyleDiagnostics = onStyleDiagnostics,
content = content, // This might need adjustment based on how overlays are handled
customDisposableEffect = { _, holderRef ->

Expand Down
37 changes: 36 additions & 1 deletion src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ class MapboxMapViewController internal constructor(
this@MapboxMapViewController.mapDesignType = mapDesign
mapDesignTypeChangeListener?.invoke(mapDesign)
}

// Last, once the overlays are back on: a vector style
// adjustment lost everything it set with the old style, and
// this is the moment the new one is known to be complete.
styleLoadedListener?.invoke()
}
}

Expand Down Expand Up @@ -358,9 +363,39 @@ class MapboxMapViewController internal constructor(

internal var mapDesignTypeChangeListener: MapboxMapDesignTypeChangeHandler? = null

/**
* Called once the map has a style up and the overlays are back on it.
*
* How a vector style adjustment survives a design change. Kept in step
* with `MapLibreViewController.styleLoadedListener`.
*/
internal var styleLoadedListener: (() -> Unit)? = null

/** The document the map was last asked for, and the one it reported loading. */
private var requestedStyle: String? = null
private var loadedStyleValue: String? = null

/**
* Whether the style on screen is the document the map was last asked
* for.
*
* What [MapboxStyleMutationTarget] answers `isReady` with, and not the
* same question as "is a style loaded". A vector style hands the map a
* new document and then applies the deltas compiled for it; in between,
* the map still has the document being replaced, whose layers have
* other names, so every delta came back as a property this map "cannot
* set by name". Mirrors `MapLibreViewController.isShowingRequestedStyle`.
*/
internal fun isShowingRequestedStyle(): Boolean =
holder.map.style != null && loadedStyleValue == requestedStyle

override fun setMapDesignType(value: MapboxDesignType) {
val document = value.getValue()
requestedStyle = document
mainCoroutine.launch {
holder.mapView.mapboxMap.loadStyle(value.getValue())
holder.mapView.mapboxMap.loadStyle(document) {
loadedStyleValue = document
}
}
}

Expand Down
142 changes: 142 additions & 0 deletions src/main/java/com/mapconductor/mapbox/MapboxStyleMutationTarget.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.mapconductor.mapbox

import com.mapbox.bindgen.Expected
import com.mapbox.bindgen.None
import com.mapbox.bindgen.Value
import com.mapbox.maps.LayerPosition
import com.mapbox.maps.Style
import com.mapconductor.core.InternalMapConductorApi
import com.mapconductor.core.map.StyleMutationTarget

/**
* Changes a loaded Mapbox style in place.
*
* The seven calls [StyleMutationTarget] asks for, against Mapbox's style
* API. Everything above them — what a mutation means, which can be skipped,
* keeping one bad property from costing the rest — is
* `applyStyleMutations` in core, shared with every other provider.
*
* ## Why this is shorter than the MapLibre one
*
* Mapbox v11 keeps a generic door open: `setStyleLayerProperty` takes the
* spec's own property name and a `Value`, and `Value.fromJson` builds one
* straight from text. Paint keys, layout keys, `filter`, `minzoom` and
* `maxzoom` all go through that one call, and a whole layer document goes
* through `addStyleLayer`. There is no typed-constructor detour and no
* expression builder, so the compiler's JSON reaches the renderer unread.
*
* MapLibre on Android has no such door, which is why
* `MapLibreStyleMutationTarget` is three times the length for the same seven
* calls. The adapters differ; the decisions above them do not.
*/
@InternalMapConductorApi
internal class MapboxStyleMutationTarget(
private val style: () -> Style?,
/**
* Whether the style on screen is the document these deltas are for.
*
* Not `style() != null`: a vector style hands the map a new document
* and applies the deltas compiled for it straight after, and in between
* the map still has the one being replaced. See
* `MapboxMapViewController.isShowingRequestedStyle`.
*/
private val ready: () -> Boolean,
) : StyleMutationTarget {
override val isReady: Boolean get() = ready()

override fun hasLayer(layerId: String): Boolean = style()?.styleLayerExists(layerId) == true

override fun setPaint(
layerId: String,
key: String,
valueJson: String,
) = setProperty(layerId, key, valueJson)

// Mapbox does not split paint from layout on this call: both are the
// spec's property name on the layer.
override fun setLayout(
layerId: String,
key: String,
valueJson: String,
) = setProperty(layerId, key, valueJson)

override fun setFilter(
layerId: String,
filterJson: String,
) = setProperty(layerId, "filter", filterJson)

override fun setZoomRange(
layerId: String,
minZoom: Float,
maxZoom: Float,
) {
val target = style() ?: return
// Both ends, because the caller always sends both -- a mutation that
// moves one has to restate the other to be undoable.
orThrow("minzoom", target.setStyleLayerProperty(layerId, "minzoom", Value(minZoom.toDouble())))
orThrow("maxzoom", target.setStyleLayerProperty(layerId, "maxzoom", Value(maxZoom.toDouble())))
}

override fun addLayer(
layerId: String,
layerJson: String,
beforeId: String?,
) {
val target = style() ?: return
// Already there: the style was reloaded and the whole set is going
// back on.
if (target.styleLayerExists(layerId)) return
val layer = valueOf(layerJson)
// `beforeId` is the layer to go *under*, matching the style
// document's own order -- earlier in the list is drawn first.
val position =
beforeId
?.takeIf { target.styleLayerExists(it) }
?.let { LayerPosition(null, it, null) }
orThrow("addLayer", target.addStyleLayer(layer, position))
}

override fun removeLayer(layerId: String) {
val target = style() ?: return
// Mapbox calls removing an absent layer an error; the contract says
// it is not one.
if (!target.styleLayerExists(layerId)) return
orThrow("removeLayer", target.removeStyleLayer(layerId))
}

private fun setProperty(
layerId: String,
key: String,
valueJson: String,
) {
val target = style() ?: return
orThrow(key, target.setStyleLayerProperty(layerId, key, valueOf(valueJson)))
}

/**
* A JSON text as the `Value` Mapbox wants.
*
* Literals, expression arrays and whole layer objects all arrive the same
* way, so there is nothing here to decide — unlike MapLibre, which needs
* the value's shape chosen for it.
*/
private fun valueOf(json: String): Value {
val parsed = Value.fromJson(json)
if (parsed.isError) throw IllegalArgumentException("not a style value: ${parsed.error}")
return parsed.value ?: Value.nullValue()
}

/**
* Mapbox answers with an error rather than throwing, and a refused
* property that went unnoticed would leave the map looking unlike what
* the rules asked for with nothing said. Throwing puts it back on the
* path core already has for this: the mutation comes back unapplied and
* the caller reports it.
*/
private fun orThrow(
what: String,
result: Expected<String, None>,
) {
if (result.isError) throw UnsupportedOperationException("$what: ${result.error}")
}
}
Loading