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
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public actual fun Map(
)
} else {
markers.forEach { marker ->
key(marker.id, marker.contentId) {
key(marker.getId(), marker.contentId) {
val content = customMarkerContent[marker.contentId]

if (content != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import androidx.annotation.RestrictTo
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonIgnoreUnknownKeys

/**
* Theme options for map appearance.
Expand Down Expand Up @@ -89,17 +89,19 @@ public data class MapUISettings(
* parameter.
*/
@Serializable
@JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
public data class Marker(
val coordinates: Coordinates,
val title: String? = "No title was provided",
val androidMarkerOptions: AndroidMarkerOptions = AndroidMarkerOptions(),
val iosMarkerOptions: IosMarkerOptions? = null,
val contentId: String? = null,
) {
@OptIn(ExperimentalUuidApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val id: String = Uuid.random().toString()
}
)

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun Marker.getId(): String = "marker_${hashCode()}"

/**
* Represents a circle overlay on the map.
Expand All @@ -116,9 +118,11 @@ public data class Circle(
val color: Color? = null,
val lineColor: Color? = null,
val lineWidth: Float? = null,
) {
@OptIn(ExperimentalUuidApi::class) internal val id: String = Uuid.random().toString()
}
)

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun Circle.getId(): String = "circle_${hashCode()}"

/**
* Represents a polygon overlay on the map.
Expand All @@ -133,9 +137,11 @@ public data class Polygon(
val lineWidth: Float,
val color: Color? = null,
val lineColor: Color? = null,
) {
@OptIn(ExperimentalUuidApi::class) internal val id: String = Uuid.random().toString()
}
)

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun Polygon.getId(): String = "polygon_${hashCode()}"

/**
* Represents a polyline overlay on the map.
Expand All @@ -148,9 +154,11 @@ public data class Polyline(
val coordinates: List<Coordinates>,
val width: Float,
val lineColor: Color? = null,
) {
@OptIn(ExperimentalUuidApi::class) internal val id: String = Uuid.random().toString()
}
)

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun Polyline.getId(): String = "polyline_${hashCode()}"

/**
* Represents geographical coordinates (latitude and longitude).
Expand Down Expand Up @@ -184,9 +192,7 @@ public data class CameraPosition(
* @property items The list of [Marker] that make up this cluster
*/
@Serializable
public data class Cluster(val coordinates: Coordinates, val size: Int, val items: List<Marker>) {
@OptIn(ExperimentalUuidApi::class) internal val id: String = Uuid.random().toString()
}
public data class Cluster(val coordinates: Coordinates, val size: Int, val items: List<Marker>)

/**
* Configuration options for marker clustering.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public actual fun Map(
LaunchedEffect(allMarkers, circles, polygons, polylines, mapDelegate) {
val view = mapView ?: return@LaunchedEffect

val activeIds = allMarkers.map { it.id }.toSet()
val activeIds = allMarkers.map { it.getId() }.toSet()
mapDelegate?.pruneCache(activeIds)

markerMapping.clear()
Expand Down Expand Up @@ -220,13 +220,13 @@ public actual fun Map(
Box(modifier = Modifier.alpha(0f)) {
allMarkers.forEach { marker ->
val content = customMarkerContent[marker.contentId]
if (content != null && mapDelegate?.getCachedImage(marker.id) == null) {
key(marker.id, marker.contentId) {
if (content != null && mapDelegate?.getCachedImage(marker.getId()) == null) {
key(marker.getId(), marker.contentId) {
MarkerSnapshotter(
content = { content(marker) },
onSnapshotReady = { bitmap ->
val uiImage = bitmap.toUIImage()
if (uiImage != null) mapDelegate?.onBitmapReady(marker.id, uiImage)
if (uiImage != null) mapDelegate?.onBitmapReady(marker.getId(), uiImage)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ internal class MapDelegate(
}

private fun findAnnotationById(id: String): MKAnnotationProtocol? {
val markerAnn = markerMapping.entries.find { it.value.id == id }?.key
val markerAnn = markerMapping.entries.find { it.value.getId() == id }?.key
if (markerAnn != null) return markerAnn

return mapView.annotations.filterIsInstance<MKClusterAnnotation>().find { clusterAnn ->
Expand Down Expand Up @@ -366,7 +366,7 @@ internal class MapDelegate(
view.annotation = annotation
view.clusteringIdentifier =
if (clusterSettings.enabled) "kmp_marker_cluster_group" else null
view.setMarkerImage(imageCache[marker.id])
view.setMarkerImage(imageCache[marker.getId()])

return view
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Do
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun generateClusterId(markers: List<Marker>): String {
val size = markers.size
val contentHash = markers.map(Marker::id).sorted().hashCode()
val contentHash = markers.map { it.getId() }.sorted().hashCode()

return "cluster_${size}_$contentHash"
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ internal fun List<Marker>.toJson(webCustomMarkerContent: Map<String, (Marker) ->
* @return [JsonObject] containing marker ID, position, title, and metadata.
*/
internal fun Marker.toJson() = buildJsonObject {
put("id", id)
put("id", getId())
put("position", coordinates.toJson())
put("title", title)
put("opacity", 1.0f)
Expand Down Expand Up @@ -161,7 +161,7 @@ internal fun Color.toHex() = "#%06X".format(0xFFFFFF and toArgb())
* @return [JsonObject] with center, radius, and stroke/fill styles.
*/
internal fun Circle.toJson() = buildJsonObject {
put("id", id)
put("id", getId())
put("center", center.toJson())
put("radius", radius)

Expand All @@ -183,7 +183,7 @@ internal fun Circle.toJson() = buildJsonObject {
* @return [JsonObject] containing the list of paths and styling properties.
*/
internal fun Polygon.toJson() = buildJsonObject {
put("id", id)
put("id", getId())
put("paths", JsonArray(coordinates.map(Coordinates::toJson)))
put("strokeWeight", lineWidth)

Expand All @@ -203,7 +203,7 @@ internal fun Polygon.toJson() = buildJsonObject {
* @return [JsonObject] containing the path coordinates and line styles.
*/
internal fun Polyline.toJson() = buildJsonObject {
put("id", id)
put("id", getId())
put("path", JsonArray(coordinates.map(Coordinates::toJson)))
put("strokeWeight", width)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.multiplatform.webview.jsbridge.IJsMessageHandler
import com.multiplatform.webview.jsbridge.JsMessage
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.web.WebViewNavigator
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ internal fun registerMapEvents(

jsBridge.registerHandler("onMarkerClick") { params, _ ->
val markerId = params
val clickedMarker = markers.find { marker -> marker.id == markerId }
val clickedMarker = markers.find { marker -> marker.getId() == markerId }
clickedMarker?.let { onMarkerClick?.invoke(it) }
}

Expand All @@ -66,15 +67,15 @@ internal fun registerMapEvents(
jsBridge.registerHandler("onMapLoaded") { _, _ -> onMapLoaded?.invoke() }

jsBridge.registerHandler("onCircleClick") { id, _ ->
circles.find { it.id == id }?.let { onCircleClick?.invoke(it) }
circles.find { it.getId() == id }?.let { onCircleClick?.invoke(it) }
}

jsBridge.registerHandler("onPolygonClick") { id, _ ->
polygons.find { it.id == id }?.let { onPolygonClick?.invoke(it) }
polygons.find { it.getId() == id }?.let { onPolygonClick?.invoke(it) }
}

jsBridge.registerHandler("onPolylineClick") { id, _ ->
polylines.find { it.id == id }?.let { onPolylineClick?.invoke(it) }
polylines.find { it.getId() == id }?.let { onPolylineClick?.invoke(it) }
}

jsBridge.registerHandler("onClusterClick") { params, _ ->
Expand All @@ -83,13 +84,14 @@ internal fun registerMapEvents(
}

jsBridge.registerHandler("renderCluster") { params, navigator ->
val cluster = Json.decodeFromString<Cluster>(params)
val clusterJS = Json.decodeFromString<ClusterJS>(params)
val cluster = clusterJS.toCluster()
val html = clusterSettings.webClusterContent?.invoke(cluster)

if (html != null) {
val formattedHtml = html.trimIndent()
val escapedHtmlJson = Json.encodeToString(formattedHtml)
navigator?.evaluateJavaScript("applyClusterHtml(${cluster.id}, '$escapedHtmlJson')")
navigator?.evaluateJavaScript("applyClusterHtml(${clusterJS.id}, '$escapedHtmlJson')")
}
}
}
Expand All @@ -116,3 +118,26 @@ private fun WebViewJsBridge.registerHandler(
}
)
}

/**
* Internal Data Transfer Object (DTO) used for deserializing cluster data received from the
* JavaScript Google Maps bridge.
*
* This class mirrors the JavaScript cluster structure, including the [id] generated by the marker
* clusterer, which is required for further communication (e.g., custom rendering via
* [applyClusterHtml]).
*/
@Serializable
private data class ClusterJS(
val id: String,
val coordinates: Coordinates,
val size: Int,
val items: List<Marker>,
)

/**
* Converts a [ClusterJS] object to a [Cluster] object.
*
* @return A [Cluster] object containing the same data as the [ClusterJS] object.
*/
private fun ClusterJS.toCluster() = Cluster(coordinates = coordinates, size = size, items = items)
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.swmansion.kmpmaps.core.MapUISettings
import com.swmansion.kmpmaps.core.Marker
import com.swmansion.kmpmaps.core.Polygon
import com.swmansion.kmpmaps.core.Polyline
import com.swmansion.kmpmaps.core.getId
import com.swmansion.kmpmaps.core.toAppleMapsColor
import kotlin.collections.set
import kotlinx.cinterop.ExperimentalForeignApi
Expand Down Expand Up @@ -71,7 +72,7 @@ internal fun updateGoogleMapsMarkers(
)

if (marker.contentId != null && customMarkerContent.containsKey(marker.contentId)) {
val cachedImage = mapDelegate?.getCachedImage(marker.id)
val cachedImage = mapDelegate?.getCachedImage(marker.getId())

if (cachedImage != null) {
gmsMarker.setIcon(cachedImage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.swmansion.kmpmaps.core.Marker
import com.swmansion.kmpmaps.core.MarkerSnapshotter
import com.swmansion.kmpmaps.core.Polygon
import com.swmansion.kmpmaps.core.Polyline
import com.swmansion.kmpmaps.core.getId
import com.swmansion.kmpmaps.core.toUIImage
import com.swmansion.kmpmaps.googlemaps.GoogleMapsInitializer.ensureInitialized
import kotlinx.cinterop.BetaInteropApi
Expand Down Expand Up @@ -167,7 +168,7 @@ public actual fun Map(
) {
val view = mapView ?: return@LaunchedEffect

val activeIds = allMarkers.map { it.id }.toSet()
val activeIds = allMarkers.map { it.getId() }.toSet()
mapDelegate?.pruneCache(activeIds)

val manager = clusterManager
Expand Down Expand Up @@ -250,15 +251,17 @@ public actual fun Map(

Box(modifier = Modifier.alpha(0f)) {
allMarkers.forEach { marker ->
if (mapDelegate?.getCachedImage(marker.id) == null) {
key(marker.id) {
if (mapDelegate?.getCachedImage(marker.getId()) == null) {
key(marker.getId()) {
MarkerSnapshotter(
content = {
val content = customMarkerContent[marker.contentId]
if (content != null) content(marker) else DefaultPin(marker)
},
onSnapshotReady = { bitmap ->
bitmap.toUIImage()?.let { mapDelegate?.onBitmapReady(marker.id, it) }
bitmap.toUIImage()?.let {
mapDelegate?.onBitmapReady(marker.getId(), it)
}
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.swmansion.kmpmaps.core.Coordinates
import com.swmansion.kmpmaps.core.Marker
import com.swmansion.kmpmaps.core.Polygon
import com.swmansion.kmpmaps.core.Polyline
import com.swmansion.kmpmaps.core.getId
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCSignatureOverride
Expand Down Expand Up @@ -162,7 +163,7 @@ internal class MapDelegate(
imageCache[id] = image
renderingQueue.remove(id)

val gmsMarker = markerMapping.entries.find { it.value.id == id }?.key
val gmsMarker = markerMapping.entries.find { it.value.getId() == id }?.key
gmsMarker?.let {
it.setIcon(image)
it.setTracksViewChanges(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.swmansion.kmpmaps.core.Coordinates
import com.swmansion.kmpmaps.core.DefaultCluster
import com.swmansion.kmpmaps.core.Marker
import com.swmansion.kmpmaps.core.generateClusterId
import com.swmansion.kmpmaps.core.getId
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
import platform.UIKit.UIImage
Expand All @@ -38,14 +39,14 @@ internal class MarkerClusterManagerDelegate(
when (userData) {
is MarkerClusterItem -> {
val marker = userData.marker
val cached = mapDelegate?.getCachedImage(marker.id)
val cached = mapDelegate?.getCachedImage(marker.getId())

if (cached != null) {
willRenderMarker.setIcon(cached)
} else {
val content = customMarkerContent[marker.contentId]
if (content != null) {
mapDelegate?.renderingQueue[marker.id] = { content(marker) }
mapDelegate?.renderingQueue[marker.getId()] = { content(marker) }
willRenderMarker.setIcon(UIImage())
}
}
Expand Down
Loading