webmap: try improving leaflet map, create separate layer for player icons, tidy up redux state store stuff
This commit is contained in:
parent
540d1c9ed3
commit
3416ae2758
3 changed files with 187 additions and 145 deletions
|
|
@ -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<FactorioGameState, FactorioUpdate>,
|
||||
wsService: WebsocketService
|
||||
wsService: WebsocketService,
|
||||
) : Application() {
|
||||
private var appState: MutableMap<String, Any> = 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<String, Any>) {
|
||||
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<String, Any> {
|
||||
return appState
|
||||
}
|
||||
override fun dispose(): Map<String, Any> = appState
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FactorioGameState, FactorioUpdate> =
|
|||
createReduxStore(
|
||||
::factorioGameReducer,
|
||||
FactorioGameState(
|
||||
kvMaps = createFactorioMap(),
|
||||
players = emptyMap()
|
||||
map = FactorioMap(),
|
||||
players = emptyMap(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
data class FactorioGameState(
|
||||
val kvMaps: Maps,
|
||||
val map: FactorioMap,
|
||||
val players: Map<PlayerIndex, PlayerState>,
|
||||
)
|
||||
) {
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue