webmap: try improving leaflet map, create separate layer for player icons, tidy up redux state store stuff

This commit is contained in:
Adam 2022-05-24 17:39:46 +02:00
parent 540d1c9ed3
commit 3416ae2758
3 changed files with 187 additions and 145 deletions

View file

@ -9,6 +9,7 @@ import io.kvision.CoreModule
import io.kvision.FontAwesomeModule import io.kvision.FontAwesomeModule
import io.kvision.html.div import io.kvision.html.div
import io.kvision.maps.Maps import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.layer.tile.GridLayer import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
import io.kvision.module import io.kvision.module
import io.kvision.panel.root import io.kvision.panel.root
@ -23,7 +24,7 @@ import org.w3c.dom.asList
class App( class App(
reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>, reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
wsService: WebsocketService wsService: WebsocketService,
) : Application() { ) : Application() {
private var appState: MutableMap<String, Any> = mutableMapOf() private var appState: MutableMap<String, Any> = mutableMapOf()
@ -37,7 +38,7 @@ class App(
} }
private val kvMaps: Maps private val kvMaps: Maps
get() = reduxStore.getState().kvMaps get() = reduxStore.getState().map.kvMap
override fun start(state: Map<String, Any>) { override fun start(state: Map<String, Any>) {
this.appState = state.toMutableMap() this.appState = state.toMutableMap()
@ -54,6 +55,7 @@ class App(
it.redraw() it.redraw()
} }
}) })
// this.asDynamic()._fadeAnimated = false // this.asDynamic()._fadeAnimated = false
} }
@ -85,9 +87,7 @@ class App(
} }
} }
override fun dispose(): Map<String, Any> { override fun dispose(): Map<String, Any> = appState
return appState
}
} }

View file

@ -3,36 +3,70 @@ package dev.adamko.kafkatorio.webmap
import io.kvision.maps.Maps import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.DoneCallback import io.kvision.maps.externals.leaflet.DoneCallback
import io.kvision.maps.externals.leaflet.control.Control
import io.kvision.maps.externals.leaflet.control.Layers
import io.kvision.maps.externals.leaflet.control.set
import io.kvision.maps.externals.leaflet.geo.CRS import io.kvision.maps.externals.leaflet.geo.CRS
import io.kvision.maps.externals.leaflet.geo.LatLng import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.externals.leaflet.layer.LayerGroup
import io.kvision.maps.externals.leaflet.layer.tile.TileLayer import io.kvision.maps.externals.leaflet.layer.tile.TileLayer
import io.kvision.utils.obj
import io.kvision.utils.px import io.kvision.utils.px
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
//
//val polyline = Maps.L.polyline(
// listOf(
// LatLng(55, 2),
// LatLng(65, 2),
// LatLng(65, 20),
// LatLng(55, 20),
// LatLng(55, 2),
// )
//) {
// noClip = true
//}
private const val tileUrlTemplate = """http://localhost:9073/tiles/s1/z{z}/x{x}/y{y}.png""" class FactorioMap(
private val tileUrlTemplate: String = """http://localhost:9073/tiles/s1/z{z}/x{x}/y{y}.png""",
) {
fun createFactorioMap() = Maps { val kvMap: Maps = Maps {
width = 800.px width = 800.px
height = 800.px height = 800.px
margin = 10.px margin = 10.px
}
configureLeafletMap { val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer()
setView(center = LatLng(0, 0), zoom = 0)
val baseTileLayer: TileLayer<TileLayer.TileLayerOptions> = L.tileLayer( val playerIconsLayer: LayerGroup = LayerGroup()
init {
val baseLayers: Control.LayersObject = obj<Control.LayersObject> {}
baseLayers["Terrain"] = factorioTerrainLayer
val overlays: Control.LayersObject = obj<Control.LayersObject> {}
overlays["Players"] = playerIconsLayer
val layersControl = Layers(
baseLayers = baseLayers,
overlays = overlays,
options = obj<Layers.LayersOptions> {
sortLayers = true
}
)
kvMap.configureLeafletMap {
setView(center = LatLng(0, 0), zoom = 0)
L.scale {
metric = true
imperial = false
}.addTo(this)
options.crs = CRS.Simple
options.zoomSnap = 0
options.zoomDelta = 0.1
// options.fadeAnimation = false
addControl(layersControl)
factorioTerrainLayer.addTo(this)
playerIconsLayer.addTo(this)
}
}
private fun buildFactorioTerrainLayer(): TileLayer<TileLayer.TileLayerOptions> {
val baseTileLayer = L.tileLayer(
tileUrlTemplate tileUrlTemplate
) { ) {
attribution = "kafkatorio" attribution = "kafkatorio"
@ -60,18 +94,6 @@ fun createFactorioMap() = Maps {
} }
} }
baseTileLayer.addTo(this) return baseTileLayer
L.scale {
metric = true
imperial = false
}.addTo(this)
options.crs = CRS.Simple
options.zoomSnap = 0
options.zoomDelta = 0.1
// options.fadeAnimation = false
} }
} }

View file

@ -7,9 +7,9 @@ import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.Tick import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.common.toHexString import dev.adamko.kafkatorio.schema.common.toHexString
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.geo.LatLng import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.externals.leaflet.layer.LayerGroup
import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
import io.kvision.redux.ReduxStore import io.kvision.redux.ReduxStore
@ -22,16 +22,26 @@ fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
createReduxStore( createReduxStore(
::factorioGameReducer, ::factorioGameReducer,
FactorioGameState( FactorioGameState(
kvMaps = createFactorioMap(), map = FactorioMap(),
players = emptyMap() players = emptyMap(),
) )
) )
data class FactorioGameState( data class FactorioGameState(
val kvMaps: Maps, val map: FactorioMap,
val players: Map<PlayerIndex, PlayerState>, val players: Map<PlayerIndex, PlayerState>,
) ) {
val playerIconsLayer: LayerGroup
get() = map.playerIconsLayer
// init {
// kvMaps.leafletMap {
// playerIconsLayer.addTo(this)
// }
// }
}
sealed class FactorioUpdate : RAction { sealed class FactorioUpdate : RAction {
@ -39,106 +49,92 @@ sealed class FactorioUpdate : RAction {
} }
fun factorioGameReducer(state: FactorioGameState, update: FactorioUpdate): FactorioGameState = fun factorioGameReducer(gameState: FactorioGameState, update: FactorioUpdate): FactorioGameState =
when (update) { when (update) {
is FactorioUpdate.Player -> { is FactorioUpdate.Player -> handlePlayerUpdate(gameState, update)
val playerState = state.players.getOrElse(update.data.key.index) {
PlayerState(update.data.key.index, update.tick)
}
playerState.update(update.tick, update.data)
state.kvMaps.leafletMap {
val playerMarker = playerState.mapMarker
if (playerMarker != null) {
when (playerState.playerDetails) {
is PlayerDetailsState.Known -> if (!hasLayer(playerMarker)) addLayer(playerMarker)
is PlayerDetailsState.Unknown -> if (hasLayer(playerMarker)) removeLayer(playerMarker)
}
}
}
val updatedPlayersState = state.players + (playerState.index to playerState)
state.copy(players = updatedPlayersState)
}
} }
class PlayerState( private fun handlePlayerUpdate(
gameState: FactorioGameState,
update: FactorioUpdate.Player,
): FactorioGameState {
val previous = gameState.players[update.data.key.index]
?: PlayerState(update.data.key.index, update.tick)
val updated = previous.update(update.tick, update.data)
if (previous.mapMarker != updated.mapMarker) {
if (previous.mapMarker != null) {
println("removing player icon ${previous.index}")
gameState.playerIconsLayer.removeLayer(previous.mapMarker)
}
if (updated.mapMarker != null) {
println("adding player icon ${updated.index}")
gameState.playerIconsLayer.addLayer(updated.mapMarker)
}
}
val updatedPlayersState = gameState.players + (previous.index to updated)
return gameState.copy(players = updatedPlayersState)
}
data class PlayerState(
val index: PlayerIndex, val index: PlayerIndex,
private var lastTick: Tick, val lastTick: Tick,
val playerDetails: PlayerProperties = PlayerProperties.Partial(),
val mapMarker: CircleMarker? = null,
) { ) {
var playerDetails: PlayerDetailsState = PlayerDetailsState.Unknown(index)
private set
var mapMarker: CircleMarker? = null fun update(tick: Tick, update: PlayerUpdate): PlayerState {
private set
fun update(tick: Tick, update: PlayerUpdate) {
if (lastTick.value > tick.value) { if (lastTick.value > tick.value) {
println("discarding old PlayerUpdate, ${lastTick.value} > ${tick.value}") println("discarding old PlayerUpdate, ${lastTick.value} > ${tick.value}")
return return this
} }
this.lastTick = tick
this.playerDetails = updatePlayerDetails(update)
updateMapPosition() val newState = this.copy(
lastTick = tick,
println("updated PlayerState index:${playerDetails.index}, name:${playerDetails.name}") playerDetails = playerDetails.update(update),
} mapMarker = updateMapMarker()
)
private fun updatePlayerDetails(update: PlayerUpdate): PlayerDetailsState { println("updated PlayerState index:${newState.index}, name:${newState.playerDetails.name}")
return newState
val position = update.position ?: playerDetails.position
val surfaceIndex = update.surfaceIndex ?: playerDetails.surfaceIndex
val colour = update.colour ?: playerDetails.colour
val name = update.name ?: playerDetails.name
return when {
position != null
&& surfaceIndex != null
&& colour != null
&& name != null ->
PlayerDetailsState.Known(
index = playerDetails.index,
position = position,
surfaceIndex = surfaceIndex,
colour = colour,
name = name,
)
else ->
PlayerDetailsState.Unknown(
index = playerDetails.index,
position = position,
surfaceIndex = surfaceIndex,
colour = colour,
name = name,
)
}
} }
private fun updateMapPosition() { private fun updateMapMarker(): CircleMarker? {
return when (playerDetails) {
when (val details = playerDetails) { is PlayerProperties.Partial -> {
is PlayerDetailsState.Unknown -> { println("removing player $index map marker")
println("removing player ${details.index} map marker") null
mapMarker?.remove()
} }
is PlayerDetailsState.Known -> updateMapMarker(details)
}
is PlayerProperties.Complete -> {
val mapMarker = mapMarker ?: createMapMarker(playerDetails)
mapMarker.apply {
options.color = playerDetails.hexColour
options.fillColor = playerDetails.hexColour
val lat = playerDetails.latLng.lat.toInt()
val lng = playerDetails.latLng.lng.toInt()
setTooltipContent("${playerDetails.name} [$lat, $lng]")
setLatLng(playerDetails.latLng)
}
}
}
} }
private fun updateMapMarker(playerDetails: PlayerDetailsState.Known) {
if (mapMarker == null) { companion object {
mapMarker = L.circleMarker(playerDetails.latLng) { private fun createMapMarker(playerDetails: PlayerProperties.Complete): CircleMarker =
L.circleMarker(playerDetails.latLng) {
fillOpacity = 1f fillOpacity = 1f
radius = 10 radius = 10
className = "player-marker" className = "player-marker"
@ -148,54 +144,78 @@ class PlayerState(
// interactive = false // interactive = false
}) })
} }
}
mapMarker?.apply {
val playerColour = playerDetails.colour.toHexString(false)
options.color = playerColour
options.fillColor = playerColour
val lat = playerDetails.latLng.lat.toInt()
val lng = playerDetails.latLng.lng.toInt()
setTooltipContent("${playerDetails.name} [$lat, $lng]")
setLatLng(playerDetails.latLng)
}
} }
} }
sealed interface PlayerDetailsState { sealed interface PlayerProperties {
val index: PlayerIndex
val position: MapEntityPosition? val position: MapEntityPosition?
val surfaceIndex: SurfaceIndex? val surfaceIndex: SurfaceIndex?
val colour: Colour? val colour: Colour?
val name: String? val name: String?
val latLng: LatLng? val latLng: LatLng?
val isConnected: Boolean
val isShowOnMap: Boolean
data class Unknown(
override val index: PlayerIndex,
data class Partial(
override val position: MapEntityPosition? = null, override val position: MapEntityPosition? = null,
override val surfaceIndex: SurfaceIndex? = null, override val surfaceIndex: SurfaceIndex? = null,
override val colour: Colour? = null, override val colour: Colour? = null,
override val name: String? = null, override val name: String? = null,
) : PlayerDetailsState { override val isConnected: Boolean = false,
override val isShowOnMap: Boolean = false,
) : PlayerProperties {
override val latLng: LatLng? = null override val latLng: LatLng? = null
} }
data class Known(
override val index: PlayerIndex,
data class Complete(
override val position: MapEntityPosition, override val position: MapEntityPosition,
override val surfaceIndex: SurfaceIndex, override val surfaceIndex: SurfaceIndex,
override val colour: Colour, override val colour: Colour,
override val name: String, override val name: String,
) : PlayerDetailsState { override val isConnected: Boolean = false,
override val isShowOnMap: Boolean = false,
) : PlayerProperties {
override val latLng: LatLng = position.latLng() override val latLng: LatLng = position.latLng()
val hexColour: String = colour.toHexString(false)
} }
fun update(update: PlayerUpdate): PlayerProperties {
val position = update.position ?: this.position
val surfaceIndex = update.surfaceIndex ?: this.surfaceIndex
val colour = update.colour ?: this.colour
val name = update.name ?: this.name
val isConnected = update.isConnected ?: false
val isShowOnMap = update.isShowOnMap ?: false
return when {
position != null
&& surfaceIndex != null
&& colour != null
&& name != null ->
Complete(
position = position,
surfaceIndex = surfaceIndex,
colour = colour,
name = name,
isConnected = isConnected,
isShowOnMap = isShowOnMap,
)
else ->
Partial(
position = position,
surfaceIndex = surfaceIndex,
colour = colour,
name = name,
isConnected = isConnected,
isShowOnMap = isShowOnMap,
)
}
}
} }