webmap
- tidy up player layer - no need for smaller web tiles, make the large ones render as pixelated - make tile server and websocket server URL not-hardcoded - tidy up dynamic tile refreshing
This commit is contained in:
parent
e9a6f99fb8
commit
f151027464
9 changed files with 312 additions and 135 deletions
|
|
@ -38,8 +38,7 @@ class App(
|
|||
wsService: WebsocketService,
|
||||
) : Application(), CoroutineScope {
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
CoroutineName("WebMapApp") + Job(rootJob)
|
||||
override val coroutineContext: CoroutineContext = CoroutineName("WebMapApp")
|
||||
|
||||
private var appState: MutableMap<String, Any> = mutableMapOf()
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ class App(
|
|||
override fun start(state: Map<String, Any>) {
|
||||
this.appState = state.toMutableMap()
|
||||
|
||||
val root = root("kvapp") {
|
||||
root("kvapp") {
|
||||
div("Kafkatorio Web Map")
|
||||
add(kvMaps)
|
||||
}
|
||||
|
|
@ -74,18 +73,16 @@ class App(
|
|||
it.redraw()
|
||||
}
|
||||
})
|
||||
|
||||
// this.asDynamic()._fadeAnimated = false
|
||||
}
|
||||
|
||||
val doc = root.getElement()?.ownerDocument
|
||||
val window = doc!!.defaultView!!
|
||||
|
||||
wsService.packetsFlow
|
||||
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
|
||||
.onEach {
|
||||
println("[packetsFlow] triggering tile refresh ${it.filename.value}")
|
||||
gameState.map.refreshUpdatedTilePng(doc, window, it.filename)
|
||||
gameState.map.refreshUpdatedTilePng(
|
||||
// doc, window,
|
||||
it.filename
|
||||
)
|
||||
}.launchIn(this)
|
||||
|
||||
}
|
||||
|
|
@ -104,8 +101,8 @@ fun main() {
|
|||
{
|
||||
val reduxStore = createFactorioReduxStore()
|
||||
val wsService = WebsocketService(
|
||||
"ws://localhost:9073/ws",
|
||||
reduxStore,
|
||||
// "ws://localhost:9073/ws", // TODO load from config
|
||||
reduxStore = reduxStore,
|
||||
)
|
||||
App(
|
||||
reduxStore = reduxStore,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
object Props {
|
||||
@Suppress("HttpUrlsUsage")
|
||||
val websocketServerUrl: String
|
||||
get() = window.location.href
|
||||
.replace("http://", "ws://")
|
||||
.replace("https://", "wss://") +
|
||||
"ws/foo"
|
||||
}
|
||||
33
modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/externals/leaflet.kt
vendored
Normal file
33
modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/externals/leaflet.kt
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package dev.adamko.kafkatorio.webmap.externals
|
||||
|
||||
import io.kvision.maps.externals.leaflet.DoneCallback
|
||||
import io.kvision.maps.externals.leaflet.geo.LatLng
|
||||
import io.kvision.maps.externals.leaflet.layer.tile.TileLayer
|
||||
import io.kvision.maps.externals.leaflet.layer.vector.Polyline
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
|
||||
inline var TileLayer<TileLayer.TileLayerOptions>.tileOnLoad: TileOnLoadFn?
|
||||
get() = asDynamic()._tileOnLoad as TileOnLoadFn
|
||||
set(value) {
|
||||
asDynamic()._tileOnLoad = value
|
||||
}
|
||||
|
||||
|
||||
typealias TileOnLoadFn = (done: DoneCallback, tile: HTMLElement) -> Unit
|
||||
|
||||
|
||||
sealed interface PolylinePathPoints {
|
||||
value class D1(val points: Array<LatLng>) : PolylinePathPoints
|
||||
value class D2(val points: Array<Array<LatLng>>) : PolylinePathPoints
|
||||
value class D3(val points: Array<Array<Array<LatLng>>>) : PolylinePathPoints
|
||||
}
|
||||
|
||||
inline fun Polyline<*>.getLatLngs2(): PolylinePathPoints? {
|
||||
return when (val points = this.getLatLngs()) {
|
||||
is Array<LatLng> -> PolylinePathPoints.D1(points as Array<LatLng>)
|
||||
is Array<Array<LatLng>> -> PolylinePathPoints.D2(points as Array<Array<LatLng>>)
|
||||
is Array<Array<Array<LatLng>>> -> PolylinePathPoints.D3(points as Array<Array<Array<LatLng>>>)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.TilePngFilename
|
||||
import dev.adamko.kafkatorio.webmap.externals.TileOnLoadFn
|
||||
import dev.adamko.kafkatorio.webmap.externals.tileOnLoad
|
||||
import io.kvision.maps.Maps
|
||||
import io.kvision.maps.Maps.Companion.L
|
||||
import io.kvision.maps.externals.leaflet.DoneCallback
|
||||
|
|
@ -11,15 +13,15 @@ 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 kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.Document
|
||||
import kotlinx.js.jso
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.Image
|
||||
import org.w3c.dom.Window
|
||||
import org.w3c.dom.asList
|
||||
import org.w3c.fetch.NO_CORS
|
||||
import org.w3c.fetch.RELOAD
|
||||
|
|
@ -29,7 +31,7 @@ import org.w3c.fetch.RequestMode
|
|||
|
||||
|
||||
class FactorioMap(
|
||||
private val tileUrlTemplate: String = """http://localhost:9073/tiles/s1/z{z}/x{x}/y{y}.png""",
|
||||
private val tileUrlTemplate: String = """/tiles/s1/z{z}/x{x}/y{y}.png""",
|
||||
) {
|
||||
|
||||
val kvMap: Maps = Maps {
|
||||
|
|
@ -38,21 +40,24 @@ class FactorioMap(
|
|||
margin = 10.px
|
||||
}
|
||||
|
||||
val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer()
|
||||
private val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer()
|
||||
|
||||
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 baseLayers: Control.LayersObject = jso {
|
||||
set("Terrain", factorioTerrainLayer)
|
||||
}
|
||||
|
||||
val overlays: Control.LayersObject = jso {
|
||||
set("Players", playerIconsLayer)
|
||||
}
|
||||
|
||||
val layersControl = Layers(
|
||||
baseLayers = baseLayers,
|
||||
overlays = overlays,
|
||||
options = obj<Layers.LayersOptions> {
|
||||
options = jso {
|
||||
sortLayers = true
|
||||
}
|
||||
)
|
||||
|
|
@ -78,21 +83,19 @@ class FactorioMap(
|
|||
|
||||
|
||||
suspend fun refreshUpdatedTilePng(
|
||||
document: Document,
|
||||
window: Window,
|
||||
tilePngFilename: TilePngFilename,
|
||||
): Unit = coroutineScope {
|
||||
launch {
|
||||
document
|
||||
.querySelectorAll("img.leaflet-tile-loaded")
|
||||
.asList()
|
||||
.filterIsInstance<Image>()
|
||||
.filter { img ->
|
||||
tilePngFilename.value in img.src
|
||||
}
|
||||
.forEach { img ->
|
||||
val imgSrc = img.src //.substringBeforeLast('?')
|
||||
document
|
||||
.querySelectorAll("img.leaflet-tile-loaded")
|
||||
.asList()
|
||||
.filterIsInstance<Image>()
|
||||
.filter { img ->
|
||||
tilePngFilename.value in img.src
|
||||
}
|
||||
.forEach { img ->
|
||||
val imgSrc = img.src
|
||||
|
||||
launch {
|
||||
window.fetch(
|
||||
imgSrc,
|
||||
RequestInit(
|
||||
|
|
@ -100,24 +103,14 @@ class FactorioMap(
|
|||
mode = RequestMode.NO_CORS,
|
||||
)
|
||||
).then {
|
||||
println("[refreshUpdatedTilePng] updating $imgSrc")
|
||||
img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
|
||||
img.src = imgSrc
|
||||
window.requestAnimationFrame {
|
||||
println("[refreshUpdatedTilePng] updating $imgSrc")
|
||||
img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
|
||||
img.src = imgSrc
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
}
|
||||
|
||||
//// println("fetching $imgSrc")
|
||||
// val newImg = Image()
|
||||
// newImg.onload = {
|
||||
//// window.requestAnimationFrame {
|
||||
//// println("image loaded ${newImg!!.src}")
|
||||
// img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
|
||||
// img.src = newImg.src
|
||||
// Unit
|
||||
//// }
|
||||
// }
|
||||
// newImg.src = imgSrc //+ "?t=${currentTimeMillis()}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFactorioTerrainLayer(): TileLayer<TileLayer.TileLayerOptions> {
|
||||
|
|
@ -132,7 +125,7 @@ class FactorioMap(
|
|||
minZoom = -2
|
||||
maxZoom = 6
|
||||
|
||||
maxNativeZoom = 3
|
||||
maxNativeZoom = -1
|
||||
minNativeZoom = -1
|
||||
|
||||
noWrap = true
|
||||
|
|
@ -140,13 +133,14 @@ class FactorioMap(
|
|||
updateWhenZooming = true
|
||||
}
|
||||
|
||||
val currentTileOnLoad: (DoneCallback, HTMLElement) -> Unit =
|
||||
baseTileLayer.asDynamic()._tileOnLoad as (DoneCallback, HTMLElement) -> Unit
|
||||
baseTileLayer.asDynamic()._tileOnLoad = { done: DoneCallback, tile: HTMLElement ->
|
||||
if (tile.hasAttribute(DYNAMIC_RELOAD_ATT)) {
|
||||
val currentTileOnLoad: TileOnLoadFn? = baseTileLayer.tileOnLoad
|
||||
|
||||
if (currentTileOnLoad != null) {
|
||||
baseTileLayer.tileOnLoad = { done: DoneCallback, tile: HTMLElement ->
|
||||
if (!tile.hasAttribute(DYNAMIC_RELOAD_ATT)) {
|
||||
currentTileOnLoad(done, tile)
|
||||
}
|
||||
tile.removeAttribute(DYNAMIC_RELOAD_ATT)
|
||||
} else {
|
||||
currentTileOnLoad(done, tile)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
|||
import dev.adamko.kafkatorio.webmap.latLng
|
||||
import io.kvision.maps.Maps
|
||||
import io.kvision.maps.externals.leaflet.geo.LatLng
|
||||
import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip
|
||||
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
|
||||
import io.kvision.utils.obj
|
||||
import kotlinx.js.jso
|
||||
|
||||
|
||||
fun handlePlayerUpdate(
|
||||
|
|
@ -100,7 +99,7 @@ data class PlayerState(
|
|||
className = "player-marker"
|
||||
// interactive = false
|
||||
}.also { circle ->
|
||||
circle.bindTooltip("", obj<Tooltip.TooltipOptions> {
|
||||
circle.bindTooltip("", jso {
|
||||
// interactive = false
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.WebSocket
|
||||
import org.w3c.dom.events.Event
|
||||
|
||||
|
||||
class WebsocketService(
|
||||
wsUrl: String,
|
||||
wsUrl: String = Props.websocketServerUrl,
|
||||
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
|
||||
) : CoroutineScope {
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ class WebsocketService(
|
|||
CoroutineName("WebsocketService") + Job(rootJob)
|
||||
|
||||
private val ws = WebSocket(wsUrl)
|
||||
// private val ws = WebSocket("ws://localhost:12080/ws/foo")
|
||||
|
||||
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
|
||||
val packetsFlow: SharedFlow<EventServerPacket>
|
||||
|
|
@ -40,6 +42,9 @@ class WebsocketService(
|
|||
|
||||
init {
|
||||
ws.onmessage = ::handleMessageEvent
|
||||
ws.onerror = ::handleError
|
||||
|
||||
println("[WebsocketService] init ${ws.url}, ${ws.protocol}")
|
||||
}
|
||||
|
||||
private fun handleMessageEvent(msg: MessageEvent) {
|
||||
|
|
@ -52,35 +57,46 @@ class WebsocketService(
|
|||
&& data.endsWith("}")
|
||||
) {
|
||||
|
||||
println(data.replace('\n', ' '))
|
||||
println("[WebsocketService.handleMessageEvent] ${data.replace('\n', ' ')}")
|
||||
|
||||
val packet = jsonMapper.decodeFromString(EventServerPacket.serializer(), data)
|
||||
|
||||
launch {
|
||||
_packetsFlow.emit(packet)
|
||||
val packet: EventServerPacket? = runCatching {
|
||||
jsonMapper.decodeFromString(EventServerPacket.serializer(), data)
|
||||
}.getOrElse {
|
||||
null
|
||||
}
|
||||
|
||||
when (packet) {
|
||||
is EventServerPacket.ChunkTileSaved -> {}
|
||||
if (packet != null) {
|
||||
|
||||
is EventServerPacket.Kafkatorio ->
|
||||
when (val packetData = packet.packet.data) {
|
||||
is PlayerUpdate -> reduxStore.dispatch(
|
||||
FactorioUpdate.Player(packet.packet.tick, packetData)
|
||||
)
|
||||
is ConfigurationUpdate,
|
||||
is ConsoleChatUpdate,
|
||||
is ConsoleCommandUpdate,
|
||||
is PrototypesUpdate,
|
||||
is SurfaceUpdate,
|
||||
is EntityUpdate,
|
||||
is MapChunkUpdate -> {
|
||||
// to be continued...
|
||||
launch { _packetsFlow.emit(packet) }
|
||||
|
||||
when (packet) {
|
||||
is EventServerPacket.ChunkTileSaved -> {}
|
||||
|
||||
is EventServerPacket.Kafkatorio ->
|
||||
when (val packetData = packet.packet.data) {
|
||||
is PlayerUpdate -> reduxStore.dispatch(
|
||||
FactorioUpdate.Player(packet.packet.tick, packetData)
|
||||
)
|
||||
is ConfigurationUpdate,
|
||||
is ConsoleChatUpdate,
|
||||
is ConsoleCommandUpdate,
|
||||
is PrototypesUpdate,
|
||||
is SurfaceUpdate,
|
||||
is EntityUpdate,
|
||||
is MapChunkUpdate -> {
|
||||
// to be continued...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("[WebsocketService.handleMessageEvent] unknown json message ${msg.data}")
|
||||
}
|
||||
} else {
|
||||
println(data)
|
||||
println("[WebsocketService.handleMessageEvent] non-json message ${msg.data}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: Event) {
|
||||
println("ws error: ${JSON.stringify(error)}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/* https://github.com/Leaflet/Leaflet/issues/3575 */
|
||||
.leaflet-tile {
|
||||
width: 257px !important;
|
||||
height: 257px !important;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
/* https://github.com/Leaflet/Leaflet/issues/3575 */
|
||||
/*width: 257px !important;*/
|
||||
/*height: 257px !important;*/
|
||||
/*box-shadow: 0 0 1px rgba(0, 0, 0, 0.05);*/
|
||||
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue