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.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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue