it is literally working
This commit is contained in:
parent
841f07c847
commit
a0c330384a
17 changed files with 499 additions and 63 deletions
|
|
@ -11,6 +11,13 @@ interface FactorioEvent<T extends FactorioObjectData> {
|
|||
tick: uint;
|
||||
}
|
||||
|
||||
interface Colour {
|
||||
alpha: float;
|
||||
blue: float;
|
||||
green: float;
|
||||
red: float;
|
||||
}
|
||||
|
||||
interface PositionData {
|
||||
x: double;
|
||||
y: double;
|
||||
|
|
@ -19,6 +26,9 @@ interface PositionData {
|
|||
interface PlayerData extends FactorioObjectData {
|
||||
associated_characters_unit_numbers: uint[];
|
||||
character_unit_number: uint | null;
|
||||
chat_colour: Colour;
|
||||
colour: Colour;
|
||||
last_online: uint;
|
||||
name: string;
|
||||
object_name: string;
|
||||
position: PositionData;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ export namespace Serdes {
|
|||
character_unit_number: player.character?.unit_number ?? null,
|
||||
associated_characters_unit_numbers: charIds,
|
||||
position: positionTableToTable(player.position),
|
||||
colour: mapColour(player.color),
|
||||
chat_colour: mapColour(player.chat_color),
|
||||
last_online: player.last_online
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,4 +65,14 @@ export namespace Serdes {
|
|||
}
|
||||
}
|
||||
|
||||
export function mapColour(color: Color): Colour {
|
||||
color = (color as ColorTable)
|
||||
return {
|
||||
red: color.r ?? 0,
|
||||
green: color.g ?? 0,
|
||||
blue: color.b ?? 0,
|
||||
alpha: color.a ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ kotlin {
|
|||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(project.dependencies.enforcedPlatform(libs.kotlin.bom))
|
||||
implementation(project.dependencies.platform(libs.kotlin.bom))
|
||||
|
||||
implementation(project.dependencies.enforcedPlatform(libs.kotlinx.serialization.bom))
|
||||
implementation(project.dependencies.platform(libs.kotlinx.serialization.bom))
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ kotlin {
|
|||
|
||||
implementation(kotlin("test"))
|
||||
|
||||
implementation(project.dependencies.enforcedPlatform(libs.kotest.bom))
|
||||
implementation(project.dependencies.platform(libs.kotest.bom))
|
||||
implementation("io.kotest:kotest-assertions-core")
|
||||
implementation("io.kotest:kotest-property")
|
||||
implementation("io.kotest:kotest-assertions-json")
|
||||
|
|
|
|||
|
|
@ -11,13 +11,18 @@ import kotlinx.serialization.json.JsonClassDiscriminator
|
|||
@Serializable
|
||||
data class FactorioEvent<out T : FactorioObjectData>(
|
||||
/** the initial Factorio event ({defines.events}) trigger */
|
||||
@SerialName("event_type") val eventType: String,
|
||||
@SerialName("event_type")
|
||||
val eventType: String,
|
||||
/** Schema versioning */
|
||||
@SerialName("mod_version") val modVersion: String,
|
||||
@SerialName("factorio_version") val factorioVersion: String,
|
||||
@SerialName("mod_version")
|
||||
val modVersion: String,
|
||||
@SerialName("factorio_version")
|
||||
val factorioVersion: String,
|
||||
/** game time */
|
||||
@SerialName("tick") val tick: UInt,
|
||||
@SerialName("data") val data: T
|
||||
@SerialName("tick")
|
||||
val tick: UInt,
|
||||
@SerialName("data")
|
||||
val data: T
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -53,10 +58,20 @@ sealed class FactorioObjectData {
|
|||
@SerialName("LuaPlayer")
|
||||
data class PlayerData(
|
||||
@Serializable(with = ListAsObjectSerializer::class)
|
||||
@SerialName("associated_characters_unit_numbers") val associatedCharactersUnitNumbers: List<UInt>,
|
||||
@SerialName("character_unit_number") val characterUnitNumber: UInt?,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("position") val position: PositionData,
|
||||
@SerialName("associated_characters_unit_numbers")
|
||||
val associatedCharactersUnitNumbers: List<UInt>,
|
||||
@SerialName("character_unit_number")
|
||||
val characterUnitNumber: UInt?,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("position")
|
||||
val position: PositionData,
|
||||
@SerialName("colour")
|
||||
val colour: Colour,
|
||||
@SerialName("chat_colour")
|
||||
val chatColour: Colour,
|
||||
@SerialName("last_online")
|
||||
val lastOnline: UInt,
|
||||
) : FactorioObjectData() {
|
||||
@Transient
|
||||
override val objectName: String = "LuaPlayer"
|
||||
|
|
@ -65,16 +80,25 @@ data class PlayerData(
|
|||
@Serializable
|
||||
@SerialName("LuaEntity")
|
||||
data class EntityData(
|
||||
@SerialName("active") val active: Boolean,
|
||||
@SerialName("health") val health: Double?,
|
||||
@SerialName("health_ratio") val healthRatio: Double,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("position") val position: PositionData,
|
||||
@SerialName("surface_index") val surfaceIndex: Int,
|
||||
@SerialName("type") val type: String,
|
||||
@SerialName("unit_number") val unitNumber: UInt? = null,
|
||||
@SerialName("active")
|
||||
val active: Boolean,
|
||||
@SerialName("health")
|
||||
val health: Double?,
|
||||
@SerialName("health_ratio")
|
||||
val healthRatio: Double,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("position")
|
||||
val position: PositionData,
|
||||
@SerialName("surface_index")
|
||||
val surfaceIndex: Int,
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("unit_number")
|
||||
val unitNumber: UInt? = null,
|
||||
|
||||
@SerialName("player_index") val playerIndex: UInt? = null,
|
||||
@SerialName("player_index")
|
||||
val playerIndex: UInt? = null,
|
||||
) : FactorioObjectData() {
|
||||
@Transient
|
||||
override val objectName: String = "LuaEntity"
|
||||
|
|
@ -83,9 +107,12 @@ data class EntityData(
|
|||
@Serializable
|
||||
@SerialName("LuaSurface")
|
||||
data class SurfaceData(
|
||||
@SerialName("daytime") val daytime: Double,
|
||||
@SerialName("index") val index: UInt,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("daytime")
|
||||
val daytime: Double,
|
||||
@SerialName("index")
|
||||
val index: UInt,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
) : FactorioObjectData() {
|
||||
@Transient
|
||||
override val objectName: String = "LuaSurface"
|
||||
|
|
@ -93,6 +120,32 @@ data class SurfaceData(
|
|||
|
||||
@Serializable
|
||||
data class PositionData(
|
||||
@SerialName("x") val x: Double,
|
||||
@SerialName("y") val y: Double,
|
||||
val x: Double,
|
||||
val y: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
* Red, green, blue and alpha values, all in range `[0, 1]` or all in range `[0, 255]` if any
|
||||
* value is > 1.
|
||||
*
|
||||
* All values here are optional. Color channels default to 0, the alpha channel defaults to 1.
|
||||
*/
|
||||
@Serializable
|
||||
data class Colour(
|
||||
val red: Float = 0f,
|
||||
val green: Float = 0f,
|
||||
val blue: Float = 0f,
|
||||
val alpha: Float = 1f,
|
||||
) {
|
||||
/** True if any value is greater than 1, so the values are hexadecimal. */
|
||||
fun isDecimal(): Boolean {
|
||||
return red > 1f
|
||||
|| green > 1f
|
||||
|| blue > 1f
|
||||
|| alpha > 1f
|
||||
}
|
||||
|
||||
/** True if all values are between `[0..1]`. */
|
||||
fun isPercentage(): Boolean = !isDecimal()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ class FactorioEventTest : BehaviorSpec({
|
|||
"position": {
|
||||
"x": -3.5703125,
|
||||
"y": 29.75
|
||||
}
|
||||
},
|
||||
"last_online": 123
|
||||
},
|
||||
"event_type": "on_player_joined_game",
|
||||
"mod_version": "0.0.4",
|
||||
|
|
@ -41,7 +42,10 @@ class FactorioEventTest : BehaviorSpec({
|
|||
position = PositionData(
|
||||
x = -3.5703125,
|
||||
y = 29.75
|
||||
)
|
||||
),
|
||||
colour = Colour(),
|
||||
chatColour = Colour(),
|
||||
lastOnline = 123u
|
||||
),
|
||||
eventType = "on_player_joined_game",
|
||||
modVersion = "0.0.4",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
|
||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
|
||||
|
||||
plugins {
|
||||
id("dev.adamko.kafkatorio.lang.kotlin-js")
|
||||
kotlin("plugin.serialization")
|
||||
// kotlin("js") // version kotlinVersion
|
||||
id("io.kvision") version "5.6.1"
|
||||
}
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ kotlin {
|
|||
browser {
|
||||
runTask {
|
||||
outputFileName = "main.bundle.js"
|
||||
sourceMaps = false
|
||||
sourceMaps = true
|
||||
devServer = KotlinWebpackConfig.DevServer(
|
||||
open = false,
|
||||
port = 3000,
|
||||
|
|
@ -37,31 +37,54 @@ kotlin {
|
|||
}
|
||||
|
||||
sourceSets {
|
||||
val kvisionVersion: String = libs.versions.kvision.get()
|
||||
|
||||
all {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||
}
|
||||
|
||||
main {
|
||||
dependencies {
|
||||
|
||||
implementation(projects.modules.eventsSchema)
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-nodejs:0.0.7")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-html:0.7.3")
|
||||
|
||||
implementation("io.kvision:kvision:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-bootstrap:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-bootstrap-css:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-bootstrap-icons:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-fontawesome:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-state:$kvisionVersion")
|
||||
implementation("io.kvision:kvision-chart:$kvisionVersion")
|
||||
kvision("kvision")
|
||||
kvision("kvision-bootstrap")
|
||||
kvision("kvision-bootstrap-css")
|
||||
kvision("kvision-bootstrap-icons")
|
||||
kvision("kvision-fontawesome")
|
||||
kvision("kvision-state")
|
||||
kvision("kvision-chart")
|
||||
kvision("kvision-maps")
|
||||
kvision("kvision-redux")
|
||||
kvision("kvision-state-flow")
|
||||
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
}
|
||||
|
||||
val webDir = file("src/main/web")
|
||||
resources.srcDir(webDir)
|
||||
}
|
||||
|
||||
test {
|
||||
dependencies {
|
||||
implementation(kotlin("test-js"))
|
||||
implementation("io.kvision:kvision-testutils:$kvisionVersion")
|
||||
|
||||
kvision("kvision-testutils")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun KotlinDependencyHandler.kvision(
|
||||
module: String,
|
||||
version: Provider<String> = libs.versions.kvision,
|
||||
configure: ExternalModuleDependency.() -> Unit = {
|
||||
isChanging = true
|
||||
}
|
||||
) {
|
||||
implementation("io.kvision:$module:${version.get()}", configure)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,73 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import io.kvision.Application
|
||||
import io.kvision.CoreModule
|
||||
import io.kvision.BootstrapModule
|
||||
import io.kvision.BootstrapCssModule
|
||||
import io.kvision.BootstrapIconsModule
|
||||
import io.kvision.FontAwesomeModule
|
||||
import io.kvision.BootstrapModule
|
||||
import io.kvision.ChartModule
|
||||
import io.kvision.CoreModule
|
||||
import io.kvision.FontAwesomeModule
|
||||
import io.kvision.html.div
|
||||
import io.kvision.maps.Maps
|
||||
import io.kvision.module
|
||||
import io.kvision.panel.root
|
||||
import io.kvision.redux.ReduxStore
|
||||
import io.kvision.startApplication
|
||||
|
||||
class App : Application() {
|
||||
override fun start() {
|
||||
root("kvapp") {
|
||||
div("Hello world")
|
||||
}
|
||||
|
||||
class App(
|
||||
reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
|
||||
wsService: WebsocketService
|
||||
) : Application() {
|
||||
private var appState: MutableMap<String, Any> = mutableMapOf()
|
||||
|
||||
var reduxStore: ReduxStore<FactorioGameState, FactorioUpdate> by appState
|
||||
var wsService: WebsocketService by appState
|
||||
|
||||
init {
|
||||
this.reduxStore = reduxStore
|
||||
this.wsService = wsService
|
||||
}
|
||||
|
||||
private val kvMaps: Maps
|
||||
get() = reduxStore.getState().kvMaps
|
||||
|
||||
override fun start(state: Map<String, Any>) {
|
||||
this.appState = state.toMutableMap()
|
||||
|
||||
root("kvapp") {
|
||||
div("Kafkatorio Web Map")
|
||||
add(kvMaps)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose(): Map<String, Any> {
|
||||
return appState
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun main() {
|
||||
startApplication(
|
||||
::App,
|
||||
module.hot,
|
||||
BootstrapModule,
|
||||
BootstrapCssModule,
|
||||
BootstrapIconsModule,
|
||||
FontAwesomeModule,
|
||||
ChartModule,
|
||||
CoreModule
|
||||
)
|
||||
startApplication(
|
||||
{
|
||||
val reduxStore = createFactorioReduxStore()
|
||||
val wsService = WebsocketService(
|
||||
"ws://localhost:9073/ws",
|
||||
reduxStore,
|
||||
)
|
||||
|
||||
App(
|
||||
reduxStore = reduxStore,
|
||||
wsService = wsService
|
||||
)
|
||||
},
|
||||
module.hot,
|
||||
BootstrapModule,
|
||||
BootstrapCssModule,
|
||||
BootstrapIconsModule,
|
||||
FontAwesomeModule,
|
||||
ChartModule,
|
||||
CoreModule
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import dev.adamko.kafkatorio.events.schema.Colour
|
||||
import dev.adamko.kafkatorio.events.schema.PlayerData
|
||||
import externals.leaflet.geo.LatLng
|
||||
import io.kvision.maps.Maps
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
fun Colour.toHexString(includeAlpha: Boolean = false): String {
|
||||
|
||||
val hexConverter: Float.() -> String = if (isDecimal()) {
|
||||
Float::decimalToHex
|
||||
} else {
|
||||
Float::percentageToHex
|
||||
}
|
||||
|
||||
val length = if (includeAlpha) 9 else 7
|
||||
return buildString(length) {
|
||||
append('#')
|
||||
append(red.hexConverter())
|
||||
append(green.hexConverter())
|
||||
append(blue.hexConverter())
|
||||
if (includeAlpha) {
|
||||
append(alpha.hexConverter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Float.decimalToHex(): String =
|
||||
roundToInt().toString(16).padStart(2, ' ')
|
||||
|
||||
fun Float.percentageToHex(): String =
|
||||
times(255).roundToInt().toString(16).padStart(2, ' ')
|
||||
|
||||
fun PlayerData.latLng(): LatLng = Maps.L.latLng((position.y * -1) to position.x)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val jsonMapper = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import externals.leaflet.geo.CRS
|
||||
import externals.leaflet.geo.LatLng
|
||||
import io.kvision.maps.DefaultTileLayers
|
||||
import io.kvision.maps.Maps
|
||||
import io.kvision.utils.px
|
||||
|
||||
//
|
||||
//val polyline = Maps.L.polyline(
|
||||
// listOf(
|
||||
// LatLng(55, 2),
|
||||
// LatLng(65, 2),
|
||||
// LatLng(65, 20),
|
||||
// LatLng(55, 20),
|
||||
// LatLng(55, 2),
|
||||
// )
|
||||
//) {
|
||||
// noClip = true
|
||||
//}
|
||||
|
||||
fun createFactorioMap() = Maps {
|
||||
width = 800.px
|
||||
height = 800.px
|
||||
margin = 10.px
|
||||
|
||||
configureLeafletMap {
|
||||
setView(LatLng(0, 0), 13)
|
||||
|
||||
// DefaultTileLayers.OpenStreetMap.addTo(this)
|
||||
|
||||
with(options) {
|
||||
crs = CRS.Simple
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import dev.adamko.kafkatorio.events.schema.FactorioEvent
|
||||
import dev.adamko.kafkatorio.events.schema.PlayerData
|
||||
import externals.leaflet.geo.LatLng
|
||||
import externals.leaflet.layer.overlay.Tooltip
|
||||
import externals.leaflet.layer.vector.CircleMarker
|
||||
import io.kvision.maps.Maps
|
||||
import io.kvision.maps.Maps.Companion.L
|
||||
import io.kvision.redux.ReduxStore
|
||||
import io.kvision.redux.createReduxStore
|
||||
import io.kvision.utils.obj
|
||||
import redux.RAction
|
||||
|
||||
fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
|
||||
createReduxStore(
|
||||
::factorioGameReducer,
|
||||
FactorioGameState(
|
||||
kvMaps = createFactorioMap(),
|
||||
players = emptyMap()
|
||||
)
|
||||
)
|
||||
|
||||
data class FactorioGameState(
|
||||
val kvMaps: Maps,
|
||||
val players: Map<String, PlayerState>,
|
||||
)
|
||||
|
||||
|
||||
sealed class FactorioUpdate : RAction {
|
||||
data class PlayerUpdate(val event: FactorioEvent<*>, val playerData: PlayerData) :
|
||||
FactorioUpdate()
|
||||
}
|
||||
|
||||
fun factorioGameReducer(state: FactorioGameState, update: FactorioUpdate): FactorioGameState =
|
||||
when (update) {
|
||||
is FactorioUpdate.PlayerUpdate -> {
|
||||
|
||||
val playerState = state.players.getOrElse(update.playerData.name) {
|
||||
val newState = PlayerState(update.playerData)
|
||||
state.kvMaps.leafletMap {
|
||||
addLayer(newState.mapMarker)
|
||||
newState.mapMarker.openPopup()
|
||||
// setView(update.playerData.latLng())
|
||||
}
|
||||
newState
|
||||
}
|
||||
|
||||
playerState.update(update.playerData)
|
||||
|
||||
val updatedPlayersState = state.players + (update.playerData.name to playerState)
|
||||
state.copy(players = updatedPlayersState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class PlayerState(
|
||||
private var playerData: PlayerData,
|
||||
val mapMarker: CircleMarker = createMapMarker(playerData)
|
||||
) {
|
||||
|
||||
fun update(playerData: PlayerData) {
|
||||
this.playerData = playerData
|
||||
mapMarker.setLatLng(playerData.latLng())
|
||||
println("updated player ${playerData.name}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun createMapMarker(playerData: PlayerData): CircleMarker {
|
||||
|
||||
val playerLatLng = playerData.latLng()
|
||||
val playerMapTooltip = "${playerData.name} [$playerLatLng]"
|
||||
|
||||
val circle = L.circleMarker(playerLatLng) {
|
||||
color = playerData.colour.toHexString(false)
|
||||
fillColor = color
|
||||
fillOpacity = 1f
|
||||
radius = 10
|
||||
className = playerData.objectName
|
||||
// interactive = false
|
||||
}
|
||||
|
||||
circle.bindTooltip(playerMapTooltip, obj<Tooltip.TooltipOptions> {
|
||||
// interactive = false
|
||||
})
|
||||
|
||||
return circle
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package dev.adamko.kafkatorio.webmap
|
||||
|
||||
import dev.adamko.kafkatorio.events.schema.FactorioEvent
|
||||
import dev.adamko.kafkatorio.events.schema.FactorioObjectData
|
||||
import dev.adamko.kafkatorio.events.schema.PlayerData
|
||||
import io.kvision.redux.ReduxStore
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.WebSocket
|
||||
|
||||
class WebsocketService(
|
||||
private val wsUrl: String,
|
||||
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
|
||||
) {
|
||||
private val ws = WebSocket(wsUrl)
|
||||
|
||||
init {
|
||||
ws.onmessage = ::handleMessageEvent
|
||||
}
|
||||
|
||||
private fun handleMessageEvent(msg: MessageEvent) {
|
||||
|
||||
val data = msg.data as? String
|
||||
|
||||
if (
|
||||
data != null
|
||||
&& data.trim().startsWith("{")
|
||||
&& data.trim().endsWith("}")
|
||||
) {
|
||||
|
||||
println(data.replace('\n', ' '))
|
||||
val event: FactorioEvent<FactorioObjectData> = jsonMapper.decodeFromString(data)
|
||||
|
||||
when (val eventData = event.data) {
|
||||
is PlayerData -> {
|
||||
reduxStore.dispatch(FactorioUpdate.PlayerUpdate(event, eventData))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
println(data)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>KVision Template</title>
|
||||
<title>Kafkatorio Web Map</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.1.0/dist/superhero/bootstrap.min.css" integrity="sha256-T8h7oW5tHp4MhFhmRoDI8xjnB2ld89qSgc3s9g8T0uU=" crossorigin="anonymous">
|
||||
<script type="text/javascript" src="main.bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue