Custom Span with path duplicates the path - android-canvas

I want to draw a curved underline for TextView Spans, so I have a custom span and textview as follows:
class UnderscoreSpan(context: Context) : ReplacementSpan() {
private val underscoreHeight = context.resources.getDimension(R.dimen.underscoreHeight)
private val vectorPath = Path()
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: FontMetricsInt?
): Int {
return measureText(paint, text, start, end).roundToInt()
}
private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float {
return paint.measureText(text, start, end)
}
override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
val w = measureText(paint, text, start, end)
val h = bottom.toFloat() - top.toFloat()
paint.color = Color.RED
paint.strokeWidth = underscoreHeight
paint.style = Paint.Style.STROKE
vectorPath.moveTo(x, bottom.toFloat() - underscoreHeight)
vectorPath.cubicTo(
x + w / 2,
bottom - underscoreHeight - h / 10,
x + w - w / 5,
bottom - underscoreHeight + h / 20,
x + w,
bottom - underscoreHeight,
)
canvas.save()
canvas.drawPath(vectorPath, paint)
paint.color = Color.BLACK
paint.style = Paint.Style.FILL
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
canvas.drawText(text, start, end, x, y.toFloat(), paint)
canvas.restore()
}
}
class UnderscoreSpanTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
fun setSpannableText(text: String) {
val start = text.findAnyOf(listOf("<b>"))?.first ?: 0
val temp = text.replace("<b>", "")
val end = temp.findAnyOf(listOf("</b>"))?.first ?: 0
val finalText = temp.replace("</b>", "")
val spannable = SpannableString(finalText)
spannable.setSpan(
UnderscoreSpan(context),
start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
super.setText(spannable)
}
}
However, the path is drawn twice with a few pixels offset. If I don't use a path (only drawRect for example), then it's fine. With the path however, it's always double, even if I close the path and don't use stroke. What am I doing wrong?

On the second onDraw call I failed to reset the path variable, so it was redrawn. So all it needed was a path.reset() call at the beginning of draw().

Related

Optimise grouping 512x512 coordinates into 117 smaller, quartered chunks

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),
)
}
}
}

Kotlin Sorting a list of objects based on their coordinate( lat and long) using Haversine formula

I want to sort a list based on their latitude and longitude...
Here is my code:
import java.util.*
import com.google.gson.GsonBuilder
import java.io.File
import java.io.InputStream
import java.util.Comparator
data class Property(val Pcode: Int, val Locality: String, val State: String, val Comments: String, val Category: String, val Longitude: Double, val Latitude: Double)
class SortPlaces(currentLatitude: Double, currentLongitude: Double) : Comparator<Property> {
var currentLat: Double
var currentLng: Double
override fun compare(property1: Property, property2: Property): Int {
val lat1: Double = property1.Latitude
val lon1: Double = property1.Longitude
val lat2: Double = property2.Latitude
val lon2: Double = property2.Longitude
val distanceToPlace1 = distance(currentLat, currentLng, lat1, lon1)
val distanceToPlace2 = distance(currentLat, currentLng, lat2, lon2)
return (distanceToPlace1 - distanceToPlace2).toInt()
}
fun distance(fromLat: Double, fromLon: Double, toLat: Double, toLon: Double): Double {
val radius = 6378137.0 // approximate Earth radius, *in meters*
val deltaLat = toLat - fromLat
val deltaLon = toLon - fromLon
val angle = 2 * Math.asin(
Math.sqrt(
Math.pow(Math.sin(deltaLat / 2), 2.0) +
Math.cos(fromLat) * Math.cos(toLat) *
Math.pow(Math.sin(deltaLon / 2), 2.0)
)
)
return radius * angle
}
init {
currentLat = currentLatitude
currentLng = currentLongitude
}
}
fun main(args: Array<String>) {
val command = Scanner(System.`in`)
val running = true
while (running) {
val inputStream: InputStream = File("./src/main/kotlin/suburbs.json").inputStream()
val inputString = inputStream.bufferedReader().use { it.readText() }
val gson = GsonBuilder().create()
val packagesArray = gson.fromJson(inputString , Array<Property>::class.java).toList()
println("Please enter a suburb name: ")
val suburbName = command.nextLine()
println("Please enter the postcode: ")
val postcode = command.nextLine()
val userProperty: Property? = packagesArray.find{ it.Locality.toLowerCase().equals(suburbName.toLowerCase()) && it.Pcode == postcode.toInt()}
//sort the list, give the Comparator the current location
Collections.sort(packagesArray, new SortPlaces(userProperty.Latitude, userProperty.Longitude));
}
command.close()
}
I got error: Too many arguments for public open fun <T : Comparable<T!>!> sort(list: (Mutable)List<T!>!): Unit defined in java.util.Collections
at my sort{} function
my userProperty has to be Property? because the find{} method return Property?
Then Collections.sort() can not sort Property? type because the SortPLaces only accept Comparator not Comparator<Property?>
What should I do?
There are multiple errors in your code. To create a new object in Kotlin, you don't write the word new like you do in Java. Also, as you have noticed, find returns a nullable type - Property?. You need to check for nulls when using userProperty. A Property matching the criteria you want may not necessarily be found, after all.
if (userProperty != null) {
Collections.sort(packagesArray, SortPlaces(userProperty.Latitude, userProperty.Longitude))
} else {
// no property is found! Think about what you should do in such a case
}
Since you are sorting the list in line, you should not make an immutable list with toList when you are deserialising the JSON, but rather a MutableList:
val packagesArray = gson.fromJson(inputString, Array<Property>::class.java).toMutableList()
Also, you seem to be using a lot of Java APIs. In Kotlin, a lot of the Java APIs that you are using have more idiomatic Kotlin counterparts. To sort the list, you don't need the SortPlaces class at all. Simply use sortBy on the array, and call your distance function in the lambda.
data class Property(
val pcode: Int,
val locality: String,
val state: String,
val comments: String,
val category: String,
val longitude: Double,
val latitude: Double,
)
fun distance(fromLat: Double, fromLon: Double, toLat: Double, toLon: Double): Double {
val radius = 6378137.0 // approximate Earth radius, *in meters*
val deltaLat = toLat - fromLat
val deltaLon = toLon - fromLon
val angle = 2 * asin(
sqrt(
sin(deltaLat / 2).pow(2.0) +
cos(fromLat) * cos(toLat) *
sin(deltaLon / 2).pow(2.0)
)
)
return radius * angle
}
fun main(args: Array<String>) {
val running = true
while (running) {
val inputStream = File("./src/main/kotlin/suburbs.json").inputStream()
val inputString = inputStream.bufferedReader().use { it.readText() }
val gson = GsonBuilder().create()
val packagesArray = gson.fromJson(inputString , Array<Property>::class.java).toMutableList()
println("Please enter a suburb name: ")
val suburbName = readLine()
println("Please enter the postcode: ")
val postcode = readLine()
val userProperty = packagesArray.find {
it.locality.lowercase() == suburbName?.lowercase() && it.pcode == postcode?.toInt()
}
//sort the list, give the Comparator the current location
if (userProperty != null) {
packagesArray.sortBy {
distance(userProperty.latitude, userProperty.longitude, it.latitude, it.longitude)
}
} else {
// did not find such a property!
}
}
}

Kotlin - Random.nextInt Range

Aim of code: Shopping system,function which shows a matched product name from the warehouse
what is the no. range of Random.nextInt() if no no. is assigned inside ()?
in fun fillWarehouse, if i do not set no. inside "StockUnit(Random.nextInt(),Random.nextInt())", when i call println("Number of items: ${p.availableItems}") in main, No. -890373473 / 1775292982 etc. were generated.
if i set 100 inside like "StockUnit(Random.nextInt(100),Random.nextInt(100))", No. 263 / 199 etc. were generated. why is it not within 0-100? may i know how to change my code, so that "Number of items" is within 100?
any links or topics should i work for, to write better code?
i cannot find the answers from https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/
Sincere thanks!
fun main(args: Array<String>) {
val warehouse = Warehouse()
...
println("Show info")
showInfo(warehouse)
}
fun showInfo(warehouse: Warehouse) {
println("Get Info")
val input = readLine() ?: "-"
val p = warehouse.getProductByName(input)
if (p != null) {
println("Product: $p")
println("Number of items: ${p.availableItems}")
println("Profit: ${p.profitPerItem}")
}
}
class Warehouse {
private val products = mutableListOf<Product>()
...
fun getProductByName (productName: String): Product? {
for (prod in products)
if (prod.productName == productName) return prod
return null
}
fun fillWarehouse (productName: String,
basePrice: Double,
productDescription: String,
chargeOnTop: Double = 50.0,
intialStockUnits: Int = 3) {
val newProduct = Product(productName, basePrice, basePrice * (1 + chargeOnTop / 100), productDescription)
//add quantity, daysBeforeExpiration
for (i in 1 .. intialStockUnits){
val unit = StockUnit(Random.nextInt(),Random.nextInt() )
newProduct.addStock(unit)
}
open class Product(
val productName: String,
var basePrice: Double,
open val salesPrice: Double,
val description: String) {
...
var stockUnits = mutableListOf<StockUnit>()
...
// availableItems = Total of stockUnits
var availableItems: Int = 0
get() = stockUnits.sumBy { it.quantity }
}
class StockUnit(var quantity:Int, var daysBeforeExpiration:Int){
...
}

How do I bind a custom property to a textfield bidirectionally?

I have a complex object that I want to display in a textfield. This is working fine with a stringBinding. But I don't know how to make it two-way so that the textfield is editable.
package com.example.demo.view
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
class MainView : View("Hello TornadoFX") {
val complexThing: Int = 1
val complexProperty = SimpleObjectProperty<Int>(complexThing)
val complexString = complexProperty.stringBinding { complexProperty.toString() }
val plainString = "asdf"
val plainProperty = SimpleStringProperty(plainString)
override val root = vbox {
textfield(complexString)
label(plainProperty)
textfield(plainProperty)
}
}
When I run this, the plainString is editable and I see the label change because the edits are going back into the property.
How can I write a custom handler or what class do I need to use to make the stringBinding be read and write? I looked through a lot of the Property and binding documentation but did not see anything obvious.
Ta-Da
class Point(val x: Int, val y: Int) //You can put properties in constructor
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
if(string.isNullOrBlank()) return null //Empty strings aren't valid
val xy = string.split(",", limit = 2) //Only using 2 coordinate values so max is 2
if(xy.size < 2) return null //Min values is also 2
val x = xy[0].trim().toIntOrNull() //Trim white space, try to convert
val y = xy[1].trim().toIntOrNull()
return if(x == null || y == null) null //If either conversion fails, count as invalid
else Point(x, y)
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6) //Probably doesn't need to be its own member
val pointProperty = SimpleObjectProperty<Point>(point)
val pc = PointConverter()
override val root = vbox {
label(pointProperty, converter = pc) //Avoid extra properties, put converter in construction
textfield(pointProperty, pc)
}
}
I made edits to your converter to "account" for invalid input by just returning null. This is just a simple band-aid solution that doesn't enforce correct input, but it does refuse to put bad values in your property.
This can probably be done more cleanly. I bet there is a way around the extra property. The example is fragile because it doesn't do input checking in the interest of keeping it simple. But it works to demonstrate the solution:
class Point(x: Int, y: Int) {
val x: Int = x
val y: Int = y
}
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
val xy = string?.split(",")
return Point(xy[0].toInt(), xy[1].toInt())
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6)
val pointProperty = SimpleObjectProperty<Point>(point)
val pointDisplayProperty = SimpleStringProperty()
val pointStringProperty = SimpleStringProperty()
val pc = PointConverter()
init {
pointDisplayProperty.set(pc.toString(pointProperty.value))
pointStringProperty.set(pc.toString(pointProperty.value))
pointStringProperty.addListener { observable, oldValue, newValue ->
pointProperty.set(pc.fromString(newValue))
pointDisplayProperty.set(pc.toString(pointProperty.value))
}
}
override val root = vbox {
label(pointDisplayProperty)
textfield(pointStringProperty)
}
}

doubleBinding has no effect

I'm taking my first experiment with TornadoFX and ran into a problem I don't understand.
I have an object Wind:
enum class Direction(val displayName: String, val abbrevation: String, val deltaX: Int, val deltaY: Int) {
NORTH("Észak", "É", 0, -1),
NORTH_EAST("Északkelet", "ÉK", 1, -1),
EAST("Kelet", "K", 1, 0),
SOUTH_EAST("Délkelet", "DK", 1, 1),
SOUTH("Dél", "D", 0, 1),
SOUTH_WEST("Délnyugat", "DNy", -1, 1),
WEST("Nyugat", "Ny", -1, 0),
NORTH_WEST("Északnyugat", "ÉNy", -1, -1);
val diagonal: Boolean = deltaX != 0 && deltaY != 0
val degree: Double = ordinal * 45.0
fun turnClockwise(eighth: Int = 1) = values()[(ordinal + eighth) umod 8]
fun turnCounterClockwise(eighth: Int = 1) = values()[(ordinal - eighth) umod 8]
fun turn(eighth: Int = 1) = if (eighth < 0) turnCounterClockwise(eighth.absoluteValue) else turnClockwise(eighth)
infix operator fun plus(eighth: Int) = turn(eighth)
infix operator fun minus(eighth: Int) = turn(-eighth)
infix operator fun minus(other: Direction) = (ordinal - other.ordinal) umod 8
}
object Wind {
val directionProperty = SimpleObjectProperty<Direction>(Direction.NORTH)
var direction: Direction
get() = directionProperty.value
set(value) {
println("SET WIND: $value")
directionProperty.value = value
}
}
I would like to bound a rotation transformation to the setting of wind direction.
When I use the old, JavaFX style, it works:
rot.angleProperty().bind(
createDoubleBinding(
Callable {
println("Direction: ${Wind.direction}");
Wind.directionProperty.value.degree * 45
},
Wind.directionProperty))
When I try to use the more elegant, Kotlin-style version, it doesn't bind:
rot.angleProperty().doubleBinding(rot.angleProperty() ) {
println("Direction: ${Wind.direction}")
Wind.directionProperty.value.degree * 45
}
Did I miss something?
The doubleBinding() function creates a Binding, but it does not bind it to anything.
In fact, we have two ways to create this binding:
doubleBinding(someProperty) { ... }. The operates on the property (this) and expects you to return a Double. It is not nullable.
someProperty.doubleBinding() { ... } receives the value as a parameter and expects you to return a Double. The parameter is nullable, so you need to account for that
That leaves you with two options:
rot.angleProperty().bind(doubleBinding(Wind.directionProperty) {
value.degree * 45
})
Or
rot.angleProperty().bind(Wind.directionProperty.doubleBinding {
it?.degree ?: 0.0 * 45
})
Which one you choose is mostly a matter of taste, though one will be more natural than the other in some cases.