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: 2 additions & 0 deletions mapconductor-bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ val moduleInfo = mapOf(
"for-mapbox" to getModuleVersion(":mapconductor-for-mapbox"),
"for-maplibre" to getModuleVersion(":mapconductor-for-maplibre"),
"icons" to getModuleVersion(":mapconductor-icons"),
"heatmap" to getModuleVersion(":mapconductor-heatmap"),
"tile-server" to getModuleVersion(":mapconductor-tile-server"),
"marker-clustering" to getModuleVersion(":mapconductor-marker-clustering"),
"marker-native-strategy" to getModuleVersion(":mapconductor-marker-native-strategy"),
"marker-strategy" to getModuleVersion(":mapconductor-marker-strategy")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.mapconductor.core.polygon.PolygonOverlay
import com.mapconductor.core.polygon.PolygonState
import com.mapconductor.core.polyline.PolylineOverlay
import com.mapconductor.core.polyline.PolylineState
import com.mapconductor.core.raster.RasterLayerOverlay
import com.mapconductor.core.raster.RasterLayerState
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -37,6 +39,8 @@ open class MapViewScope {
val polygonRemoveSharedFlow = MutableSharedFlow<String>(1000)
val groundImageFlow = MutableStateFlow<MutableMap<String, GroundImageState>>(mutableMapOf())
val groundImageRemoveSharedFlow = MutableSharedFlow<String>(1000)
val rasterLayerFlow = MutableStateFlow<MutableMap<String, RasterLayerState>>(mutableMapOf())
val rasterLayerRemoveSharedFlow = MutableSharedFlow<String>(1000)

init {
CoroutineScope(Dispatchers.IO).launch {
Expand Down Expand Up @@ -78,6 +82,16 @@ open class MapViewScope {
groundImageFlow.value = newMap
}
}

CoroutineScope(Dispatchers.IO).launch {
rasterLayerRemoveSharedFlow.debounceBatch(5.milliseconds, 300).collect { ids ->
val newMap = rasterLayerFlow.value.toMutableMap()
ids.forEach { id ->
newMap.remove(id)
}
rasterLayerFlow.value = newMap
}
}
}

fun buildRegistry(): MapOverlayRegistry {
Expand All @@ -87,6 +101,7 @@ open class MapViewScope {
registry.register(PolylineOverlay(polylineFlow))
registry.register(PolygonOverlay(polygonFlow))
registry.register(GroundImageOverlay(groundImageFlow))
registry.register(RasterLayerOverlay(rasterLayerFlow))
return registry
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mapconductor.core.heatmap

import com.mapconductor.core.debounceBatch
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

class HeatmapPointCollector(
scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) {
private val addSharedFlow = MutableSharedFlow<HeatmapPointState>(1000)
private val removeSharedFlow = MutableSharedFlow<String>(1000)
val flow = MutableStateFlow<MutableMap<String, HeatmapPointState>>(mutableMapOf())

init {
scope.launch {
addSharedFlow.debounceBatch(5.milliseconds, 100).collect { states ->
val newMap = flow.value.toMutableMap()
states.forEach { state ->
newMap[state.id] = state
}
flow.value = newMap
}
}

scope.launch {
removeSharedFlow.debounceBatch(5.milliseconds, 300).collect { ids ->
val newMap = flow.value.toMutableMap()
ids.forEach { id ->
newMap.remove(id)
}
flow.value = newMap
}
}
}

suspend fun add(state: HeatmapPointState) {
addSharedFlow.emit(state)
}

fun remove(id: String) {
removeSharedFlow.tryEmit(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mapconductor.core.heatmap

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import com.mapconductor.core.MapViewScope
import com.mapconductor.core.features.GeoPoint

@Composable
fun MapViewScope.HeatmapPoint(state: HeatmapPointState) {
val collector = LocalHeatmapPointCollector.current
LaunchedEffect(state) {
collector.add(state)
}

DisposableEffect(state.id) {
onDispose {
collector.remove(state.id)
}
}
}

@Composable
fun MapViewScope.HeatmapPoint(
position: GeoPoint,
weight: Double = 1.0,
id: String? = null,
) {
val state =
HeatmapPointState(
id = id,
position = position,
weight = weight,
)
HeatmapPoint(state)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mapconductor.core.heatmap

import androidx.compose.runtime.compositionLocalOf

val LocalHeatmapPointCollector =
compositionLocalOf<HeatmapPointCollector> {
error("HeatmapPoint must be under the <HeatmapOverlay />")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.mapconductor.core.heatmap

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import com.mapconductor.core.features.GeoPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged

class HeatmapPointState(
position: GeoPoint,
weight: Double = 1.0,
id: String? = null,
) {
val id =
(
id ?: pointId(
listOf(
position.hashCode(),
weight.hashCode(),
),
)
).toString()

private fun pointId(hashCodes: List<Int>): Int =
hashCodes.reduce { result, hashCode ->
31 * result + hashCode
}

private val currentPosition = mutableStateOf(position)
var position: GeoPoint
get() = currentPosition.value
set(value) {
currentPosition.value = value
}

var weight by mutableStateOf(weight)

fun copy(
id: String? = this.id,
position: GeoPoint = this.position,
weight: Double = this.weight,
): HeatmapPointState =
HeatmapPointState(
id = id,
position = position,
weight = weight,
)

override fun equals(other: Any?): Boolean {
val otherState = other as? HeatmapPointState ?: return false
return hashCode() == otherState.hashCode()
}

override fun hashCode(): Int {
var result = weight.hashCode()
result = 31 * result + currentPosition.value.latitude.hashCode()
result = 31 * result + currentPosition.value.longitude.hashCode()
result = 31 * result + currentPosition.value.altitude.hashCode()
return result
}

fun fingerPrint(): HeatmapPointFingerPrint =
HeatmapPointFingerPrint(
id.hashCode(),
weight.hashCode(),
currentPosition.value.latitude.hashCode(),
currentPosition.value.longitude.hashCode(),
currentPosition.value.altitude.hashCode(),
)

fun asFlow(): Flow<HeatmapPointFingerPrint> = snapshotFlow { fingerPrint() }.distinctUntilChanged()
}

data class HeatmapPointFingerPrint(
val id: Int,
val weight: Int,
val latitude: Int,
val longitude: Int,
val altitude: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import com.mapconductor.core.polygon.LocalPolygonCollector
import com.mapconductor.core.polygon.PolygonCapable
import com.mapconductor.core.polyline.LocalPolylineCollector
import com.mapconductor.core.polyline.PolylineCapable
import com.mapconductor.core.raster.LocalRasterLayerCollector
import com.mapconductor.core.raster.RasterLayerCapable
import com.mapconductor.settings.Settings
import android.util.Log
import android.view.View
Expand Down Expand Up @@ -121,6 +123,18 @@ fun <
}
}
}
val rasterLayers = scope.rasterLayerFlow.collectAsState()
(controller as? RasterLayerCapable)?.let { rasterLayerCapable ->
rasterLayers.value.values.forEach { rasterLayerState ->
LaunchedEffect(rasterLayerState.id) {
rasterLayerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest {
if (rasterLayerCapable.hasRasterLayer(rasterLayerState)) {
rasterLayerCapable.updateRasterLayer(rasterLayerState)
}
}
}
}
}
val polygons = scope.polygonFlow.collectAsState()
polygons.value.values.forEach { polygonState ->
LaunchedEffect(polygonState.id) {
Expand Down Expand Up @@ -241,6 +255,7 @@ fun <
LocalPolylineCollector provides scope.polylineFlow,
LocalPolygonCollector provides scope.polygonFlow,
LocalGroundImageCollector provides scope.groundImageFlow,
LocalRasterLayerCollector provides scope.rasterLayerFlow,
) {
// 子(Marker など)の収集&描画
with(scope) { content?.invoke(this) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.mapconductor.core.raster

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import java.io.Serializable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged

class RasterLayerState(
source: RasterSource,
opacity: Float = 1.0f,
visible: Boolean = true,
id: String? = null,
extra: Serializable? = null,
) {
val id =
(
id ?: rasterLayerId(
listOf(
source.hashCode(),
opacity.hashCode(),
visible.hashCode(),
extra?.hashCode() ?: 0,
),
)
).toString()

var source by mutableStateOf(source)
var opacity by mutableStateOf(opacity)
var visible by mutableStateOf(visible)
var extra by mutableStateOf(extra)

private fun rasterLayerId(hashCodes: List<Int>): Int =
hashCodes.reduce { result, hashCode ->
31 * result + hashCode
}

override fun equals(other: Any?): Boolean {
val otherState = (other as? RasterLayerState) ?: return false
return hashCode() == otherState.hashCode()
}

override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + opacity.hashCode()
result = 31 * result + visible.hashCode()
result = 31 * result + (extra?.hashCode() ?: 0)
return result
}

fun copy(
source: RasterSource = this.source,
opacity: Float = this.opacity,
visible: Boolean = this.visible,
id: String? = this.id,
extra: Serializable? = this.extra,
): RasterLayerState =
RasterLayerState(
source = source,
opacity = opacity,
visible = visible,
id = id,
extra = extra,
)

fun fingerPrint(): RasterLayerFingerPrint =
RasterLayerFingerPrint(
id = id.hashCode(),
source = source.hashCode(),
opacity = opacity.hashCode(),
visible = visible.hashCode(),
extra = extra?.hashCode() ?: 0,
)

fun asFlow(): Flow<RasterLayerFingerPrint> =
snapshotFlow { fingerPrint() }
.distinctUntilChanged()
}

data class RasterLayerFingerPrint(
val id: Int,
val source: Int,
val opacity: Int,
val visible: Int,
val extra: Int,
)

data class RasterLayerEvent(
val state: RasterLayerState,
)

typealias OnRasterLayerEventHandler = (RasterLayerEvent) -> Unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mapconductor.core.raster

interface RasterLayerCapable {
suspend fun compositionRasterLayers(data: List<RasterLayerState>)

suspend fun updateRasterLayer(state: RasterLayerState)

fun hasRasterLayer(state: RasterLayerState): Boolean
}
Loading
Loading