From 3416ae27587d67d42caf16e77f6c0dabc8be61dd Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Tue, 24 May 2022 17:39:46 +0200 Subject: [PATCH] webmap: try improving leaflet map, create separate layer for player icons, tidy up redux state store stuff --- .../dev/adamko/kafkatorio/webmap/App.kt | 10 +- .../dev/adamko/kafkatorio/webmap/map.kt | 88 ++++--- .../dev/adamko/kafkatorio/webmap/state.kt | 234 ++++++++++-------- 3 files changed, 187 insertions(+), 145 deletions(-) diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt index 889868f..dc0d26f 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt @@ -9,6 +9,7 @@ import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.html.div import io.kvision.maps.Maps +import io.kvision.maps.Maps.Companion.L import io.kvision.maps.externals.leaflet.layer.tile.GridLayer import io.kvision.module import io.kvision.panel.root @@ -23,7 +24,7 @@ import org.w3c.dom.asList class App( reduxStore: ReduxStore, - wsService: WebsocketService + wsService: WebsocketService, ) : Application() { private var appState: MutableMap = mutableMapOf() @@ -37,7 +38,7 @@ class App( } private val kvMaps: Maps - get() = reduxStore.getState().kvMaps + get() = reduxStore.getState().map.kvMap override fun start(state: Map) { this.appState = state.toMutableMap() @@ -54,6 +55,7 @@ class App( it.redraw() } }) + // this.asDynamic()._fadeAnimated = false } @@ -85,9 +87,7 @@ class App( } } - override fun dispose(): Map { - return appState - } + override fun dispose(): Map = appState } diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt index c614b94..93de604 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt @@ -3,36 +3,70 @@ package dev.adamko.kafkatorio.webmap import io.kvision.maps.Maps import io.kvision.maps.Maps.Companion.L 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.LatLng +import io.kvision.maps.externals.leaflet.layer.LayerGroup import io.kvision.maps.externals.leaflet.layer.tile.TileLayer +import io.kvision.utils.obj import io.kvision.utils.px 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 { - width = 800.px - height = 800.px - margin = 10.px + val kvMap: Maps = Maps { + width = 800.px + height = 800.px + margin = 10.px + } - configureLeafletMap { - setView(center = LatLng(0, 0), zoom = 0) + val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer() - val baseTileLayer: TileLayer = L.tileLayer( + val playerIconsLayer: LayerGroup = LayerGroup() + + init { + val baseLayers: Control.LayersObject = obj {} + baseLayers["Terrain"] = factorioTerrainLayer + + val overlays: Control.LayersObject = obj {} + overlays["Players"] = playerIconsLayer + + val layersControl = Layers( + baseLayers = baseLayers, + overlays = overlays, + options = obj { + 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 { + + val baseTileLayer = L.tileLayer( tileUrlTemplate ) { attribution = "kafkatorio" @@ -60,18 +94,6 @@ fun createFactorioMap() = Maps { } } - baseTileLayer.addTo(this) - - L.scale { - metric = true - imperial = false - }.addTo(this) - - - options.crs = CRS.Simple - options.zoomSnap = 0 - options.zoomDelta = 0.1 - -// options.fadeAnimation = false + return baseTileLayer } } diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/state.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/state.kt index 9ff75ab..52f4191 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/state.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/state.kt @@ -7,9 +7,9 @@ import dev.adamko.kafkatorio.schema.common.SurfaceIndex import dev.adamko.kafkatorio.schema.common.Tick import dev.adamko.kafkatorio.schema.common.toHexString import dev.adamko.kafkatorio.schema.packets.PlayerUpdate -import io.kvision.maps.Maps import io.kvision.maps.Maps.Companion.L 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.vector.CircleMarker import io.kvision.redux.ReduxStore @@ -22,16 +22,26 @@ fun createFactorioReduxStore(): ReduxStore = createReduxStore( ::factorioGameReducer, FactorioGameState( - kvMaps = createFactorioMap(), - players = emptyMap() + map = FactorioMap(), + players = emptyMap(), ) ) data class FactorioGameState( - val kvMaps: Maps, + val map: FactorioMap, val players: Map, -) +) { + + val playerIconsLayer: LayerGroup + get() = map.playerIconsLayer + +// init { +// kvMaps.leafletMap { +// playerIconsLayer.addTo(this) +// } +// } +} 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) { - is FactorioUpdate.Player -> { - - 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) - } + is FactorioUpdate.Player -> handlePlayerUpdate(gameState, update) } -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, - 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 - private set - - fun update(tick: Tick, update: PlayerUpdate) { + fun update(tick: Tick, update: PlayerUpdate): PlayerState { if (lastTick.value > tick.value) { println("discarding old PlayerUpdate, ${lastTick.value} > ${tick.value}") - return + return this } - this.lastTick = tick - this.playerDetails = updatePlayerDetails(update) - updateMapPosition() - - println("updated PlayerState index:${playerDetails.index}, name:${playerDetails.name}") - } - - private fun updatePlayerDetails(update: PlayerUpdate): PlayerDetailsState { - - 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, - ) - } + val newState = this.copy( + lastTick = tick, + playerDetails = playerDetails.update(update), + mapMarker = updateMapMarker() + ) + println("updated PlayerState index:${newState.index}, name:${newState.playerDetails.name}") + return newState } - private fun updateMapPosition() { + private fun updateMapMarker(): CircleMarker? { + return when (playerDetails) { - when (val details = playerDetails) { - is PlayerDetailsState.Unknown -> { - println("removing player ${details.index} map marker") - mapMarker?.remove() + is PlayerProperties.Partial -> { + println("removing player $index map marker") + null } - 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) { - mapMarker = L.circleMarker(playerDetails.latLng) { + companion object { + private fun createMapMarker(playerDetails: PlayerProperties.Complete): CircleMarker = + L.circleMarker(playerDetails.latLng) { fillOpacity = 1f radius = 10 className = "player-marker" @@ -148,54 +144,78 @@ class PlayerState( // 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 { - - val index: PlayerIndex +sealed interface PlayerProperties { val position: MapEntityPosition? val surfaceIndex: SurfaceIndex? val colour: Colour? val name: String? 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 surfaceIndex: SurfaceIndex? = null, override val colour: Colour? = null, override val name: String? = null, - ) : PlayerDetailsState { + override val isConnected: Boolean = false, + override val isShowOnMap: Boolean = false, + ) : PlayerProperties { override val latLng: LatLng? = null } - data class Known( - override val index: PlayerIndex, + data class Complete( override val position: MapEntityPosition, override val surfaceIndex: SurfaceIndex, override val colour: Colour, override val name: String, - ) : PlayerDetailsState { + override val isConnected: Boolean = false, + override val isShowOnMap: Boolean = false, + ) : PlayerProperties { 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, + ) + } + } }