delete old code, try and tidy up coroutines launching

This commit is contained in:
Adam 2022-02-08 14:55:11 +01:00
parent 1af5912477
commit 215ef66fbd
7 changed files with 29 additions and 623 deletions

View file

@ -30,8 +30,6 @@ class KafkatorioTopology(
playerUpdatesToWsServer(websocketServer, builder)
// val serverMapDataTable = buildFactorioServerMap(packets, tilePrototypesTable)
val tileProtoColourDict = tileProtoColourDictionary(packets)
val tilePrototypesTable = groupTilesIntoChunksWithColours(
@ -40,16 +38,6 @@ class KafkatorioTopology(
)
saveMapTiles(tilePrototypesTable)
// the old way...
// val allMapTilesTable: KTable<TileUpdateRecordKey, MapTile> = allMapTilesTable(builder)
// val tilePrototypeColourTable: KTable<PrototypeName, Colour> = tilePrototypeColourTable(builder)
//
// val webMapTiles: KTable<WebMapTileChunkPosition, WebMapTileChunkPixels> =
// aggregateWebMapTiles(allMapTilesTable, tilePrototypeColourTable)
//
// saveTileImages(webMapTiles)
val topology = builder.build()
val streams = KafkaStreams(topology, appProps.kafkaConfig)

View file

@ -20,22 +20,6 @@ import kotlinx.serialization.Serializable
import org.apache.kafka.streams.kstream.KStream
import org.apache.kafka.streams.kstream.KTable
//@Serializable
//data class FactorioServerMap(
// val serverId: FactorioServerId,
// val tilePrototypes: Map<PrototypeName, MapTilePrototype>,
// val surfaces: Map<SurfaceIndex, FactorioMapSurface>,
//)
//@Serializable
//data class FactorioMapSurface(
// val index: SurfaceIndex,
// val tiles: Map<MapTilePosition, PrototypeName>,
//)
@Serializable
data class ServerMapChunkId(
val serverId: FactorioServerId,
@ -135,62 +119,3 @@ fun groupTilesIntoChunksWithColours(
return colourisedChunkTable
}
//@Serializable
//private data class FactorioServerMapAggregator(
// val tilePrototypes: SortedMap<PrototypeName, Colour> = TreeMap(),
// val surfaces: MutableMap<SurfaceIndex, FactorioMapSurfaceAggregator> = mutableMapOf(),
//) {
//
// operator fun plus(packet: KafkatorioPacket): FactorioServerMapAggregator {
// return when (packet) {
// is FactorioEvent -> {
// when (val data = packet.data) {
// is MapChunk -> plus(data)
// is MapTiles -> plus(data)
// else -> this
// }
// }
// is FactorioPrototypes -> plus(packet)
// is FactorioConfigurationUpdate -> this
// }
// }
//
// operator fun plus(mapTiles: MapTiles): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding ${mapTiles.tiles.size} MapTiles from surface ${mapTiles.surfaceIndex}")
//
// val surfaceIndex = SurfaceIndex(mapTiles.surfaceIndex)
//
// val surface = surfaces.getOrPut(surfaceIndex) { FactorioMapSurfaceAggregator(surfaceIndex) }
//
// val surfaceTiles = mapTiles.tiles.associate { it.position to PrototypeName(it.prototypeName) }
// surface.tiles.putAll(surfaceTiles)
//
// return this
// }
//
// operator fun plus(mapChunk: MapChunk): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding mapChunk ${mapChunk.position}")
// return plus(mapChunk.tiles)
// }
//
// operator fun plus(packet: FactorioPrototypes): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding FactorioPrototypes")
//
// val packetPrototypes = packet.prototypes
// .filterIsInstance<MapTilePrototype>()
// .associate { PrototypeName(it.name) to it.mapColour }
//
// tilePrototypes.putAll(packetPrototypes)
//
// return this
// }
//
//}
//
//@Serializable
//private data class FactorioMapSurfaceAggregator(
// val index: SurfaceIndex,
// val tiles: MutableMap<MapTilePosition, PrototypeName> = mutableMapOf(),
//)

View file

@ -1,78 +0,0 @@
package dev.adamko.kafkatorio.processor.topology
import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.FactorioObjectData
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.MapChunk
import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.events.schema.MapTiles
import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.materializedWith
import dev.adamko.kotka.extensions.streams.filter
import dev.adamko.kotka.extensions.streams.flatMap
import dev.adamko.kotka.extensions.streams.mapValues
import dev.adamko.kotka.extensions.streams.toTable
import dev.adamko.kotka.kxs.serde
import java.time.Duration
import kotlinx.serialization.Serializable
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.KStream
import org.apache.kafka.streams.kstream.KTable
import org.apache.kafka.streams.kstream.Suppressed
@Serializable
data class TileUpdateRecordKey(
val surfaceIndex: SurfaceIndex,
val tilePosition: MapTilePosition,
)
fun allMapTilesTable(
builder: StreamsBuilder
): KTable<TileUpdateRecordKey, MapTile> {
val luaTilesUpdatesStream: KStream<FactorioServerId, MapTiles> =
builder.stream<FactorioServerId, FactorioEvent>(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
consumedAs("consume-map-tiles-packets", jsonMapper.serde(), jsonMapper.serde())
)
.filter("events.filter.map-tiles") { _: FactorioServerId, event: FactorioEvent ->
event.data is MapTiles
}
.mapValues("events.extract-map-tiles") { _, event: FactorioEvent ->
(event.data as? MapTiles)!!
}
val chunkTilesUpdateStream: KStream<FactorioServerId, MapTiles> =
builder.stream<FactorioServerId, FactorioEvent>(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
consumedAs("consume-map-chunk-packets", jsonMapper.serde(), jsonMapper.serde())
)
.filter("events.filter.map-chunks") { _: FactorioServerId, event: FactorioEvent ->
event.data is MapChunk
}
.mapValues("events.extract-map-chunks") { _, event: FactorioEvent ->
(event.data as? MapChunk)!!.tiles
}
return luaTilesUpdatesStream
.merge(chunkTilesUpdateStream)
.flatMap("flat-map-all-tiles") { _, value ->
value.tiles.map { tile ->
val key = TileUpdateRecordKey(
SurfaceIndex(value.surfaceIndex),
tile.position
)
key to tile
}
}
.toTable(
"all-tiles-keyed-on-position",
materializedWith(kxsBinary.serde(), kxsBinary.serde())
)
}

View file

@ -59,33 +59,3 @@ fun splitFactorioServerPacketStream(
}
}
}
//@Serializable
//data class FactorioPacketKey(
// val serverId: FactorioServerId,
// /** arbitrary distinctive key */
// val key: String,
//)
//
//private fun createPacketKey(packet: KafkatorioPacket): String {
//
// return when (packet) {
// is FactorioEvent -> factorioEventKey(packet)
// is FactorioConfigurationUpdate,
// is FactorioPrototypes -> packet.hashCode().toString()
// }
//}
//
//private fun factorioEventKey(event: FactorioEvent): String {
// val id: String = when (val data = event.data) {
// is ConsoleChatMessage -> data.authorPlayerIndex?.toString()
// is EntityData -> data.type
// is MapChunk -> data.position.toString()
//// is MapTile -> data.position.toString()
// is MapTiles -> data.tiles.firstOrNull()?.position?.toMapChunkPosition()?.toString()
// is PlayerData -> (data.characterUnitNumber?.toString() ?: data.name)
// is SurfaceData -> data.index.toString()
// } ?: event.tick.toString()
//
// return id
//}

View file

@ -19,10 +19,9 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.runningFold
@ -39,15 +38,9 @@ private val cScope = CoroutineScope(
Dispatchers.Default + SupervisorJob() + CoroutineName("f-server-map-updates")
)
private val serverMapChunkHandler = ServerMapChunkHandler()
///** A bridge between Kafka and Kotlin */
//private val chunkedTiles: MutableSharedFlow<ServerMapChunkTiles<ColourHex>> =
// MutableSharedFlow(
// 0,
// 1,
// BufferOverflow.SUSPEND
// )
/** A bridge between Kafka and Kotlin */
private val serverMapChunkHandler = ServerMapChunkHandler()
fun saveMapTiles(
@ -60,90 +53,11 @@ fun saveMapTiles(
if (value != null) {
runBlocking(Dispatchers.Default) {
println("emitting chunkedTiles ${value.chunkId} tiles: ${value.map.size}")
// chunkedTiles.emit(value)
serverMapChunkHandler.emit(value)
}
}
}
// cScope.launch {
// supervisorScope {
// chunkedTiles
// .conflate()
// .distinctUntilChanged()
// .debounce(1.seconds)
// .map {
// println("createMapTileImage ${it.chunkId}")
// createMapTileImage(it)
// }
// .collect()
// }
// }
// cScope.launch {
// println("launching serverMapDataFlow merging")
//
// chunkedTiles
// .conflate()
// .flatMapConcat { chunk ->
// ZoomLevel.values
// .asFlow()
// .flatMapConcat { zoom ->
// chunk.map.entries
// .groupBy({ (tile, _) -> tile.toMapChunkPosition(zoom.chunkSize) }
// ) {
// it.key to it.value
// }
// .map { (chunkPos, tiles) ->
// val chunkId = chunk.chunkId.copy(
// zoomLevel = zoom,
// chunkPosition = chunkPos
// )
// ServerMapChunkTiles(chunkId, tiles.toMap())
// }
// .asFlow()
// }
// }
// .runningFold(
// mapOf<ServerMapChunkId, MutableSharedFlow<ServerMapChunkTiles<ColourHex>>>()
// ) { accumulator, value ->
//
// println("folding chunkedTiles ${value.chunkId}, current flows: ${accumulator.keys}")
//
// val chunkFlow = accumulator.getOrElse(value.chunkId) {
//
// println("creating new MutableSharedFlow for ${value.chunkId}")
//
// val input = MutableSharedFlow<ServerMapChunkTiles<ColourHex>>(
// replay = 0,
// extraBufferCapacity = 1,
// onBufferOverflow = BufferOverflow.DROP_OLDEST,
// )
//
// supervisorScope {
// println("launching MutableSharedFlow for ${value.chunkId}")
// input
// .conflate()
// .distinctUntilChanged()
// .debounce(30.seconds)
// .mapLatest {
// println("createMapTileImage ${it.chunkId}")
// createMapTileImage(it)
// }
// .launchIn(this)
// }
//
// input
// }
//
// chunkFlow.emit(value)
//
// accumulator + (value.chunkId to chunkFlow)
// }
// .collect()
//
// println("end of cScope.launch")
// }
}
@ -161,33 +75,6 @@ private value class TilePngFilename(
})
}
//private fun createMapTileImage(
// chunkTiles: ServerMapChunkTiles<ColourHex>,
//
// zoomLevel: ZoomLevel
//) {
//
// println("creating chunkData ${chunkTiles.chunkId.chunkPosition}...")
//
// val filename = TilePngFilename(chunkTiles.chunkId)
//
// val chunkImageFile = File(filename.value)
// if (chunkImageFile.parentFile.mkdirs()) {
// println("created new map tile parentFile directory ${chunkImageFile.absolutePath}")
// }
//
// val image =
// createMapTileImage(
// chunkTiles.chunkId.chunkPosition,
// chunkTiles.map,
// chunkTiles.chunkId.zoomLevel
// )
//
// println("saving map tile $chunkImageFile")
// val savedTile = image.output(PngWriter.NoCompression, chunkImageFile)
// println("savedTile: $savedTile")
//}
//
private val TRANSPARENT_AWT = ColourHex.TRANSPARENT.toRgbColor().awt()
@ -257,48 +144,36 @@ private class ServerMapChunkHandler : CoroutineScope {
init {
launch {
supervisorScope {
saveImagesFlow
// .conflate()
.onEach { (filename, img) ->
println("saving map tile $filename")
allChunksFlow
.runningFold(mapOf<ServerMapChunkId, MutableSharedFlow<Cmd.ChunkSubdivide>>()) { acc, src ->
val chunkImageFile = File(filename.value)
if (chunkImageFile.parentFile.mkdirs()) {
println("created new map tile parentFile directory ${chunkImageFile.absolutePath}")
}
val flow = acc.getOrElse(src.chunkId) {
println("creating new subdivide-flow for chunkId ${src.chunkId}")
subdivisionFlow()
}
val savedTile = img.output(PngWriter.NoCompression, chunkImageFile)
println("savedTile: $savedTile")
}
.collect()
flow.emit(Cmd.ChunkSubdivide(src.chunkId, src.map))
acc + (src.chunkId to flow)
}
}
launch {
supervisorScope {
allChunksFlow
// .conflate()
.runningFold(mapOf<ServerMapChunkId, MutableSharedFlow<Cmd.ChunkSubdivide>>()) { acc, src ->
val flow = acc.getOrElse(src.chunkId) {
println("creating new subdivide-flow for chunkId ${src.chunkId}")
subdivisionFlow()
}
launch {
flow.emit(Cmd.ChunkSubdivide(src.chunkId, src.map))
}
acc + (src.chunkId to flow)
}
.onEach {
println("allChunksFlow has ${it.size} subdivide-flows: ${it.keys.joinToString()}")
}
.collect()
.onEach {
println("allChunksFlow has ${it.size} subdivide-flows: ${it.keys.joinToString()}")
}
}
.launchIn(this)
saveImagesFlow
.onEach { (filename, img) ->
println("saving map tile $filename")
val chunkImageFile = File(filename.value)
if (chunkImageFile.parentFile.mkdirs()) {
println("created new map tile parentFile directory ${chunkImageFile.absolutePath}")
}
val savedTile = img.output(PngWriter.NoCompression, chunkImageFile)
println("savedTile: $savedTile")
}
.launchIn(this)
}

View file

@ -1,173 +0,0 @@
package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.color.RGBColor
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kotka.extensions.groupedAs
import dev.adamko.kotka.extensions.materializedAs
import dev.adamko.kotka.extensions.materializedWith
import dev.adamko.kotka.extensions.namedAs
import dev.adamko.kotka.extensions.streams.groupBy
import dev.adamko.kotka.extensions.tables.groupBy
import dev.adamko.kotka.extensions.tables.join
import dev.adamko.kotka.extensions.tables.mapValues
import dev.adamko.kotka.extensions.tables.toStream
import dev.adamko.kotka.extensions.toKeyValue
import dev.adamko.kotka.kxs.serde
import java.awt.Color
import java.time.Duration
import kotlinx.serialization.Serializable
import org.apache.kafka.streams.kstream.KTable
import org.apache.kafka.streams.kstream.SessionWindows
import org.apache.kafka.streams.kstream.Suppressed
import org.apache.kafka.streams.kstream.Windowed
/** The tiles of the webmap will be this many pixels high/wide */
const val WEB_MAP_IMAGE_TILE_SIZE = 256
/** Debounce duration */
private val WINDOW_INACTIVITY_DURATION = Duration.ofSeconds(60)
@Serializable
data class WebMapTileChunkPosition(
val chunkSize: Int,
val x: Int,
val y: Int,
val surfaceIndex: SurfaceIndex,
)
@Serializable
private data class WebMapTilePixel(
val mapColour: Colour,
val tilePosition: MapTilePosition,
)
@Serializable
@JvmInline
value class WebMapTileChunkPixels(
val pixels: Map<MapTilePosition, Colour>,
)
/** Temporary mutable accumulator */
@Serializable
@JvmInline
private value class WebMapTileChunkPixelsAcc(
val pixels: MutableMap<MapTilePosition, Colour> = mutableMapOf(),
) {
operator fun plus(pixel: WebMapTilePixel): WebMapTileChunkPixelsAcc {
pixels[pixel.tilePosition] = pixel.mapColour
return this
}
operator fun plus(other: WebMapTileChunkPixelsAcc): WebMapTileChunkPixelsAcc {
this.pixels += other.pixels
return this
}
operator fun minus(other: WebMapTileChunkPixelsAcc): WebMapTileChunkPixelsAcc {
this.pixels -= other.pixels.keys
return this
}
}
fun aggregateWebMapTiles(
allMapTilesTable: KTable<TileUpdateRecordKey, MapTile>,
tilePrototypeColourTable: KTable<PrototypeName, Colour>,
): KTable<WebMapTileChunkPosition, WebMapTileChunkPixels> {
val pixels: KTable<TileUpdateRecordKey, WebMapTilePixel> =
allMapTilesTable
.join(
tilePrototypeColourTable,
"convert-tiles-to-colours",
materializedAs(
"store-web-map-tile-pixels",
kxsBinary.serde(),
kxsBinary.serde(),
),
{ PrototypeName(it.prototypeName) },
) { tile: MapTile, colour: Colour ->
WebMapTilePixel(colour, tile.position)
}
val chunkedPixelsWindowed: KTable<Windowed<WebMapTileChunkPosition>, WebMapTileChunkPixelsAcc> =
pixels
.toStream("stream-webmap-tile-pixels")
.groupBy(
groupedAs(
"group-webmap-tile-colours-into-chunks",
kxsBinary.serde(),
kxsBinary.serde(),
)
) { pos: TileUpdateRecordKey, _: WebMapTilePixel ->
val (chunkX, chunkY) = pos.tilePosition.toMapChunkPosition(WEB_MAP_IMAGE_TILE_SIZE)
val chunkPos = WebMapTileChunkPosition(
WEB_MAP_IMAGE_TILE_SIZE,
chunkX,
chunkY,
pos.surfaceIndex
)
chunkPos
}
.windowedBy(
SessionWindows.ofInactivityGapAndGrace(
WINDOW_INACTIVITY_DURATION,
WINDOW_INACTIVITY_DURATION,
)
)
.aggregate(
{ WebMapTileChunkPixelsAcc() },
{ _: WebMapTileChunkPosition, newValue: WebMapTilePixel, acc: WebMapTileChunkPixelsAcc ->
acc + newValue
},
{ _, aggOne: WebMapTileChunkPixelsAcc, aggTwo: WebMapTileChunkPixelsAcc ->
aggOne + aggTwo
},
namedAs("web-map-tile-colours-into-$WEB_MAP_IMAGE_TILE_SIZE-chunks"),
materializedAs(
"web-map-tile-colour-chunks-aggregate",
kxsBinary.serde(),
kxsBinary.serde()
)
)
.suppress(
Suppressed.untilWindowCloses(
Suppressed.BufferConfig.unbounded()
).withName("chunked-pixels-debounce")
)
// merge windows
val chunkedPixels: KTable<WebMapTileChunkPosition, WebMapTileChunkPixelsAcc> =
chunkedPixelsWindowed
.groupBy(
groupedAs("merge-chunked-pixels.group-keys", kxsBinary.serde(), kxsBinary.serde())
) { a: Windowed<WebMapTileChunkPosition>, b: WebMapTileChunkPixelsAcc ->
(a.key() to b).toKeyValue()
}
.reduce(
/* adder */
{ pixels1, pixels2 -> pixels1 + pixels2 },
/* subtractor */
{ pixels1, pixels2 -> pixels1 - pixels2 },
namedAs("merge-chunked-pixels.reduce-values"),
materializedAs("merge-chunked-pixels.reduce-values.store", kxsBinary.serde(), kxsBinary.serde())
)
return chunkedPixels
.mapValues(
"finalise-web-map-tile-colour-chunk-aggregation",
materializedAs("web-map-tile-colour-chunks", kxsBinary.serde(), kxsBinary.serde())
) { _: WebMapTileChunkPosition, v: WebMapTileChunkPixelsAcc ->
WebMapTileChunkPixels(v.pixels)
}
}

View file

@ -1,101 +0,0 @@
package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.ImmutableImage
import com.sksamuel.scrimage.color.RGBColor
import com.sksamuel.scrimage.nio.PngWriter
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kotka.extensions.tables.toStream
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.File
import kotlin.math.abs
import kotlin.math.roundToInt
import org.apache.kafka.streams.kstream.KTable
//
//val COLOUR_TRANSPARENT: RGBColor = RGBColor.fromAwt(Color(0f, 0f, 0f, 0f))
//
//fun saveTileImages(
// webMapTiles: KTable<WebMapTileChunkPosition, WebMapTileChunkPixels>
//) {
//
// webMapTiles
// .toStream("get-webmap-tile-updates")
// .foreach { position: WebMapTileChunkPosition, pixels: WebMapTileChunkPixels ->
// runCatching {
// saveMapTilesPng(position, pixels.pixels)
// }.onFailure { e ->
// println("error saving map tile png chunk: $position")
// e.printStackTrace()
// throw e
// }
// }
//}
//
////val saveMapTilesContext = Dispatchers.IO.limitedParallelism(1)
//
//
//private fun saveMapTilesPng(
// chunkPosition: WebMapTileChunkPosition,
// pixels: Map<MapTilePosition, Colour>,
//) {
// val chunkOriginX: Int = chunkPosition.x * chunkPosition.chunkSize // - 1
// val chunkOriginY: Int = chunkPosition.y * chunkPosition.chunkSize // - 1
// val surfaceIndex: Int = chunkPosition.surfaceIndex.index
//
// val chunkImage =
// ImmutableImage.filled(
// chunkPosition.chunkSize,
// chunkPosition.chunkSize,
// COLOUR_TRANSPARENT.awt(),
// BufferedImage.TYPE_INT_ARGB
// )
//
// pixels.forEach { (tilePosition, colour) ->
//
// val rgbColour = colour.toRgbColor()
//
// val pixelX = abs(abs(tilePosition.x) - abs(chunkOriginX))
// val pixelY = abs(abs(tilePosition.y) - abs(chunkOriginY))
//
// chunkImage.setColor(
// pixelX,
// pixelY,
// rgbColour
// )
//
// }
//
// val zoom = 1u
// val file =
// File(tileFilename("$surfaceIndex", "$zoom", "${chunkPosition.x}", "${chunkPosition.y}"))
//
// if (file.parentFile.mkdirs()) {
// println("created new map tile parentFile directory ${file.absolutePath}")
// }
//
// println("saving map tile $file")
// val savedTile = chunkImage.output(PngWriter.NoCompression, file)
// println("savedTile: $savedTile")
//}
//
//fun tileFilename(
// surfaceIndex: String,
// zoom: String,
// chunkX: String,
// chunkY: String,
//) = "src/main/resources/kafkatorio-web-map/s${surfaceIndex}/z$zoom/x${chunkX}/y${chunkY}.png"
//
////fun subdivide(tile: ImmutableImage, size: Int) {
////
//// val halfSize = size / 2
////
//// val bl = tile.resizeTo(halfSize, halfSize, Position.BottomLeft)
//// val savedTile = bl.output(PngWriter.NoCompression, file)
////
//// tile.resizeTo(halfSize, halfSize, Position.BottomRight)
//// tile.resizeTo(halfSize, halfSize, Position.TopLeft)
//// tile.resizeTo(halfSize, halfSize, Position.TopRight)
////
////}