I'm creating image map tiles for Leaflet.js based on data from a computer game. I'm processing the map data in Kotlin. A map tile server for Leaflet.js has to host image tiles at various zoom-levels, so I need to create them.
These are the resolutions I want to create, based on a source image of 512x512px.
512x512 pixels (zoomed out the most)
256x256 pixels
128x128 pixels
64x64 pixels
32x32 pixels (the most zoomed in)
A code example is at the bottom of this post.
I'm using groupBy at the moment, but the performance isn't great.
// for each possible chunk size...
ChunkSize.entries.flatMap { chunkSize ->
// and for each tile...
chunk.tiles.entries.groupBy(
// get the chunk the tile belongs to
{ (tile, _) -> tile.toChunkPosition(chunkSize) }
) { (tile, colour) ->
tile to colour
}.map { (chunkPosition, tiles) ->
// aggregate the grouped tiles into a map,
// and create a new chunk
Chunk(
tiles = tiles.toMap(),
size = chunkSize,
position = chunkPosition,
)
}
}
// this can take up to 0.5 seconds
It takes around 0.5 seconds to convert a 512x512px source image into
1 512x512px tile
4 256x256px tiles
16 128x128px tiles
32 64x64px tiles
64 32x32px tiles
I'd like to improve the performance.
Options
Sorting and chunking/windowing
Using windows won't be easy, because the data in the tiles isn't necessarily continuous. There might be gaps between some tiles.
Grouping
I've tried using Grouping, but I didn't note a significant difference. The lazy evaluation isn't useful here, and using a mutable map to try and improve the accumulation didn't help either.
ChunkSize.entries.flatMap { chunkSize ->
val grouped: Map<ChunkPosition, MutableMap<TilePosition, Colour>> =
chunk.tiles.entries.groupingBy { (tile, _) ->
tile.toChunkPosition(chunkSize)
}.fold(
initialValueSelector = { _, _ -> mutableMapOf() },
) { _, accumulator, (tilePosition, colour) ->
accumulator[tilePosition] = colour
accumulator
}
grouped.entries.map { (chunkPosition, tiles) ->
Chunk(
tiles = tiles,
size = chunkSize,
position = chunkPosition,
)
}
}
Optimise toChunkPosition?
The function for getting the chunk position for every tile, and it's using division, which can be slow.
fun TilePosition.toChunkPosition(chunkSize: ChunkSize) =
ChunkPosition(
floor(x.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
)
Coroutines
I'm open to using coroutines, so work can be done in parallel, but first I want to optimise the existing code.
Full code
This is a simplified example. The chunk sizes have been reduced to 1, 2, 4, 8, and 16 pixels.
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.time.measureTimedValue
val sourceChunk = Chunk(
size = ChunkSize.MAX,
position = ChunkPosition(0, 0),
// create some dummy test data
tiles = listOf(
"0000000000000088",
"1111111110000088",
"0000000000000088",
"0000000222722288",
"0090000000700000",
"3393333330700000",
"0090000000700000",
"0090000444744444",
"0090000000700000",
"5595555000700000",
"0090000000000000",
"0090000066666666",
).flatMapIndexed { y, row ->
row.mapIndexed { x, colour ->
TilePosition(x, y) to Colour("$colour")
}
}.toMap()
)
fun main() {
println("Source chunk")
printChunk(sourceChunk)
println("-------")
val (chunks, time) = measureTimedValue {
subdivideChunk(sourceChunk)
}
chunks.forEach {
println("-------")
printChunk(it)
}
println("-------")
println("took: $time")
}
fun subdivideChunk(chunk: Chunk): List<Chunk> {
return ChunkSize.entries.flatMap { chunkSize ->
val grouped: Map<ChunkPosition, MutableMap<TilePosition, Colour>> =
chunk.tiles.entries.groupingBy { (tile, _) ->
tile.toChunkPosition(chunkSize)
}.fold(
initialValueSelector = { _, _ -> mutableMapOf() },
) { _, accumulator, (tilePosition, colour) ->
accumulator[tilePosition] = colour
accumulator
}
grouped.entries.map { (chunkPosition, tiles) ->
Chunk(
tiles = tiles,
size = chunkSize,
position = chunkPosition,
)
}
chunk.tiles.entries.groupBy(
{ (tile, _) -> tile.toChunkPosition(chunkSize) }
) { (tile, colour) ->
tile to colour
}.map { (chunkPosition, tiles) ->
Chunk(
tiles = tiles.toMap(),
size = chunkSize,
position = chunkPosition,
)
}
chunk.tiles.entries
.groupingBy { (tile, _) ->
tile.toChunkPosition(chunkSize)
}.fold(mutableMapOf<TilePosition, Colour>()) { accumulator, (tilePosition, colour) ->
accumulator += tilePosition to colour
accumulator
}.map { (chunkPosition, tiles) ->
Chunk(
tiles = tiles,
size = chunkSize,
position = chunkPosition,
)
}
}
}
fun printChunk(chunk: Chunk) {
println("chunk ${chunk.position} ${chunk.size}")
val minX = chunk.tiles.keys.minOf { it.x }
val minY = chunk.tiles.keys.minOf { it.y }
val maxX = chunk.tiles.keys.maxOf { it.x }
val maxY = chunk.tiles.keys.maxOf { it.y }
(minY..maxY).forEach { y ->
(minX..maxX).forEach { x ->
print(chunk.tiles[TilePosition(x, y)]?.rgba ?: " ")
}
println()
}
}
data class Chunk(
val tiles: Map<TilePosition, Colour>,
val size: ChunkSize,
val position: ChunkPosition,
) {
val topLeftTile: TilePosition = position.toTilePosition(size)
val bottomRightTile: TilePosition = TilePosition(
x = topLeftTile.x + size.lengthInTiles - 1,
y = topLeftTile.y + size.lengthInTiles - 1,
)
val xTileRange = topLeftTile.x..bottomRightTile.x
val yTileRange = topLeftTile.y..bottomRightTile.y
operator fun contains(tilePosition: TilePosition): Boolean =
tilePosition.x in xTileRange && tilePosition.y in yTileRange
}
data class Colour(val rgba: String)
data class TilePosition(val x: Int, val y: Int)
fun TilePosition.toChunkPosition(chunkSize: ChunkSize) =
ChunkPosition(
floor(x.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
)
data class ChunkPosition(val x: Int, val y: Int)
fun ChunkPosition.toTilePosition(chunkSize: ChunkSize) =
TilePosition(
x * chunkSize.lengthInTiles,
y * chunkSize.lengthInTiles,
)
enum class ChunkSize(
val zoomLevel: Int,
) : Comparable<ChunkSize> {
CHUNK_512(-1),
CHUNK_256(0),
CHUNK_128(1),
CHUNK_064(2),
CHUNK_032(3),
;
/** 1, 2, 4, 8, or 16 */
val lengthInTiles: Int = 2f.pow(3 - zoomLevel).roundToInt()
companion object {
val entries: Set<ChunkSize> = values().toSet()
val MAX: ChunkSize = entries.maxByOrNull { it.lengthInTiles }!!
val MIN: ChunkSize = entries.minByOrNull { it.lengthInTiles }!!
}
}
I resolved this with 3 fixes
I didn't need to create sub-tiles for small zoom levels.
Instead of calculating the chunk position for each tile, I instead calculated the position once and then verified if the remaining tiles was within this chunk a 'chunk boundary'
I refactored so that instead of splitting a large chunk into smaller chunks, I built up larger chunks from smaller chunks
The result is twice as fast, when using my basic example. In the full-fat code with real data it's significantly faster, and that's without any further improvements (e.g. using coroutines to process
in parallel).
CSS render pixelated
Originally I created smaller tiles because the tiles became blurry as I zoomed in.
However, the original map data is all just pixels. Zooming in doesn't increase the amount of pixels.
If you open up the source image in a basic image editor, like Paint, and zoom in you'll see this
source image doesn't blur.
The tiles became blurry because my web browser was 'optimising' the images. Instead, I set the CSS
property image-rendering: pixelated
. This disables the optimisation.
Because of this I no longer need to create zoomed in tiles. Leaflet can instead dynamically scale
the larger images.
Group by chunk boundary
Instead of the 'expensive' toChunkPosition() used to determine if a tile is in a specific
chunk, I instead created a 'chunk boundary' that has two boundary points - 'left top' and 'right
bottom'.
data class ChunkPositionBounds(
val leftTop: TilePosition,
val rightBottom: TilePosition,
) : ClosedRange<TilePosition> {
constructor(chunkPosition: ChunkPosition, chunkSize: ChunkSize) : this(
chunkPosition.leftTopTile(chunkSize),
chunkPosition.rightBottomTile(chunkSize),
)
override val start: TilePosition
get() = leftTop
override val endInclusive: TilePosition
get() = rightBottom
}
I implemented the ClosedRange<MapTilePosition> so I can create a nice operator function
for MapChunkPosition
data class ChunkPosition(val x: Int, val y: Int) {
fun leftTopTile(chunkSize: ChunkSize): TilePosition =
TilePosition(
x * chunkSize.lengthInTiles,
y * chunkSize.lengthInTiles,
)
fun rightBottomTile(chunkSize: ChunkSize): TilePosition =
leftTopTile(chunkSize) + (chunkSize.lengthInTiles - 1)
}
Now given a chunk I can verify all its tiles have the correct position
/** Verify all [tiles][Chunk.tiles] are contained within [chunk] */
fun validateChunk(chunk: Chunk): Chunk {
val (validTiles, invalidTiles) = chunk.tiles.entries.partition { (tilePos, _) ->
tilePos in chunk
}
return if (invalidTiles.isNotEmpty()) {
println("WARNING Chunk $chunk contained ${invalidTiles.size}/${chunk.tiles.size} out-of-bounds tiles $invalidTiles")
chunk.copy(tiles = validTiles.associate { it.key to it.value })
} else {
chunk
}
}
Re-use previously grouped tiles
The final optimisation is that I only grouped tiles into a chunk in this per-tile basis once.
Once I was sure the input data was 'clean', I could re-use the chunked data to create larger
tiles.
(This optimisation is not so important given that smaller tiles are not needed thanks to the
css-rendering trick - but I'll describe it in case someone else finds this useful.)
/**
* Get all chunks of size [srcChunkSize] from [srcChunks], then for each source chunk
* create a new [ChunkPosition], based on [newChunkSize].
*
* Group the chunks by the new position, and merge the tiles of each chunk.
*/
fun regroupByChunkSize(
srcChunks: List<Chunk>,
srcChunkSize: ChunkSize,
newChunkSize: ChunkSize,
): List<Chunk> {
return srcChunks
.filter { it.size == srcChunkSize }
.groupBy { chunk ->
chunk.position
.leftTopTile(chunk.size)
.toChunkPosition(newChunkSize)
}.map { (newChunkPosition, chunks) ->
val tiles = chunks.flatMap {
it.tiles.entries
}.associate { (k, v) -> k to v }
Chunk(
tiles = tiles,
size = newChunkSize,
position = newChunkPosition,
)
}
}
I could then iteratively call regroupByChunkSize(...) to incrementally aggregate chunks.
fun aggregateChunks(
sourceChunks: List<Chunk>
): List<Chunk> {
val allGroupedChunks = mutableListOf<Chunk>()
val groupedChunks032 = sourceChunks.map { chunk -> validateChunk(chunk) }
allGroupedChunks += groupedChunks032
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_032, ChunkSize.CHUNK_064)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_064, ChunkSize.CHUNK_128)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_128, ChunkSize.CHUNK_256)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_256, ChunkSize.CHUNK_512)
return allGroupedChunks.toList()
}
(This could be improved. Also, this example looks strange. The actual code I'm using is using Kafka,
so sourceChunks is a live updating list, and each regroupByChunkSize(...) call is an independent
Stream Task.)
Full example code
This code is very scruffy. I'm just using it for demo purposes.
If you want to use smaller chunk sizes, then change
val lengthInTiles: Int = 2f.pow(8 - zoomLevel).roundToInt()
to
val lengthInTiles: Int = 2f.pow(3 - zoomLevel).roundToInt()
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.time.measureTimedValue
fun main() {
val sourceChunks = testData()
val (chunks, time) = measureTimedValue {
aggregateChunks(sourceChunks)
}
// chunks.forEach {
// println("-------")
// printChunk(it)
// }
// println("-------")
println("took: $time")
}
fun aggregateChunks(
sourceChunks: List<Chunk>
): List<Chunk> {
val allGroupedChunks = mutableListOf<Chunk>()
val groupedChunks032 = sourceChunks.map { chunk -> validateChunk(chunk) }
allGroupedChunks += groupedChunks032
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_032, ChunkSize.CHUNK_064)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_064, ChunkSize.CHUNK_128)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_128, ChunkSize.CHUNK_256)
allGroupedChunks += regroupByChunkSize(allGroupedChunks, ChunkSize.CHUNK_256, ChunkSize.CHUNK_512)
return allGroupedChunks.toList()
}
/** Verify all [tiles][Chunk.tiles] are contained within [chunk] */
fun validateChunk(chunk: Chunk): Chunk {
val (validTiles, invalidTiles) = chunk.tiles.entries.partition { (tilePos, _) ->
tilePos in chunk
}
return if (invalidTiles.isNotEmpty()) {
println("WARNING Chunk $chunk contained ${invalidTiles.size}/${chunk.tiles.size} out-of-bounds tiles $invalidTiles")
chunk.copy(tiles = validTiles.associate { it.key to it.value })
} else {
chunk
}
}
/**
* Get all chunks of size [srcChunkSize] from [srcChunks], then for each source chunk
* create a new [ChunkPosition], based on [newChunkSize].
*
* Group the chunks by the new position, and merge the tiles of each chunk.
*/
fun regroupByChunkSize(
srcChunks: List<Chunk>,
srcChunkSize: ChunkSize,
newChunkSize: ChunkSize,
): List<Chunk> {
return srcChunks
.filter { it.size == srcChunkSize }
.groupBy { chunk ->
chunk.position
.leftTopTile(chunk.size)
.toChunkPosition(newChunkSize)
}.map { (newChunkPosition, chunks) ->
val tiles = chunks.flatMap {
it.tiles.entries
}.associate { (k, v) -> k to v }
Chunk(
tiles = tiles,
size = newChunkSize,
position = newChunkPosition,
)
}
}
fun printChunk(chunk: Chunk) {
println("chunk ${chunk.position} ${chunk.size}")
val minX = chunk.tiles.keys.minOf { it.x }
val minY = chunk.tiles.keys.minOf { it.y }
val maxX = chunk.tiles.keys.maxOf { it.x }
val maxY = chunk.tiles.keys.maxOf { it.y }
(minY..maxY).forEach { y ->
(minX..maxX).forEach { x ->
print(chunk.tiles[TilePosition(x, y)]?.rgba ?: " ")
}
println()
}
}
data class Chunk(
val tiles: Map<TilePosition, Colour>,
val size: ChunkSize,
val position: ChunkPosition,
) {
val bounds: ChunkPositionBounds by lazy {
ChunkPositionBounds(position, size)
}
operator fun contains(tilePosition: TilePosition): Boolean {
return tilePosition in bounds
}
}
data class ChunkPositionBounds(
val leftTop: TilePosition,
val rightBottom: TilePosition,
) : ClosedRange<TilePosition> {
constructor(chunkPosition: ChunkPosition, chunkSize: ChunkSize) : this(
chunkPosition.leftTopTile(chunkSize),
chunkPosition.rightBottomTile(chunkSize),
)
override val start: TilePosition
get() = leftTop
override val endInclusive: TilePosition
get() = rightBottom
}
data class Colour(val rgba: String)
data class TilePosition(val x: Int, val y: Int) : Comparable<TilePosition> {
override operator fun compareTo(other: TilePosition): Int =
when {
other.x != x -> x.compareTo(other.x)
else -> y.compareTo(other.y)
}
operator fun plus(addend: Int) =
TilePosition(x + addend, y + addend)
fun toChunkPosition(chunkSize: ChunkSize) =
ChunkPosition(
floor(x.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
)
}
data class ChunkPosition(val x: Int, val y: Int) {
fun leftTopTile(chunkSize: ChunkSize): TilePosition =
TilePosition(
x * chunkSize.lengthInTiles,
y * chunkSize.lengthInTiles,
)
fun rightBottomTile(chunkSize: ChunkSize): TilePosition =
leftTopTile(chunkSize) + (chunkSize.lengthInTiles - 1)
}
enum class ChunkSize(
val zoomLevel: Int,
) : Comparable<ChunkSize> {
CHUNK_512(-1),
CHUNK_256(0),
CHUNK_128(1),
CHUNK_064(2),
CHUNK_032(3),
;
/** 1, 2, 4, 8, or 16 */
val lengthInTiles: Int = 2f.pow(8 - zoomLevel).roundToInt()
companion object {
val entries: Set<ChunkSize> = values().toSet()
val MAX: ChunkSize = entries.maxByOrNull { it.lengthInTiles }!!
val MIN: ChunkSize = entries.minByOrNull { it.lengthInTiles }!!
}
}
fun testData(): List<Chunk> {
val r = Random(1)
val src = List(x.ChunkSize.MAX.lengthInTiles) {
List(x.ChunkSize.MAX.lengthInTiles) { r.nextInt(0..9).toString() }
}
return src.flatMapIndexed { y, row ->
row.mapIndexed { x, colour ->
val tilePos = TilePosition(x, y)
val tileColour = Colour("$colour")
Chunk(
tiles = mapOf(tilePos to tileColour),
size = ChunkSize.MIN,
position = tilePos.toChunkPosition(ChunkSize.MIN),
)
}
}
}
Related
I hope to place a line in new coordinate system, the new origin coordinate is val orig=Point(100,50), the new X axis is to right, and new Y axis is to up.
At present, I use function extensions, just like Code A.
I find it's not good, there are many repeated code such as .toX(orig), .toY(orig).
How can I design the data structure to improve the code?
Code A
val orig=Point(100,50)
drawIntoCanvas {
it.drawLine(
Offset(x = 0f.toX(orig), y = 0f.toY(orig)),
Offset(x = (size.width- 200).toX(orig), y = 0f.toY(orig)),
axisPaint
)
val shadowPath = Path()
val data = maxCountList.toList()
val step= 20
for (i in data.indices){
shadowPath.lineTo((orignX+step*i).toX(orig),data[i].toFloat().toY(orig))
}
shadowPath.close()
it.drawPath(shadowPath,pathPaint)
it.nativeCanvas.drawText("Max",50f.toX(orig), 100f.toY(orig), textPaint)
}
fun Float.toX(originCoordinate: Point) : Float {
return originCoordinate.x+this
}
fun Float.toY(originCoordinate: Point): Float {
return originCoordinate.y-this
}
Ended
The Can I pass a delegated to a variable in Kotlin? is perfect solution!
data class MyPoint(val x: Float, val y: Float) {
val Float.toXX: Float get() = x + this
val Float.toYY: Float get() = y - this
}
// In a function:
val orig = MyPoint(size.width / 2, size.height / 2)
orig.run {
drawPath.moveTo(10.0f.toXX, 20.0f.toYY)
drawPath.moveTo(5.0f.toXX, 80.0f.toYY)
drawPath.moveTo(1.0f.toXX, 3.0f.toYY)
}
I would say that the simplest way would be to actually make your origin the actual origin of the canvas while drawing, by translating the canvas:
fun Canvas.withTranslation(x: Float, y: Float, block: (Canvas) -> Unit) {
withSave {
translate(x, y)
block(this)
}
}
fun DrawScope.drawIntoCanvasWithOrigin(origin: PointF, block: (Canvas) -> Unit) {
drawIntoCanvas {
it.withTranslation(origin.x, origin.y) { canvas ->
block(canvas)
}
}
}
Then you do not need to add the origin to every single value because everything is already relative to your origin, and you can just write:
val orig=Point(100,50)
drawIntoCanvasWithOrigin(orig.toPointF()) {
it.drawLine(
Offset(x = 0f, y = 0f),
Offset(x = size.width- 200, y = 0f),
axisPaint
)
val shadowPath = Path()
val data = maxCountList.toList()
val step= 20
for (i in data.indices){
shadowPath.lineTo(orignX+step*i,data[i].toFloat())
}
shadowPath.close()
it.drawPath(shadowPath,pathPaint)
it.nativeCanvas.drawText("Max",50f, 100f, textPaint)
}
Taking into account your comment that you want to mirror the y-axis at the defined point of origin, we can extend the code accordingly:
fun Canvas.withScale(x: Float, y: Float, block: (Canvas) -> Unit) {
withSave {
scale(x, y)
block(this)
}
}
fun DrawScope.drawIntoCanvasWithOrigin(origin: PointF, block: (Canvas) -> Unit) {
drawIntoCanvas {
it.withTranslation(origin.x, origin.y) { canvasAtOrigin ->
canvasAtOrigin.withScale(1.0f, -1.0f) { mirroredCanvas ->
block(mirroredCanvas)
}
}
}
}
I second Ivo that the extension functions do not really increase readability. What I would do is to not focus on x and y, but on the Points. An offset in a two-dimensional layout is a new Point, so you're probably better off doing something like this:
fun Point.offset(offsetX: Float, offsetY: Float): Point {
return Point(this.x + offsetX, this.y + offsetY)
}
And you use it like this:
it.drawLine(
orig.offset(0f, 0f), // which is of course the same as: orig
orig.offset(size.width - 200f, 0f),
axisPaint
)
I guess the question and answer are kind of subjective, but in my opinion do the extensions not add anything. And actually make it unnecessarily complex.
Just writing
it.drawLine(
Offset(x = orig.x + 0f, y = orig.y + 0f),
Offset(x = orig.x + (size.width- 200), y = orig.y - 0f),
axisPaint
)
is better in my opinion. Also no point to add 0f to it so for this example I would just go with
it.drawLine(
Offset(x = orig.x, y = orig.y),
Offset(x = orig.x + (size.width- 200), y = orig.y),
axisPaint
)
I wrote this in Kotlin:
fun fromLists(cells: List<List<Double>>): Matrix {
return Matrix(cells.stream()
.map { x -> x.toDoubleArray() }
.toArray { i: Int -> Array(i, { k: Int -> DoubleArray(k) }) } )
}
Is there any way to reduce repetition in this code?
(Matrix itself is uninteresting, it just wraps an Array<DoubleArray>)
val ex1: Array<DoubleArray> = cells.map { it.toDoubleArray() }.toTypedArray()
// this should be faster, it doesn't create extra List like the previous example
val ex2: Array<DoubleArray> = Array(cells.size) { i -> cells[i].toDoubleArray() }
In my app i'm executing two for loops however those for loops need to scheduled in a order here is the use case:
There are two for loops :
1- ImageStickerslist
2-TextStickerslist
What i want to do is after imagestickerslist if properly finised only then textstickerslist will be executed.
Here imagesticker list consists of url path which is used to load images from glide however if those images are of high resolution it eventually makes the thread continue even if the image is not yet loaded from url. To solve this tried adding blocking calls to glide on ready and full method but it won't prove to be of any help. I'm very confused how blocking calls work any help into this will be really appreciated.
Here's my code where for loops are executed:
runBlocking {
launch {
imagestickers.forEach {
runBlocking {
var image = it.path
var x = it.x
var y = it.y
image!!.log()
setimagestickers(image!!, x!!, y!!, it.width!!, it.height!!)
}
}
}.join()
textstickers.forEach {
runBlocking {
var text = it.text.toString()
var color = it.color
var font = it.font
var size = it.size
var x = it.x
var y = it.y
setTextSticker(text, Color.parseColor(color), size!!, x!!, y!!)
}
}
}
Here are my two methods where main computation is taking place:
fun setimagestickers(path:String,x:Int,y:Int,w:Int,h:Int){
Glide.with(this#NewStickerActivity).asBitmap().timeout(6000000).load(path).into(object : CustomTarget<Bitmap>() {
override fun onLoadCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
var size: ViewGroup.LayoutParams
var bmp1 = resource
size = UiHelper.getHeightWidth(this#NewStickerActivity, (w).toInt(), (h).toInt())
var resizedBitmap = Bitmap.createScaledBitmap(bmp1, size.width, size.height, false)
var drawable = BitmapDrawable(resources, resizedBitmap)
var dsImageSticker = DrawableSticker(drawable)
dsImageSticker.setTag("ImageSticker")
var pm: List<Int>
if (density > 3.0) {
pm = UiHelper.getmargins(this#NewStickerActivity, (x).toInt(), (y).toInt())
} else {
pm = UiHelper.getmargins(this#NewStickerActivity, (x).toInt(), (y).toInt())
}
Log.i("Hmmm:", pm.get(0).toFloat().toString() + "::" + pm.get(1).toFloat().toString())
stickerView.addStickerAndSetMatrix1(
dsImageSticker,
pm.get(0).toFloat(),
pm.get(1).toFloat()
)
}
})
}
fun setTextSticker(text: String, color: Int,size: Int, x: Int, y: Int){
val bmp1: Bitmap
val drawable: Drawable
var l: List<Int>
if (density > 3.0) {
l = UiHelper.getmargins(this#NewStickerActivity, (x).toInt(), (y * 1.07).toInt())
} else {
l = UiHelper.getmargins(this#NewStickerActivity, x.toInt(), y.toInt())
}
//var tf = Typeface.createFromFile(assets,"fonts/"+path)
var tf = Typeface.createFromAsset(assets, "fonts/Myriad Pro Bold SemiExtended.ttf")
bmp1 = createBitmapFromLayoutWithText(this#NewStickerActivity, size, text, color, 0f, tf, 0f, 0f, color, Gravity.LEFT)
drawable = BitmapDrawable(resources, bmp1)
var dsTextSticker = DrawableSticker(drawable)
dsTextSticker.setTag("textSticker")
Log.i("Hmmm:", l.get(0).toFloat().toString() + "::" + l.get(1).toFloat().toString())
/*if (rotate) {
stic.addStickerAndSetrotate(
dsTextSticker, rotationdegress,
l.get(0).toFloat(),
l.get(1).toFloat()
)
} else {*/
stickerView.addStickerAndSetMatrix1(
dsTextSticker,
l.get(0).toFloat(),
l.get(1).toFloat())
}
UPDATE:
I got this working without coroutines by incrementing and fetching images in a sequence:
Firstly i took a Int and then kept incrementing until it reached list size here is my code:
Firstly i did this :
var i = 0
setimagestickers(imagestickers.get(i).path!!, imagestickers.get(i).x!!, imagestickers.get(i).y!!, imagestickers.get(i).width!!, imagestickers.get(i).height!!)
After that inside on resource ready did the trick!!
Glide.with(this#NewStickerActivity).asBitmap().timeout(6000000).load(path).into(object : CustomTarget<Bitmap>() {
override fun onLoadCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
var size: ViewGroup.LayoutParams
var bmp1 = resource
size = UiHelper.getHeightWidth(this#NewStickerActivity, (w).toInt(), (h).toInt())
var resizedBitmap = Bitmap.createScaledBitmap(bmp1, size.width, size.height, false)
var drawable = BitmapDrawable(resources, resizedBitmap)
var dsImageSticker = DrawableSticker(drawable)
dsImageSticker.setTag("ImageSticker")
var pm: List<Int>
if (density > 3.0) {
pm = UiHelper.getmargins(this#NewStickerActivity, (x).toInt(), (y).toInt())
} else {
pm = UiHelper.getmargins(this#NewStickerActivity, (x).toInt(), (y).toInt())
}
Log.i("Hmmm:", pm.get(0).toFloat().toString() + "::" + pm.get(1).toFloat().toString())
stickerView.addStickerAndSetMatrix1(
dsImageSticker,
pm.get(0).toFloat(),
pm.get(1).toFloat()
)
i++
if(i < imagestickers.size){
setimagestickers(imagestickers.get(i).path!!, imagestickers.get(i).x!!, imagestickers.get(i).y!!, imagestickers.get(i).width!!, imagestickers.get(i).height!!)
}
else{
if(textstickers.isNullOrEmpty()){
loader!!.hide()
}
else {
setTextSticker(textstickers.get(j).text!!, Color.parseColor(textstickers.get(j).color), textstickers.get(j).size!!, textstickers.get(j).x!!, textstickers.get(j).y!!)
}
}
}
})
However i'm still wondering how can i solve it with coroutines rather than this approach!!!
Basically, you're making async Glide calls without suspending the coroutine until they are done. The first thing you must change is introduce a suspend fun getImageSticker():
suspend fun getSticker(path: String): Bitmap =
suspendCancellableCoroutine { continuation -> Glide
.with(this#NewStickerActivity)
.asBitmap()
.timeout(6000000)
.load(path)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, x: Transition<in Bitmap>?) {
continuation.resume(resource)
}
})
}
Note that this returns a Bitmap. Now you can just call it as if it was a regular, blocking function, and use its result from the caller side:
suspend fun setimagestickers(path: String, x: Int, y: Int, w: Int, h: Int) {
val resource = getSticker(path)
var size: ViewGroup.LayoutParams
var bmp1 = resource
size = UiHelper.getHeightWidth(this#NewStickerActivity, (w).toInt(), (h).toInt())
var resizedBitmap = Bitmap.createScaledBitmap(bmp1, size.width, size.height, false)
}
Now, if you want to parallelize setimagestickers calls, launch them in a loop:
launch {
imagestickers.map {
launch {
var image = it.path
var x = it.x
var y = it.y
image!!.log()
setimagestickers(image!!, x!!, y!!, it.width!!, it.height!!)
}
}.joinAll()
textstickers.forEach {
...
}
}
None of your code will benefit from runBlocking or the coroutines you launch.
If you are aiming to simply process each of your Image type objects in paralellel due to the long running nature of each task, then you can do something like this:
fun main() {
runBlocking {
imagestickers.map {
launch {
var image = it.path
var x = it.x
var y = it.y
image!!.log()
setimagestickers(image!!, x!!, y!!, it.width!!, it.height!!)
}
}.joinAll()
textstickers.map {
launch {
var text = it.text.toString()
var color = it.color
var font = it.font
var size = it.size
var x = it.x
var y = it.y
setTextSticker(text, Color.parseColor(color), size!!, x!!, y!!)
}
}.joinAll()
}
}
Why I removed some of the stuff you were doing:
The nested runBlocking calls you had did nothing. `runBlocking means "block until whatever is inside this block is finished". Your for loops were not async, and would run until they were done anyway.
The first coroutine launch you had did nothing as well. You launched it, and then immediately called join. This means that you launch a coroutine, but then immediately wait until it is finished before proceeding.
What this does:
Rather than loops, we map each image object to a list of jobs for concurrently running coroutines.
We then wait for every job to finish in each list before proceeding to the next list.
Please Note: It does not look like any of your code benefits from having non-blocking IO, in which case, this parralelism will be limited to the amount of threads you have. That fetching of the image will block the thread it is on (regardless of using coroutines) unless you use a non-blocking library that properly yields while it fetches your image.
I've been playing around with ojAlgo and I've been pretty thrilled with it so far. I've worked through a few studies with it but I'm having trouble with this problem described in this article.
I'm using Kotlin instead of Java, but that shouldn't cause any issues. I'm stuck trying to input an expression into my model but bounding on a variable rather than a literal numeric value. How do I input that?
Here is my work so far:
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
fun main(args: Array<String>) {
val model = ExpressionsBasedModel()
val ingredients = sequenceOf(
Ingredient("Pork", 4.32, 30),
Ingredient("Wheat", 2.46, 20),
Ingredient("Starch", 1.86, 17)
).map { it.name to it }
.toMap()
val sausageTypes = sequenceOf(
SausageType("Economy", .40),
SausageType("Premium", .60)
).map { it.description to it }
.toMap()
// Map concatenated string keys to variables
val variables = ingredients.values.asSequence().flatMap { ingredient ->
sausageTypes.values.asSequence()
.map { type -> Combo(ingredient,type)}
}.map { it.toString() to Variable.make(it.toString()).lower(0).weight(it.ingredient.cost) }
.toMap()
// add variables to model
model.addVariables(variables.values)
// Pe + We + Se = 350 * 0.05
model.addExpression("EconomyDemand").level(350.0 * 0.05).apply {
set(variables["Pork-Economy"], 1)
set(variables["Wheat-Economy"], 1)
set(variables["Starch-Economy"], 1)
}
// Pp + Wp + Sp = 500 * 0.05
model.addExpression("PremiumDemand").level(500.0 * 0.05).apply {
set(variables["Pork-Premium"], 1)
set(variables["Wheat-Premium"], 1)
set(variables["Starch-Premium"], 1)
}
// Pe >= 0.4(Pe + We + Se)
// compile error?
model.addExpression("EconomyGovRestriction").upper(variables["Pork-Economy"]).apply {
set(variables["Pork-Economy"], .4)
set(variables["Wheat-Economy"], .4)
set(variables["Starch-Economy"], .4)
}
}
data class Combo(val ingredient: Ingredient, val sausageType: SausageType) {
override fun toString() = "$sausageType-$ingredient"
}
data class SausageType(val description: String, val porkRequirement: Double) {
override fun toString() = description
}
data class Ingredient(val name: String, val cost: Double, val availability: Int) {
override fun toString() = name
}
For future readers, here is the full working solution I came up with.
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.math.RoundingMode
fun main(args: Array<String>) {
val model = ExpressionsBasedModel()
val ingredients = sequenceOf(
Ingredient("Pork", 4.32, 30),
Ingredient("Wheat", 2.46, 20),
Ingredient("Starch", 1.86, 17)
).map { it.name to it }
.toMap()
val sausageTypes = sequenceOf(
SausageType("Economy", .40),
SausageType("Premium", .60)
).map { it.description to it }
.toMap()
// Map concatenated string keys to variables
val variables = ingredients.values.asSequence().flatMap { ingredient ->
sausageTypes.values.asSequence()
.map { type -> Combo(ingredient,type)}
}.map { it.toString() to Variable.make(it.toString()).lower(0).weight(it.ingredient.cost) }
.toMap()
// add variables to model
model.addVariables(variables.values)
// Pe + We + Se = 350 * 0.05
model.addExpression("EconomyDemand").level(17.5).apply {
set(variables["Pork-Economy"], 1)
set(variables["Wheat-Economy"], 1)
set(variables["Starch-Economy"], 1)
}
// Pp + Wp + Sp = 500 * 0.05
model.addExpression("PremiumDemand").level(25).apply {
set(variables["Pork-Premium"], 1)
set(variables["Wheat-Premium"], 1)
set(variables["Starch-Premium"], 1)
}
// Pe >= 0.4(Pe + We + Se)
model.addExpression("EconomyPorkRatio").upper(0.0).apply {
set(variables["Pork-Economy"], -0.6)
set(variables["Wheat-Economy"], .4)
set(variables["Starch-Economy"], .4)
}
// Pe >= 0.6(Pp + Wp + Sp)
model.addExpression("PremiumPorkRatio").upper(0.0).apply {
set(variables["Pork-Premium"], -0.4)
set(variables["Wheat-Premium"], .6)
set(variables["Starch-Premium"], .6)
}
// Se <= .25(Pe + We + Se)
// Sp <= .25(Pp + Wp + Sp)
sausageTypes.values.forEach {
model.addExpression("${it}StarchRestriction").lower(0.0).apply {
set(variables["Pork-$it"], .25)
set(variables["Wheat-$it"], .25)
set(variables["Starch-$it"], -0.75)
}
}
// Pe + Pp <= 30
// We + Wp <= 20
// Se + Sp <= 17
ingredients.values.forEach { ingredient ->
model.addExpression("${ingredient}SupplyConstraint").upper(ingredient.availability).apply {
sausageTypes.values.forEach { sausageType ->
set(variables["$ingredient-$sausageType"], 1)
}
}
}
// Pe + Pp >= 23
model.addExpression("ContractPorkRestriction").lower(23).apply {
set(variables["Pork-Economy"], 1)
set(variables["Pork-Premium"], 1)
}
// go!
val result = model.minimise()
println("OPTIMIZED COST: ${result.value}")
model.variables.asSequence()
.map { it.name }
.zip(result.asSequence().map { it.setScale(3, RoundingMode.HALF_DOWN) })
.forEach(::println)
}
data class Combo(val ingredient: Ingredient, val sausageType: SausageType) {
override fun toString() = "$ingredient-$sausageType"
}
data class SausageType(val description: String, val porkRequirement: Double) {
override fun toString() = description
}
data class Ingredient(val name: String, val cost: Double, val availability: Int) {
override fun toString() = name
}
OUTPUT:
OPTIMIZED COST: 140.955
(Pork-Economy, 8.000)
(Pork-Premium, 15.000)
(Wheat-Economy, 5.125)
(Wheat-Premium, 3.750)
(Starch-Economy, 4.375)
(Starch-Premium, 6.250)
You can't do that. You can't directly model expr1 >= expr2. Instead you have to model (expr1 - expr2) >= 0. There is an example on the ojAlgo wiki describing how to model a similar problem: https://github.com/optimatika/ojAlgo/wiki/The-Diet-Problem
What will be the syntax of creating a 3D matrix in Kotlin. It's Java equivalent is as follows:
public static final int[][][] data = {{{0,0},{0}},{{0,1},{0}},{{1,0},{0}},{{1,1},{1}}};
Thanks
Edit:
Also how can I print the Kotlin code using the simple println?
When working with arrays in most languages I find it nice to create a helper class, rather than working directly with an int[][][] type. This way you can ensure certain invariants hold (such as all rows having the same length), and ensure better data locality. It can also let you efficiently implement certain operations such as slicing, sub-matrices, transpose etc.
My usual set of classes would look something like this for 3D. (though I'd probably template on the stored type, rather than hard code it for Int)
Its pretty incomplete, but the main at the end shows how many of the functions work.
But to show how you can create a 3D array from values you can do
val V = /* .. as in mEQ5aNLrK3lqs3kfSa5HbvsTWe0nIu's answer */
val M = Matrix3D(NX,NY,NZ).transform( { v, ix, iy, iz -> V[ix][iy][iz] } )
Further examples are
fun main(args: Array<String>) {
// Create an empty matrix
val v = Matrix3D(4,4,2);
// We can access elements via [a,b,c] or [a][b][c]
v[0,1,1] = 7;
print(v)
println("v[0,1,1]=" + v[0,1,1])
println("v[0][1][1]=" + v[0][1][1])
println("-----")
// Make the matrix a little more interesting
v.transform({ w,ix,iy,iz -> ix+iy+iz})
print(v)
println("-----")
// Transform just the slice with ix=2
// Slices are fast, as they copy no elements.
// but if you change them you change the original
v[2].transform({w,iy,iz -> w+3})
print(v)
// If you dont want to change the original you can always
// create an independent copy
print(v[2].bake().transform({w,iy,iz -> w-3}))
println("-----")
// W is the slice of v with ix=0
// Can easily extend the slicing options to allow slicing along
// any axis - I'd like to add v[_,1,_] to mean the slice with iy=1
// but I've not got to that yet.
val W = v[0]
print("W=\n")
print(v[0])
print("W^T=\n")
// Fast transpose, no elements are copied.
val WT=v[0].transpose()
print(WT)
// Changing the transpose slice writes back into the original
WT[1,1]=5
print(V)
}
fun print(M:Matrix3D) {
for(iz in 0..(M.nz-1)) {
for(iy in 0..(M.ny-1)) {
for(ix in 0..(M.nx-1)){
print("%d ".format(M[ix,iy,iz]))
}
print("\n")
}
print("\n")
}
}
fun print(M:Matrix2D) {
for(iy in 0..(M.ny-1)) {
for(ix in 0..(M.nx-1)){
print("%d ".format(M[ix,iy]))
}
print("\n")
}
}
The library code looks like this:
class Matrix1D(
val v:Array<Int>,
val nx:Int,
val offset:Int,
val xstride:Int) {
// TODO: Check that the nx,offset,strides etc are valid
constructor(nx:Int) : this(Array(nx,{i->0}), nx, 0, 1) {
}
fun offsetof(ix:Int):Int {
return offset + ix*xstride
}
operator fun get(ix:Int): Int {
return v[offsetof(ix)]
}
operator fun set(ix:Int, v:Int) {
this.v[offsetof(ix)] = v
}
fun reverse() : Matrix1D {
return Matrix1D(v, nx, offsetof(nx-1), -xstride)
}
fun submatrix(startx:Int, newNX:Int) : Matrix1D {
return Matrix1D(v,newNX,offsetof(startx), xstride)
}
fun transform(body: (Int, Int) -> Int ) {
for(ix in 0..(nx-1)){
this[ix] = body(this[ix], ix)
}
}
fun bake() : Matrix1D {
val rv = Matrix1D(nx);
for(ix in 0..(nx-1)) {
rv[ix] = this[ix]
}
return rv
}
}
class Matrix2D(
val v:Array<Int>,
val nx:Int, val ny:Int,
val offset:Int,
val xstride:Int, val ystride:Int) {
// TODO: Check that the nx,ny,offset,strides etc are valid
constructor(nx:Int, ny:Int) : this(Array(nx*ny,{i->0}), nx, ny, 0, 1, nx ) {
}
fun offsetof(ix:Int,iy:Int): Int {
return offset + ix*xstride + iy*ystride
}
operator fun get(ix:Int,iy:Int): Int {
return v[offsetof(ix,iy)]
}
operator fun set(ix:Int,iy:Int,v:Int) {
this.v[offsetof(ix,iy)] = v
}
operator fun get(ix:Int): Matrix1D {
return Matrix1D(v, ny, offsetof(ix,0), ystride)
}
fun transpose(): Matrix2D {
return Matrix2D(v,ny,nx,offset,ystride,xstride)
}
fun submatrix(startx:Int, starty:Int, newNX:Int, newNY:Int) : Matrix2D {
return Matrix2D(v,newNX,newNY,offsetof(startx,starty), xstride, ystride)
}
fun transform(body: (Int, Int, Int) -> Int ) {
for(iy in 0..(ny-1)) {
for(ix in 0..(nx-1)){
this[ix,iy] = body(this[ix,iy], ix,iy)
}
}
}
fun bake() : Matrix2D {
val rv = Matrix2D(nx,ny);
for(ix in 0..(nx-1)) {
for(iy in 0..(ny-1)) {
rv[ix,iy] = this[ix,iy]
}
}
return rv
}
}
class Matrix3D(
val v:Array<Int>,
val nx:Int, val ny:Int, val nz:Int,
val offset:Int,
val xstride:Int, val ystride:Int, val zstride:Int) {
// TODO: Check that the nx,ny,nz,offset,strides etc are valid
constructor(nx:Int, ny:Int, nz:Int) : this(Array(nx*ny*nz,{i->0}), nx, ny, nz, 0, 1, nx, nx*ny ) {
}
operator fun get(ix:Int,iy:Int,iz:Int): Int {
return v[offset + ix*xstride + iy*ystride + iz*zstride]
}
operator fun set(ix:Int,iy:Int,iz:Int, v:Int) {
this.v[offset + ix*xstride + iy*ystride + iz*zstride] = v
}
operator fun get(ix:Int): Matrix2D {
return Matrix2D(v, ny, nz, offset + ix*xstride, ystride, zstride )
}
fun transform(body: (Int, Int, Int, Int) -> Int ) {
for(iz in 0..(nz-1)) {
for(iy in 0..(ny-1)) {
for(ix in 0..(nx-1)){
this[ix,iy,iz] = body(this[ix,iy,iz], ix,iy,iz)
}
}
}
}
fun bake() : Matrix3D {
val rv = Matrix3D(nx,ny,nz);
for(ix in 0..(nx-1)) {
for(iy in 0..(ny-1)) {
for(iz in 0..(nz-1)){
rv[ix,iy,iz] = this[ix,iy,iz]
}
}
}
return rv
}
}
Kotlin currently does not support array literals.
You can use a combination of arrayOf() and intArrayOf():
val data = arrayOf(
arrayOf(intArrayOf(0, 0), intArrayOf(0)),
arrayOf(intArrayOf(0, 1), intArrayOf(0)),
arrayOf(intArrayOf(1, 0), intArrayOf(0)),
arrayOf(intArrayOf(1, 1), intArrayOf(1))
)
You can cut down a little bit on the verbosity using import aliasing if needed:
import kotlin.arrayOf as arr
import kotlin.intArrayOf as iarr
val data = arr(
arr(iarr(0, 0), iarr(0)),
arr(iarr(0, 1), iarr(0)),
arr(iarr(1, 0), iarr(0)),
arr(iarr(1, 1), iarr(1))
)
Also note that you can auto-convert Java code to Kotlin
in IntelliJ IDEA: copy Java code into a Kotlin file, a confirmation prompt will open.
online: using http://try.kotlinlang.org.
Using Multik
Multik Multidimensional array library for Kotlin.
Syntax for creating 3D array
mk.d3array(2, 2, 3) { it * it }
//output
/*[[[0, 1, 4],
[9, 16, 25]],
[[1, 0, 0],
[1, 1, 1]]]
*/
Note:Multik supports up to 4 dimensions
The Multik project’s GitHub repository
For more info check Jetbrain blog post