From d0f47bf331135c2a98a3a6ed5291229a6dbe098e Mon Sep 17 00:00:00 2001 From: "Hille, Marlon" Date: Tue, 4 Jun 2024 17:58:05 +0200 Subject: [PATCH 1/5] Kotlin implementation of https://github.com/heremaps/flexible-polyline/tree/master/java Note: There are currently still dependencies to java.util.concurrent.atomic which should be replaced with kotlin.native.concurrent or a project like https://github.com/Kotlin/kotlinx-atomicfu in case the code shall be used with a kotlin multiplatform project. Performance on JVM seems worse (about 4x) than pure Java implementation; so there is room for improvement. Signed-off-by: Hille, Marlon --- kotlin/README.md | 9 + .../here/flexiblepolyline/FlexiblePolyline.kt | 369 ++++++++++++++++++ .../flexiblepolyline/FlexiblePolylineTest.kt | 364 +++++++++++++++++ 3 files changed, 742 insertions(+) create mode 100644 kotlin/README.md create mode 100644 kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt create mode 100644 kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt diff --git a/kotlin/README.md b/kotlin/README.md new file mode 100644 index 0000000..bb6138e --- /dev/null +++ b/kotlin/README.md @@ -0,0 +1,9 @@ +# Quick compilation and testing instructions +```bash +$ kotlinc "./src/com/here/flexiblepolyline/FlexiblePolyline.kt" "./src/com/here/flexiblepolyline/FlexiblePolylineTest.kt" -include-runtime -d FlexiblePolylineText.jar +``` +to run the performance test with the default polyline length of 1000 vertices, or +```bash +$ java -jar FlexiblePolylineText.jar $POLYLINE_LENGTH +``` +to use `$POLYLINE_LENGTH` vertices for the performance test. \ No newline at end of file diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt new file mode 100644 index 0000000..946f40e --- /dev/null +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2019 HERE Europe B.V. + * Licensed under MIT, see full license in LICENSE + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ +package com.here.flexiblepolyline + +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference +/** + * The polyline encoding is a lossy compressed representation of a list of coordinate pairs or coordinate triples. + * It achieves that by: + * + * 1. Reducing the decimal digits of each value. + * 1. Encoding only the offset from the previous point. + * 1. Using variable length for each coordinate delta. + * 1. Using 64 URL-safe characters to display the result. + * + * The advantage of this encoding are the following: + * - Output string is composed by only URL-safe characters + * - Floating point precision is configurable + * - It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, elevation or some other custom value + */ +object FlexiblePolyline { + + private const val version: Int = 1 + //Base64 URL-safe characters + private val ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() + private val DECODING_TABLE = intArrayOf( + 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + ) + + /** + * Encode the list of coordinate triples.



+ * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT. + * This is lossy compression based on precision accuracy. + * + * @param coordinates [List] of coordinate triples that to be encoded. + * @param precision Floating point precision of the coordinate to be encoded. + * @param thirdDimension [ThirdDimension] which may be a level, altitude, elevation or some other custom value + * @param thirdDimPrecision Floating point precision for thirdDimension value + * @return URL-safe encoded [String] for the given coordinates. + */ + @JvmStatic + fun encode(coordinates: List?, precision: Int, thirdDimension: ThirdDimension?, thirdDimPrecision: Int): String { + require(!coordinates.isNullOrEmpty()) { "Invalid coordinates!" } + requireNotNull(thirdDimension) { "Invalid thirdDimension" } + val enc = Encoder(precision, thirdDimension, thirdDimPrecision) + val iterator = coordinates.iterator() + while (iterator.hasNext()) { + enc.add(iterator.next()) + } + return enc.getEncoded() + } + + /** + * Decode the encoded input [String] to [List] of coordinate triples.



+ * @param encoded URL-safe encoded [String] + * @return [List] of coordinate triples that are decoded from input + * + * @see PolylineDecoder.getThirdDimension + * @see LatLngZ + */ + @JvmStatic + fun decode(encoded: String?): List { + require(!(encoded == null || encoded.trim { it <= ' ' }.isEmpty())) { "Invalid argument!" } + val result: MutableList = ArrayList() + val dec = Decoder(encoded) + var lat = AtomicReference(0.0) + var lng = AtomicReference(0.0) + var z = AtomicReference(0.0) + while (dec.decodeOne(lat, lng, z)) { + result.add(LatLngZ(lat.get(), lng.get(), z.get())) + lat = AtomicReference(0.0) + lng = AtomicReference(0.0) + z = AtomicReference(0.0) + } + return result + } + + /** + * ThirdDimension type from the encoded input [String] + * @param encoded URL-safe encoded coordinate triples [String] + * @return type of [ThirdDimension] + */ + @JvmStatic + fun getThirdDimension(encoded: String): ThirdDimension? { + val index = AtomicInteger(0) + val header = AtomicLong(0) + Decoder.decodeHeaderFromString(encoded.toCharArray(), index, header) + return ThirdDimension.fromNum(header.get() shr 4 and 7) + } + + //Decode a single char to the corresponding value + private fun decodeChar(charValue: Char): Int { + val pos = charValue.code - 45 + return if (pos < 0 || pos > 77) { + -1 + } else DECODING_TABLE[pos] + } + + /* + * Single instance for configuration, validation and encoding for an input request. + */ + private class Encoder(precision: Int, private val thirdDimension: ThirdDimension, thirdDimPrecision: Int) { + private val result: StringBuilder = StringBuilder() + private val latConveter: Converter = Converter(precision) + private val lngConveter: Converter = Converter(precision) + private val zConveter: Converter = Converter(thirdDimPrecision) + private fun encodeHeader(precision: Int, thirdDimensionValue: Int, thirdDimPrecision: Int) { + /* + * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char + */ + require(!(precision < 0 || precision > 15)) { "precision out of range" } + require(!(thirdDimPrecision < 0 || thirdDimPrecision > 15)) { "thirdDimPrecision out of range" } + require(!(thirdDimensionValue < 0 || thirdDimensionValue > 7)) { "thirdDimensionValue out of range" } + val res = (thirdDimPrecision shl 7 or (thirdDimensionValue shl 4) or precision).toLong() + Converter.encodeUnsignedVarint(version.toLong(), result) + Converter.encodeUnsignedVarint(res, result) + } + + private fun add(lat: Double, lng: Double) { + latConveter.encodeValue(lat, result) + lngConveter.encodeValue(lng, result) + } + + private fun add(lat: Double, lng: Double, z: Double) { + add(lat, lng) + if (thirdDimension != ThirdDimension.ABSENT) { + zConveter.encodeValue(z, result) + } + } + + fun add(tuple: LatLngZ?) { + requireNotNull(tuple) { "Invalid LatLngZ tuple" } + add(tuple.lat, tuple.lng, tuple.z) + } + + fun getEncoded(): String { + return result.toString() + } + + init { + encodeHeader(precision, this.thirdDimension.num, thirdDimPrecision) + } + } + + /* + * Single instance for decoding an input request. + */ + private class Decoder(encoded: String) { + private val encoded: CharArray = encoded.toCharArray() + private val index: AtomicInteger = AtomicInteger(0) + private val latConverter: Converter + private val lngConverter: Converter + private val zConverter: Converter + private var precision = 0 + private var thirdDimPrecision = 0 + private var thirdDimension: ThirdDimension? = null + private fun hasThirdDimension(): Boolean { + return thirdDimension != ThirdDimension.ABSENT + } + + private fun decodeHeader() { + val header = AtomicLong(0) + decodeHeaderFromString(encoded, index, header) + precision = (header.get() and 15).toInt() // we pick the first 4 bits only + header.set(header.get() shr 4) + thirdDimension = ThirdDimension.fromNum(header.get() and 7) // we pick the first 3 bits only + thirdDimPrecision = (header.get() shr 3 and 15).toInt() + } + + fun decodeOne( + lat: AtomicReference, + lng: AtomicReference, + z: AtomicReference + ): Boolean { + if (index.get() == encoded.size) { + return false + } + require(latConverter.decodeValue(encoded, index, lat)) { "Invalid encoding" } + require(lngConverter.decodeValue(encoded, index, lng)) { "Invalid encoding" } + if (hasThirdDimension()) { + require(zConverter.decodeValue(encoded, index, z)) { "Invalid encoding" } + } + return true + } + + companion object { + fun decodeHeaderFromString(encoded: CharArray, index: AtomicInteger, header: AtomicLong) { + val value = AtomicLong(0) + + // Decode the header version + require(Converter.decodeUnsignedVarint(encoded, index, value)) { "Invalid encoding" } + require(value.get() == version.toLong()) { "Invalid format version" } + // Decode the polyline header + require(Converter.decodeUnsignedVarint(encoded, index, value)) { "Invalid encoding" } + header.set(value.get()) + } + } + + init { + decodeHeader() + latConverter = Converter(precision) + lngConverter = Converter(precision) + zConverter = Converter(thirdDimPrecision) + } + } + + /* + * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request. + * Instance should be specific to type of coordinates (e.g. Lat, Lng) + * so that specific type delta is computed for encoding. + * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) + */ + class Converter(precision: Int) { + private var multiplier: Long = 0 + private var lastValue: Long = 0 + private fun setPrecision(precision: Int) { + //multiplier = Math.pow(10.0, java.lang.Double.valueOf(precision.toDouble())).toLong() + multiplier = Math.pow(10.0, precision.toDouble()).toLong() + } + + fun encodeValue(value: Double, result: StringBuilder) { + /* + * Round-half-up + * round(-1.4) --> -1 + * round(-1.5) --> -2 + * round(-2.5) --> -3 + */ + val scaledValue = Math.round(Math.abs(value * multiplier)) * Math.round(Math.signum(value)) + var delta = scaledValue - lastValue + val negative = delta < 0 + lastValue = scaledValue + + // make room on lowest bit + delta = delta shl 1 + + // invert bits if the value is negative + if (negative) { + delta = delta.inv() + } + encodeUnsignedVarint(delta, result) + } + + //Decode single coordinate (say lat|lng|z) starting at index + fun decodeValue( + encoded: CharArray, + index: AtomicInteger, + coordinate: AtomicReference + ): Boolean { + val delta = AtomicLong() + if (!decodeUnsignedVarint(encoded, index, delta)) { + return false + } + if (delta.get() and 1 != 0L) { + delta.set(delta.get().inv()) + } + delta.set(delta.get() shr 1) + lastValue += delta.get() + coordinate.set(lastValue.toDouble() / multiplier) + return true + } + + companion object { + fun encodeUnsignedVarint(value: Long, result: StringBuilder) { + // TODO: check performance impact + /*val estimatedCapacity = 10000 // Adjust as needed + result.ensureCapacity(estimatedCapacity)*/ + // end TODO + var number = value + while (number > 0x1F) { + val pos = (number and 0x1F or 0x20).toByte() + result.append(ENCODING_TABLE[pos.toInt()]) + number = number shr 5 + } + result.append(ENCODING_TABLE[number.toByte().toInt()]) + } + + fun decodeUnsignedVarint( + encoded: CharArray, + index: AtomicInteger, + result: AtomicLong + ): Boolean { + var shift: Short = 0 + var delta: Long = 0 + var value: Long + while (index.get() < encoded.size) { + value = decodeChar(encoded[index.get()]).toLong() + if (value < 0) { + return false + } + index.incrementAndGet() + delta = delta or (value and 0x1F shl shift.toInt()) + if (value and 0x20 == 0L) { + result.set(delta) + return true + } else { + shift = (shift + 5).toShort() + } + // TODO: Check performance and tests + /*if (shift <= 0) { + return true + }*/ + // end TODO + } + return shift <= 0 + } + } + + init { + setPrecision(precision) + } + } + + /** + * 3rd dimension specification. + * Example a level, altitude, elevation or some other custom value. + * ABSENT is default when there is no third dimension en/decoding required. + */ + enum class ThirdDimension(val num: Int) { + ABSENT(0), LEVEL(1), ALTITUDE(2), ELEVATION(3), RESERVED1(4), RESERVED2(5), CUSTOM1(6), CUSTOM2(7); + + companion object { + fun fromNum(value: Long): ThirdDimension? { + for (dim in values()) { + if (dim.num.toLong() == value) { + return dim + } + } + return null + } + } + } + + /** + * Coordinate triple + */ + class LatLngZ @JvmOverloads constructor(val lat: Double, val lng: Double, val z: Double = 0.0) { + override fun toString(): String { + return "LatLngZ [lat=$lat, lng=$lng, z=$z]" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other is LatLngZ) { + val passed = other + if (passed.lat == lat && passed.lng == lng && passed.z == z) { + return true + } + } + return false + } + + override fun hashCode(): Int { + var result = lat.hashCode() + result = 31 * result + lng.hashCode() + result = 31 * result + z.hashCode() + return result + } + } +} \ No newline at end of file diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt new file mode 100644 index 0000000..35c7c70 --- /dev/null +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2019 HERE Europe B.V. + * Licensed under MIT, see full license in LICENSE + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ +import com.here.flexiblepolyline.* +import com.here.flexiblepolyline.FlexiblePolyline.Converter +import com.here.flexiblepolyline.FlexiblePolyline.ThirdDimension +import com.here.flexiblepolyline.FlexiblePolyline.LatLngZ +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +/** + * Validate polyline encoding with different input combinations. + */ +class FlexiblePolylineTest { + private fun testInvalidCoordinates() { + + //Null coordinates + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(null, 5, ThirdDimension.ABSENT, 0) } + + + //Empty coordinates list test + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(ArrayList(), 5, ThirdDimension.ABSENT, 0) } + } + + private fun testInvalidThirdDimension() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(50.1022829, 8.6982122)) + val invalid: ThirdDimension? = null + + //Invalid Third Dimension + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(pairs, 5, invalid, 0) } + } + + private fun testConvertValue() { + val conv: FlexiblePolyline.Converter = Converter(5) + val result = StringBuilder() + conv.encodeValue(-179.98321, result) + assertEquals(result.toString(), "h_wqiB") + } + + private fun testSimpleLatLngEncoding() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(50.1022829, 8.6982122)) + pairs.add(LatLngZ(50.1020076, 8.6956695)) + pairs.add(LatLngZ(50.1006313, 8.6914960)) + pairs.add(LatLngZ(50.0987800, 8.6875156)) + val expected = "BFoz5xJ67i1B1B7PzIhaxL7Y" + val computed: String = FlexiblePolyline.encode(pairs, 5, ThirdDimension.ABSENT, 0) + assertEquals(computed, expected) + } + + private fun testComplexLatLngEncoding() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(52.5199356, 13.3866272)) + pairs.add(LatLngZ(52.5100899, 13.2816896)) + pairs.add(LatLngZ(52.4351807, 13.1935196)) + pairs.add(LatLngZ(52.4107285, 13.1964502)) + pairs.add(LatLngZ(52.38871, 13.1557798)) + pairs.add(LatLngZ(52.3727798, 13.1491003)) + pairs.add(LatLngZ(52.3737488, 13.1154604)) + pairs.add(LatLngZ(52.3875198, 13.0872202)) + pairs.add(LatLngZ(52.4029388, 13.0706196)) + pairs.add(LatLngZ(52.4105797, 13.0755529)) + val expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e" + val computed: String = FlexiblePolyline.encode(pairs, 5, ThirdDimension.ABSENT, 0) + assertEquals(computed, expected) + } + + private fun testLatLngZEncode() { + val tuples: MutableList = ArrayList() + tuples.add(LatLngZ(50.1022829, 8.6982122, 10.0)) + tuples.add(LatLngZ(50.1020076, 8.6956695, 20.0)) + tuples.add(LatLngZ(50.1006313, 8.6914960, 30.0)) + tuples.add(LatLngZ(50.0987800, 8.6875156, 40.0)) + val expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" + val computed: String = FlexiblePolyline.encode(tuples, 5, ThirdDimension.ALTITUDE, 0) + assertEquals(computed, expected) + } + /** */ + /********** Decoder test starts */ + /** */ + private fun testInvalidEncoderInput() { + + //Null coordinates + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.decode(null) } + + + //Empty coordinates list test + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.decode("") } + } + + private fun testThirdDimension() { + assertTrue(FlexiblePolyline.getThirdDimension("BVoz5xJ67i1BU") === ThirdDimension.LEVEL) + assertTrue(FlexiblePolyline.getThirdDimension("BlBoz5xJ67i1BU") === ThirdDimension.ALTITUDE) + assertTrue(FlexiblePolyline.getThirdDimension("B1Boz5xJ67i1BU") === ThirdDimension.ELEVATION) + } + + private fun testDecodeConvertValue() { + val encoded = ("h_wqiB").toCharArray() + val expected = -179.98321 + val computed = AtomicReference(0.0) + val conv = Converter(5) + conv.decodeValue( + encoded, + AtomicInteger(0), + computed + ) + assertEquals(computed.get(), expected) + } + + private fun testSimpleLatLngDecoding() { + val computed: List = FlexiblePolyline.decode("BFoz5xJ67i1B1B7PzIhaxL7Y") + val expected: MutableList = ArrayList() + expected.add(LatLngZ(50.10228, 8.69821)) + expected.add(LatLngZ(50.10201, 8.69567)) + expected.add(LatLngZ(50.10063, 8.69150)) + expected.add(LatLngZ(50.09878, 8.68752)) + assertEquals(computed.size, expected.size) + for (i in computed.indices) { + assertEquals(computed[i], expected[i]) + } + } + + private fun testComplexLatLngDecoding() { + val computed: List = FlexiblePolyline.decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e") + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(52.51994, 13.38663)) + pairs.add(LatLngZ(52.51009, 13.28169)) + pairs.add(LatLngZ(52.43518, 13.19352)) + pairs.add(LatLngZ(52.41073, 13.19645)) + pairs.add(LatLngZ(52.38871, 13.15578)) + pairs.add(LatLngZ(52.37278, 13.14910)) + pairs.add(LatLngZ(52.37375, 13.11546)) + pairs.add(LatLngZ(52.38752, 13.08722)) + pairs.add(LatLngZ(52.40294, 13.07062)) + pairs.add(LatLngZ(52.41058, 13.07555)) + assertEquals(computed.size, pairs.size) + for (i in computed.indices) { + assertEquals(computed[i], pairs[i]) + } + } + + private fun testLatLngZDecode() { + val computed: List = FlexiblePolyline.decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU") + val tuples: MutableList = ArrayList() + tuples.add(LatLngZ(50.10228, 8.69821, 10.0)) + tuples.add(LatLngZ(50.10201, 8.69567, 20.0)) + tuples.add(LatLngZ(50.10063, 8.69150, 30.0)) + tuples.add(LatLngZ(50.09878, 8.68752, 40.0)) + assertEquals(computed.size, tuples.size) + for (i in computed.indices) { + assertEquals(computed[i], tuples[i]) + } + } + + private fun encodingSmokeTest() { + var lineNo = 0 + try { + Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "original.txt")).use { input -> + Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt")).use { encoded -> + var encodedFileLine : String? = ""; + var inputFileLine : String? = ""; + + // read line by line and validate the test + while (encodedFileLine != null && inputFileLine != null) { + encodedFileLine = encoded.readLine() + inputFileLine = input.readLine() + + if(encodedFileLine != null && inputFileLine != null){ + lineNo++ + var precision = 0 + var thirdDimPrecision = 0 + var hasThirdDimension = false + var thirdDimension: ThirdDimension? = ThirdDimension.ABSENT + inputFileLine = inputFileLine.trim { it <= ' ' } + encodedFileLine = encodedFileLine.trim { it <= ' ' } + + //File parsing + val inputs = inputFileLine.substring(1, inputFileLine.length - 1).split(";").toTypedArray() + val meta = inputs[0].trim { it <= ' ' }.substring(1, inputs[0].trim { it <= ' ' }.length - 1).split(",").toTypedArray() + precision = Integer.valueOf(meta[0]) + if (meta.size > 1) { + thirdDimPrecision = Integer.valueOf(meta[1].trim { it <= ' ' }) + thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim { it <= ' ' }).toLong()) + hasThirdDimension = true + } + val latLngZs = extractLatLngZ(inputs[1], hasThirdDimension) + val encodedComputed: String = FlexiblePolyline.encode(latLngZs, precision, thirdDimension, thirdDimPrecision) + val encodedExpected = encodedFileLine + assertEquals(encodedComputed, encodedExpected) + } else { break; } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + System.err.format("LineNo: $lineNo validation got exception: %s%n", e) + } + } + + private fun decodingSmokeTest() { + var lineNo = 0 + try { + Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt")).use { encoded -> + Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/decoded.txt")).use { decoded -> + var encodedFileLine : String? = ""; + var decodedFileLine : String? = ""; + + // read line by line and validate the test + while (encodedFileLine != null && decodedFileLine != null) { + encodedFileLine = encoded.readLine() + decodedFileLine = decoded.readLine() + + if(encodedFileLine != null && decodedFileLine != null){ + + lineNo++ + var hasThirdDimension = false + var expectedDimension: ThirdDimension? = ThirdDimension.ABSENT + encodedFileLine = encodedFileLine.trim { it <= ' ' } + decodedFileLine = decodedFileLine.trim { it <= ' ' } + + //File parsing + val output = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray() + val meta = output[0].trim { it <= ' ' }.substring(1, output[0].trim { it <= ' ' }.length - 1).split(",").toTypedArray() + if (meta.size > 1) { + expectedDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim { it <= ' ' }).toLong()) + hasThirdDimension = true + } + val decodedInputLine = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray()[1] + val expectedLatLngZs = extractLatLngZ(decodedInputLine, hasThirdDimension) + + //Validate thirdDimension + val computedDimension: ThirdDimension = FlexiblePolyline.getThirdDimension(encodedFileLine)!! + assertEquals(computedDimension, expectedDimension) + + //Validate LatLngZ + val computedLatLngZs: List = FlexiblePolyline.decode(encodedFileLine) + assertEquals(computedLatLngZs.size, expectedLatLngZs.size) + for (i in computedLatLngZs.indices) { + assertEquals(computedLatLngZs[i], expectedLatLngZs[i]) + } + } else { break; } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + System.err.format("LineNo: $lineNo validation got exception: %s%n", e) + } + } + + private fun testVeryLongLine(lineLength: Int) { + val PRECISION = 10 + val random = Random() + val coordinates: MutableList = ArrayList() + for (i in 0..lineLength) { + val nextPoint = LatLngZ(random.nextDouble(), random.nextDouble(), random.nextDouble()) + coordinates.add(nextPoint) + } + val encoded: String = FlexiblePolyline.encode(coordinates, PRECISION, ThirdDimension.ALTITUDE, PRECISION) + val startTime = System.nanoTime() + val decoded: List = FlexiblePolyline.decode(encoded) + val duration = System.nanoTime() - startTime + println("duration: " + duration / 1000 + "us") + println("FlexiblePolyline.decoded total number of LatLngZ: " + decoded.size) + } + + companion object { + private const val TEST_FILES_RELATIVE_PATH = "../test/" + + private fun extractLatLngZ(line: String, hasThirdDimension: Boolean): List { + val latLngZs: MutableList = ArrayList() + val coordinates = line.trim { it <= ' ' }.substring(1, line.trim { it <= ' ' }.length - 1).split(",").toTypedArray() + var itr = 0 + while (itr < coordinates.size && !isNullOrEmpty(coordinates[itr])) { + val lat = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace("(", "")) + val lng = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace(")", "")) + var z = 0.0 + if (hasThirdDimension) { + z = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace(")", "")) + } + latLngZs.add(LatLngZ(lat, lng, z)) + } + return latLngZs + } + + private fun isNullOrEmpty(str: String?): Boolean { + return !(str != null && !str.trim { it <= ' ' }.isEmpty()) + } + + private fun assertEquals(lhs: Any, rhs: Any?) { + if (lhs !== rhs) { + if (lhs != rhs) { + throw RuntimeException("Assert failed, $lhs != $rhs") + } + } + } + + private fun assertTrue(value: Boolean) { + if (!value) { + throw RuntimeException("Assert failed") + } + } + + private fun assertThrows(expectedType: Class, runnable: Runnable) { + try { + runnable.run() + } catch (actualException: Throwable) { + if (!expectedType.isInstance(actualException)) { + println("Working Directory = " + actualException.javaClass.name + " "+ actualException); + + throw RuntimeException("Assert failed, Invalid exception found!") + } + return + } + throw RuntimeException("Assert failed, No exception found!") + } + + @JvmStatic + fun main(args: Array) { + println("Working Directory = " + System.getProperty("user.dir")); + + val DEFAULT_LINE_LENGTH = 1000 + var lineLength = DEFAULT_LINE_LENGTH + if (args.size > 0) { + lineLength = args[0].toInt() + } + val test = FlexiblePolylineTest() + test.testInvalidCoordinates() + test.testInvalidThirdDimension() + test.testConvertValue() + test.testSimpleLatLngEncoding() + test.testComplexLatLngEncoding() + test.testLatLngZEncode() + test.encodingSmokeTest() + + //Decode test + test.testInvalidEncoderInput() + test.testThirdDimension() + test.testDecodeConvertValue() + test.testSimpleLatLngDecoding() + test.testComplexLatLngDecoding() + test.testLatLngZDecode() + test.decodingSmokeTest() + test.testVeryLongLine(lineLength) + } + } +} \ No newline at end of file From 0f38a2a78859bbf632f12adf9ee22e7bd96b4f7e Mon Sep 17 00:00:00 2001 From: "Hille, Marlon" Date: Fri, 7 Jun 2024 17:23:30 +0200 Subject: [PATCH 2/5] Refactors implementation to get rid of atomic.* dependencies. Also some code clean-up Signed-off-by: Hille, Marlon --- .../here/flexiblepolyline/FlexiblePolyline.kt | 189 +++++++----------- .../flexiblepolyline/FlexiblePolylineTest.kt | 11 +- 2 files changed, 72 insertions(+), 128 deletions(-) diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt index 946f40e..3784716 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt @@ -6,9 +6,11 @@ */ package com.here.flexiblepolyline -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicReference +import kotlin.math.abs +import kotlin.math.roundToLong +import kotlin.math.sign +import kotlin.math.pow + /** * The polyline encoding is a lossy compressed representation of a list of coordinate pairs or coordinate triples. * It achieves that by: @@ -25,7 +27,7 @@ import java.util.concurrent.atomic.AtomicReference */ object FlexiblePolyline { - private const val version: Int = 1 + const val VERSION = 1L //Base64 URL-safe characters private val ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() private val DECODING_TABLE = intArrayOf( @@ -63,7 +65,7 @@ object FlexiblePolyline { * @param encoded URL-safe encoded [String] * @return [List] of coordinate triples that are decoded from input * - * @see PolylineDecoder.getThirdDimension + * @see getThirdDimension * @see LatLngZ */ @JvmStatic @@ -71,14 +73,8 @@ object FlexiblePolyline { require(!(encoded == null || encoded.trim { it <= ' ' }.isEmpty())) { "Invalid argument!" } val result: MutableList = ArrayList() val dec = Decoder(encoded) - var lat = AtomicReference(0.0) - var lng = AtomicReference(0.0) - var z = AtomicReference(0.0) - while (dec.decodeOne(lat, lng, z)) { - result.add(LatLngZ(lat.get(), lng.get(), z.get())) - lat = AtomicReference(0.0) - lng = AtomicReference(0.0) - z = AtomicReference(0.0) + while (dec.hasNext()) { + result.add(dec.decodeOne()) } return result } @@ -90,10 +86,7 @@ object FlexiblePolyline { */ @JvmStatic fun getThirdDimension(encoded: String): ThirdDimension? { - val index = AtomicInteger(0) - val header = AtomicLong(0) - Decoder.decodeHeaderFromString(encoded.toCharArray(), index, header) - return ThirdDimension.fromNum(header.get() shr 4 and 7) + return Decoder(encoded).thirdDimension } //Decode a single char to the corresponding value @@ -112,6 +105,12 @@ object FlexiblePolyline { private val latConveter: Converter = Converter(precision) private val lngConveter: Converter = Converter(precision) private val zConveter: Converter = Converter(thirdDimPrecision) + + + init { + encodeHeader(precision, this.thirdDimension.num, thirdDimPrecision) + } + private fun encodeHeader(precision: Int, thirdDimensionValue: Int, thirdDimPrecision: Int) { /* * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char @@ -120,8 +119,8 @@ object FlexiblePolyline { require(!(thirdDimPrecision < 0 || thirdDimPrecision > 15)) { "thirdDimPrecision out of range" } require(!(thirdDimensionValue < 0 || thirdDimensionValue > 7)) { "thirdDimensionValue out of range" } val res = (thirdDimPrecision shl 7 or (thirdDimensionValue shl 4) or precision).toLong() - Converter.encodeUnsignedVarint(version.toLong(), result) - Converter.encodeUnsignedVarint(res, result) + Converter.encodeUnsignedVarInt(VERSION, result) + Converter.encodeUnsignedVarInt(res, result) } private fun add(lat: Double, lng: Double) { @@ -144,71 +143,52 @@ object FlexiblePolyline { fun getEncoded(): String { return result.toString() } - - init { - encodeHeader(precision, this.thirdDimension.num, thirdDimPrecision) - } } /* * Single instance for decoding an input request. */ private class Decoder(encoded: String) { - private val encoded: CharArray = encoded.toCharArray() - private val index: AtomicInteger = AtomicInteger(0) + private val encoded: CharIterator = (encoded).iterator() private val latConverter: Converter private val lngConverter: Converter private val zConverter: Converter - private var precision = 0 - private var thirdDimPrecision = 0 - private var thirdDimension: ThirdDimension? = null + var thirdDimension: ThirdDimension? = null + + init { + val header = decodeHeader() + val precision = header and 0x0f + thirdDimension = ThirdDimension.fromNum(((header shr 4) and 0x07).toLong()) + val thirdDimPrecision = ((header shr 7) and 0x0f) + latConverter = Converter(precision) + lngConverter = Converter(precision) + zConverter = Converter(thirdDimPrecision) + } + private fun hasThirdDimension(): Boolean { return thirdDimension != ThirdDimension.ABSENT } - private fun decodeHeader() { - val header = AtomicLong(0) - decodeHeaderFromString(encoded, index, header) - precision = (header.get() and 15).toInt() // we pick the first 4 bits only - header.set(header.get() shr 4) - thirdDimension = ThirdDimension.fromNum(header.get() and 7) // we pick the first 3 bits only - thirdDimPrecision = (header.get() shr 3 and 15).toInt() + private fun decodeHeader(): Int { + val version = Converter.decodeUnsignedVarInt(encoded) + require(version == VERSION) { "Invalid format version :: encoded.$version vs FlexiblePolyline.$VERSION" } + // Decode the polyline header + return Converter.decodeUnsignedVarInt(encoded).toInt() } - fun decodeOne( - lat: AtomicReference, - lng: AtomicReference, - z: AtomicReference - ): Boolean { - if (index.get() == encoded.size) { - return false - } - require(latConverter.decodeValue(encoded, index, lat)) { "Invalid encoding" } - require(lngConverter.decodeValue(encoded, index, lng)) { "Invalid encoding" } + fun decodeOne(): LatLngZ { + val lat = latConverter.decodeValue(encoded) + val lng = lngConverter.decodeValue(encoded) + if (hasThirdDimension()) { - require(zConverter.decodeValue(encoded, index, z)) { "Invalid encoding" } + val z = zConverter.decodeValue(encoded) + return LatLngZ(lat, lng, z) } - return true + return LatLngZ(lat, lng) } - companion object { - fun decodeHeaderFromString(encoded: CharArray, index: AtomicInteger, header: AtomicLong) { - val value = AtomicLong(0) - - // Decode the header version - require(Converter.decodeUnsignedVarint(encoded, index, value)) { "Invalid encoding" } - require(value.get() == version.toLong()) { "Invalid format version" } - // Decode the polyline header - require(Converter.decodeUnsignedVarint(encoded, index, value)) { "Invalid encoding" } - header.set(value.get()) - } - } - - init { - decodeHeader() - latConverter = Converter(precision) - lngConverter = Converter(precision) - zConverter = Converter(thirdDimPrecision) + fun hasNext(): Boolean { + return encoded.hasNext() } } @@ -219,12 +199,8 @@ object FlexiblePolyline { * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) */ class Converter(precision: Int) { - private var multiplier: Long = 0 + private val multiplier = (10.0.pow(precision.toDouble())).toLong() private var lastValue: Long = 0 - private fun setPrecision(precision: Int) { - //multiplier = Math.pow(10.0, java.lang.Double.valueOf(precision.toDouble())).toLong() - multiplier = Math.pow(10.0, precision.toDouble()).toLong() - } fun encodeValue(value: Double, result: StringBuilder) { /* @@ -233,7 +209,7 @@ object FlexiblePolyline { * round(-1.5) --> -2 * round(-2.5) --> -3 */ - val scaledValue = Math.round(Math.abs(value * multiplier)) * Math.round(Math.signum(value)) + val scaledValue = abs(value * multiplier).roundToLong() * sign(value).roundToLong() var delta = scaledValue - lastValue val negative = delta < 0 lastValue = scaledValue @@ -245,34 +221,23 @@ object FlexiblePolyline { if (negative) { delta = delta.inv() } - encodeUnsignedVarint(delta, result) + encodeUnsignedVarInt(delta, result) } //Decode single coordinate (say lat|lng|z) starting at index - fun decodeValue( - encoded: CharArray, - index: AtomicInteger, - coordinate: AtomicReference - ): Boolean { - val delta = AtomicLong() - if (!decodeUnsignedVarint(encoded, index, delta)) { - return false - } - if (delta.get() and 1 != 0L) { - delta.set(delta.get().inv()) + fun decodeValue(encoded: CharIterator): Double { + var l = decodeUnsignedVarInt(encoded) + if ((l and 1L) != 0L) { + l = l.inv() } - delta.set(delta.get() shr 1) - lastValue += delta.get() - coordinate.set(lastValue.toDouble() / multiplier) - return true + l = l shr 1 + lastValue += l + + return lastValue.toDouble() / multiplier } companion object { - fun encodeUnsignedVarint(value: Long, result: StringBuilder) { - // TODO: check performance impact - /*val estimatedCapacity = 10000 // Adjust as needed - result.ensureCapacity(estimatedCapacity)*/ - // end TODO + fun encodeUnsignedVarInt(value: Long, result: StringBuilder) { var number = value while (number > 0x1F) { val pos = (number and 0x1F or 0x20).toByte() @@ -282,40 +247,25 @@ object FlexiblePolyline { result.append(ENCODING_TABLE[number.toByte().toInt()]) } - fun decodeUnsignedVarint( - encoded: CharArray, - index: AtomicInteger, - result: AtomicLong - ): Boolean { + fun decodeUnsignedVarInt(encoded: CharIterator): Long { var shift: Short = 0 - var delta: Long = 0 - var value: Long - while (index.get() < encoded.size) { - value = decodeChar(encoded[index.get()]).toLong() + var result: Long = 0 + while ( encoded.hasNext() ) { + val c = encoded.next() + val value = decodeChar(c).toLong() if (value < 0) { - return false + throw IllegalArgumentException("Unexpected value found :: '$c") } - index.incrementAndGet() - delta = delta or (value and 0x1F shl shift.toInt()) - if (value and 0x20 == 0L) { - result.set(delta) - return true + result = result or ((value and 0x1FL) shl shift.toInt()) + if ((value and 0x20L) == 0L) { + return result } else { shift = (shift + 5).toShort() } - // TODO: Check performance and tests - /*if (shift <= 0) { - return true - }*/ - // end TODO } - return shift <= 0 + return result } } - - init { - setPrecision(precision) - } } /** @@ -328,7 +278,7 @@ object FlexiblePolyline { companion object { fun fromNum(value: Long): ThirdDimension? { - for (dim in values()) { + for (dim in entries) { if (dim.num.toLong() == value) { return dim } @@ -351,8 +301,7 @@ object FlexiblePolyline { return true } if (other is LatLngZ) { - val passed = other - if (passed.lat == lat && passed.lng == lng && passed.z == z) { + if (other.lat == lat && other.lng == lng && other.z == z) { return true } } diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt index 35c7c70..7835c36 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -112,16 +112,11 @@ class FlexiblePolylineTest { } private fun testDecodeConvertValue() { - val encoded = ("h_wqiB").toCharArray() + val encoded = ("h_wqiB").iterator() val expected = -179.98321 - val computed = AtomicReference(0.0) val conv = Converter(5) - conv.decodeValue( - encoded, - AtomicInteger(0), - computed - ) - assertEquals(computed.get(), expected) + val computed = conv.decodeValue(encoded) + assertEquals(computed, expected) } private fun testSimpleLatLngDecoding() { From 29af630a7c09768a94b8b0fdcbce7fc1d4561396 Mon Sep 17 00:00:00 2001 From: "Hille, Marlon" Date: Tue, 25 Jun 2024 15:21:32 +0200 Subject: [PATCH 3/5] Code clean-up and minor improvements Signed-off-by: Hille, Marlon --- .../here/flexiblepolyline/FlexiblePolyline.kt | 80 ++++++++----------- .../flexiblepolyline/FlexiblePolylineTest.kt | 29 +++---- 2 files changed, 47 insertions(+), 62 deletions(-) diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt index 3784716..55a6f08 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt @@ -26,8 +26,8 @@ import kotlin.math.pow * - It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, elevation or some other custom value */ object FlexiblePolyline { - const val VERSION = 1L + //Base64 URL-safe characters private val ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() private val DECODING_TABLE = intArrayOf( @@ -38,7 +38,8 @@ object FlexiblePolyline { ) /** - * Encode the list of coordinate triples.



+ * Encode the list of coordinate triples. + * * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT. * This is lossy compression based on precision accuracy. * @@ -53,15 +54,15 @@ object FlexiblePolyline { require(!coordinates.isNullOrEmpty()) { "Invalid coordinates!" } requireNotNull(thirdDimension) { "Invalid thirdDimension" } val enc = Encoder(precision, thirdDimension, thirdDimPrecision) - val iterator = coordinates.iterator() - while (iterator.hasNext()) { - enc.add(iterator.next()) + coordinates.iterator().forEach { + enc.add(it) } return enc.getEncoded() } /** - * Decode the encoded input [String] to [List] of coordinate triples.



+ * Decode the encoded input [String] to [List] of coordinate triples. + * * @param encoded URL-safe encoded [String] * @return [List] of coordinate triples that are decoded from input * @@ -70,11 +71,11 @@ object FlexiblePolyline { */ @JvmStatic fun decode(encoded: String?): List { - require(!(encoded == null || encoded.trim { it <= ' ' }.isEmpty())) { "Invalid argument!" } + require(!encoded.isNullOrBlank()) { "Invalid argument!" } val result: MutableList = ArrayList() val dec = Decoder(encoded) - while (dec.hasNext()) { - result.add(dec.decodeOne()) + dec.iterator().forEach { + result.add(it) } return result } @@ -102,10 +103,9 @@ object FlexiblePolyline { */ private class Encoder(precision: Int, private val thirdDimension: ThirdDimension, thirdDimPrecision: Int) { private val result: StringBuilder = StringBuilder() - private val latConveter: Converter = Converter(precision) - private val lngConveter: Converter = Converter(precision) - private val zConveter: Converter = Converter(thirdDimPrecision) - + private val latConverter: Converter = Converter(precision) + private val lngConverter: Converter = Converter(precision) + private val zConverter: Converter = Converter(thirdDimPrecision) init { encodeHeader(precision, this.thirdDimension.num, thirdDimPrecision) @@ -124,14 +124,14 @@ object FlexiblePolyline { } private fun add(lat: Double, lng: Double) { - latConveter.encodeValue(lat, result) - lngConveter.encodeValue(lng, result) + latConverter.encodeValue(lat, result) + lngConverter.encodeValue(lng, result) } private fun add(lat: Double, lng: Double, z: Double) { add(lat, lng) if (thirdDimension != ThirdDimension.ABSENT) { - zConveter.encodeValue(z, result) + zConverter.encodeValue(z, result) } } @@ -148,8 +148,8 @@ object FlexiblePolyline { /* * Single instance for decoding an input request. */ - private class Decoder(encoded: String) { - private val encoded: CharIterator = (encoded).iterator() + private class Decoder(encoded: String) : Iterator { + private val encoded: CharIterator = encoded.iterator() private val latConverter: Converter private val lngConverter: Converter private val zConverter: Converter @@ -176,7 +176,7 @@ object FlexiblePolyline { return Converter.decodeUnsignedVarInt(encoded).toInt() } - fun decodeOne(): LatLngZ { + override fun next(): LatLngZ { val lat = latConverter.decodeValue(encoded) val lng = lngConverter.decodeValue(encoded) @@ -187,7 +187,7 @@ object FlexiblePolyline { return LatLngZ(lat, lng) } - fun hasNext(): Boolean { + override fun hasNext(): Boolean { return encoded.hasNext() } } @@ -250,11 +250,11 @@ object FlexiblePolyline { fun decodeUnsignedVarInt(encoded: CharIterator): Long { var shift: Short = 0 var result: Long = 0 - while ( encoded.hasNext() ) { - val c = encoded.next() - val value = decodeChar(c).toLong() + + encoded.forEach { + val value = decodeChar(it).toLong() if (value < 0) { - throw IllegalArgumentException("Unexpected value found :: '$c") + throw IllegalArgumentException("Unexpected value found :: '$it") } result = result or ((value and 0x1FL) shl shift.toInt()) if ((value and 0x20L) == 0L) { @@ -291,28 +291,12 @@ object FlexiblePolyline { /** * Coordinate triple */ - class LatLngZ @JvmOverloads constructor(val lat: Double, val lng: Double, val z: Double = 0.0) { - override fun toString(): String { - return "LatLngZ [lat=$lat, lng=$lng, z=$z]" - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (other is LatLngZ) { - if (other.lat == lat && other.lng == lng && other.z == z) { - return true - } - } - return false - } - - override fun hashCode(): Int { - var result = lat.hashCode() - result = 31 * result + lng.hashCode() - result = 31 * result + z.hashCode() - return result - } - } + /** + * Coordinate triple + */ + data class LatLngZ( + val lat: Double, + val lng: Double, + val z: Double = 0.0 + ){} } \ No newline at end of file diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt index 7835c36..ed38b9e 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -106,6 +106,7 @@ class FlexiblePolylineTest { } private fun testThirdDimension() { + assertTrue(FlexiblePolyline.getThirdDimension("BFoz5xJ67i1BU") === ThirdDimension.ABSENT) assertTrue(FlexiblePolyline.getThirdDimension("BVoz5xJ67i1BU") === ThirdDimension.LEVEL) assertTrue(FlexiblePolyline.getThirdDimension("BlBoz5xJ67i1BU") === ThirdDimension.ALTITUDE) assertTrue(FlexiblePolyline.getThirdDimension("B1Boz5xJ67i1BU") === ThirdDimension.ELEVATION) @@ -183,16 +184,16 @@ class FlexiblePolylineTest { var thirdDimPrecision = 0 var hasThirdDimension = false var thirdDimension: ThirdDimension? = ThirdDimension.ABSENT - inputFileLine = inputFileLine.trim { it <= ' ' } - encodedFileLine = encodedFileLine.trim { it <= ' ' } + inputFileLine = inputFileLine.trim() + encodedFileLine = encodedFileLine.trim() //File parsing val inputs = inputFileLine.substring(1, inputFileLine.length - 1).split(";").toTypedArray() - val meta = inputs[0].trim { it <= ' ' }.substring(1, inputs[0].trim { it <= ' ' }.length - 1).split(",").toTypedArray() + val meta = inputs[0].trim().substring(1, inputs[0].trim().length - 1).split(",").toTypedArray() precision = Integer.valueOf(meta[0]) if (meta.size > 1) { - thirdDimPrecision = Integer.valueOf(meta[1].trim { it <= ' ' }) - thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim { it <= ' ' }).toLong()) + thirdDimPrecision = Integer.valueOf(meta[1].trim()) + thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) hasThirdDimension = true } val latLngZs = extractLatLngZ(inputs[1], hasThirdDimension) @@ -227,14 +228,14 @@ class FlexiblePolylineTest { lineNo++ var hasThirdDimension = false var expectedDimension: ThirdDimension? = ThirdDimension.ABSENT - encodedFileLine = encodedFileLine.trim { it <= ' ' } - decodedFileLine = decodedFileLine.trim { it <= ' ' } + encodedFileLine = encodedFileLine.trim() + decodedFileLine = decodedFileLine.trim() //File parsing val output = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray() - val meta = output[0].trim { it <= ' ' }.substring(1, output[0].trim { it <= ' ' }.length - 1).split(",").toTypedArray() + val meta = output[0].trim().substring(1, output[0].trim().length - 1).split(",").toTypedArray() if (meta.size > 1) { - expectedDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim { it <= ' ' }).toLong()) + expectedDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) hasThirdDimension = true } val decodedInputLine = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray()[1] @@ -281,14 +282,14 @@ class FlexiblePolylineTest { private fun extractLatLngZ(line: String, hasThirdDimension: Boolean): List { val latLngZs: MutableList = ArrayList() - val coordinates = line.trim { it <= ' ' }.substring(1, line.trim { it <= ' ' }.length - 1).split(",").toTypedArray() + val coordinates = line.trim().substring(1, line.trim().length - 1).split(",").toTypedArray() var itr = 0 while (itr < coordinates.size && !isNullOrEmpty(coordinates[itr])) { - val lat = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace("(", "")) - val lng = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace(")", "")) + val lat = java.lang.Double.valueOf(coordinates[itr++].trim().replace("(", "")) + val lng = java.lang.Double.valueOf(coordinates[itr++].trim().replace(")", "")) var z = 0.0 if (hasThirdDimension) { - z = java.lang.Double.valueOf(coordinates[itr++].trim { it <= ' ' }.replace(")", "")) + z = java.lang.Double.valueOf(coordinates[itr++].trim().replace(")", "")) } latLngZs.add(LatLngZ(lat, lng, z)) } @@ -296,7 +297,7 @@ class FlexiblePolylineTest { } private fun isNullOrEmpty(str: String?): Boolean { - return !(str != null && !str.trim { it <= ' ' }.isEmpty()) + return str == null || str.trim().isEmpty() } private fun assertEquals(lhs: Any, rhs: Any?) { From a97d4290e42ce2300420efb2bfa739e2bb7f35e4 Mon Sep 17 00:00:00 2001 From: "Hille, Marlon" Date: Tue, 25 Jun 2024 19:47:58 +0200 Subject: [PATCH 4/5] Refactors test cases to remove redundant code and make test code readable Signed-off-by: Hille, Marlon --- .../flexiblepolyline/FlexiblePolylineTest.kt | 206 ++++++++++-------- 1 file changed, 113 insertions(+), 93 deletions(-) diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt index ed38b9e..7d23503 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -11,8 +11,6 @@ import com.here.flexiblepolyline.FlexiblePolyline.LatLngZ import java.nio.file.Files import java.nio.file.Paths import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference /** * Validate polyline encoding with different input combinations. @@ -166,98 +164,31 @@ class FlexiblePolylineTest { } private fun encodingSmokeTest() { - var lineNo = 0 - try { - Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "original.txt")).use { input -> - Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt")).use { encoded -> - var encodedFileLine : String? = ""; - var inputFileLine : String? = ""; + TestCaseReader("original.txt", "round_half_up/encoded.txt").iterator().forEach { + val original = parseTestDataFromLine(it.testInput); + val encodedComputed: String = FlexiblePolyline.encode(original.latLngZs, original.precision, original.thirdDimension, original.thirdDimensionPrecision) - // read line by line and validate the test - while (encodedFileLine != null && inputFileLine != null) { - encodedFileLine = encoded.readLine() - inputFileLine = input.readLine() - - if(encodedFileLine != null && inputFileLine != null){ - lineNo++ - var precision = 0 - var thirdDimPrecision = 0 - var hasThirdDimension = false - var thirdDimension: ThirdDimension? = ThirdDimension.ABSENT - inputFileLine = inputFileLine.trim() - encodedFileLine = encodedFileLine.trim() - - //File parsing - val inputs = inputFileLine.substring(1, inputFileLine.length - 1).split(";").toTypedArray() - val meta = inputs[0].trim().substring(1, inputs[0].trim().length - 1).split(",").toTypedArray() - precision = Integer.valueOf(meta[0]) - if (meta.size > 1) { - thirdDimPrecision = Integer.valueOf(meta[1].trim()) - thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) - hasThirdDimension = true - } - val latLngZs = extractLatLngZ(inputs[1], hasThirdDimension) - val encodedComputed: String = FlexiblePolyline.encode(latLngZs, precision, thirdDimension, thirdDimPrecision) - val encodedExpected = encodedFileLine - assertEquals(encodedComputed, encodedExpected) - } else { break; } - } - } - } - } catch (e: Exception) { - e.printStackTrace() - System.err.format("LineNo: $lineNo validation got exception: %s%n", e) + assertEquals(encodedComputed, it.testResult) } } private fun decodingSmokeTest() { - var lineNo = 0 - try { - Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt")).use { encoded -> - Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/decoded.txt")).use { decoded -> - var encodedFileLine : String? = ""; - var decodedFileLine : String? = ""; + TestCaseReader("round_half_up/encoded.txt", "round_half_up/decoded.txt").iterator().forEach { + val expected = parseTestDataFromLine(it.testResult); - // read line by line and validate the test - while (encodedFileLine != null && decodedFileLine != null) { - encodedFileLine = encoded.readLine() - decodedFileLine = decoded.readLine() - - if(encodedFileLine != null && decodedFileLine != null){ - - lineNo++ - var hasThirdDimension = false - var expectedDimension: ThirdDimension? = ThirdDimension.ABSENT - encodedFileLine = encodedFileLine.trim() - decodedFileLine = decodedFileLine.trim() - - //File parsing - val output = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray() - val meta = output[0].trim().substring(1, output[0].trim().length - 1).split(",").toTypedArray() - if (meta.size > 1) { - expectedDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) - hasThirdDimension = true - } - val decodedInputLine = decodedFileLine.substring(1, decodedFileLine.length - 1).split(";").toTypedArray()[1] - val expectedLatLngZs = extractLatLngZ(decodedInputLine, hasThirdDimension) - - //Validate thirdDimension - val computedDimension: ThirdDimension = FlexiblePolyline.getThirdDimension(encodedFileLine)!! - assertEquals(computedDimension, expectedDimension) - - //Validate LatLngZ - val computedLatLngZs: List = FlexiblePolyline.decode(encodedFileLine) - assertEquals(computedLatLngZs.size, expectedLatLngZs.size) - for (i in computedLatLngZs.indices) { - assertEquals(computedLatLngZs[i], expectedLatLngZs[i]) - } - } else { break; } - } + //Validate thirdDimension + val computedDimension: FlexiblePolyline.ThirdDimension = FlexiblePolyline.getThirdDimension(it.testInput)!! + assertEquals(computedDimension, expected.thirdDimension) + + //Validate LatLngZ + val computedLatLngZs: List = FlexiblePolyline.decode(it.testInput) + + assertEquals(computedLatLngZs.size, expected.latLngZs?.size) + expected.latLngZs?.let { + for (i in computedLatLngZs.indices) { + assertEquals(computedLatLngZs[i], expected.latLngZs[i]) } - } - } catch (e: Exception) { - e.printStackTrace() - System.err.format("LineNo: $lineNo validation got exception: %s%n", e) + } ?: throw Exception("Error parsing expected results") } } @@ -278,20 +209,49 @@ class FlexiblePolylineTest { } companion object { - private const val TEST_FILES_RELATIVE_PATH = "../test/" + const val TEST_FILES_RELATIVE_PATH = "../test/" + + // Helper for parsing DecodeLines file + // Line Format: {(precision, thirdDimPrecision?, thirdDim?); [(c1Lat, c1Lng, c1Alt), ]} + private fun parseTestDataFromLine(line: String): TestData { + var precision = 0 + var thirdDimensionPrecision = 0 + var hasThirdDimension = false + var thirdDimension: FlexiblePolyline.ThirdDimension? = FlexiblePolyline.ThirdDimension.ABSENT + + // .substring gets rid of { and } + val splitBySemicolon = line.substring(1, line.length - 1).split(";").toTypedArray(); + val leftPart = splitBySemicolon[0]; + val meta = leftPart.split(",").toTypedArray(); + precision = Integer.valueOf(meta[0]) + if (meta.size > 1) { + thirdDimension = FlexiblePolyline.ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) + hasThirdDimension = true + thirdDimensionPrecision = Integer.valueOf(meta[1].trim { it <= ' ' }) + } + + val latLngZs = extractLatLngZ(splitBySemicolon[1], hasThirdDimension) + + return TestData( + precision = precision, + thirdDimensionPrecision = thirdDimensionPrecision, + thirdDimension = thirdDimension, + latLngZs = latLngZs + ) + } private fun extractLatLngZ(line: String, hasThirdDimension: Boolean): List { val latLngZs: MutableList = ArrayList() val coordinates = line.trim().substring(1, line.trim().length - 1).split(",").toTypedArray() var itr = 0 - while (itr < coordinates.size && !isNullOrEmpty(coordinates[itr])) { - val lat = java.lang.Double.valueOf(coordinates[itr++].trim().replace("(", "")) - val lng = java.lang.Double.valueOf(coordinates[itr++].trim().replace(")", "")) + while (itr < coordinates.size && coordinates[itr].isNotBlank()) { + val lat = coordinates[itr++].trim().replace("(", "").toDouble() + val lng = coordinates[itr++].trim().replace(")", "").toDouble() var z = 0.0 if (hasThirdDimension) { - z = java.lang.Double.valueOf(coordinates[itr++].trim().replace(")", "")) + z = coordinates[itr++].trim().replace(")", "").toDouble() } - latLngZs.add(LatLngZ(lat, lng, z)) + latLngZs.add(FlexiblePolyline.LatLngZ(lat, lng, z)) } return latLngZs } @@ -357,4 +317,64 @@ class FlexiblePolylineTest { test.testVeryLongLine(lineLength) } } +} + +private data class TestData( + val precision: Int = 0, + val thirdDimensionPrecision: Int = 0, + val thirdDimension: FlexiblePolyline.ThirdDimension? = null, + val latLngZs: List? = null +){} + +private data class TestCase( + val testInput: String, + val testResult: String +){} + +private class TestCaseReader(testInputFile: String, testResultFile: String) : Iterator { + private var totalLines = 0 + private var currentLine = 0 + private var testCases = mutableListOf() + + init { + try { + Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testInputFile)).use { input -> + Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testResultFile)).use { result -> + // read line by line and validate the test + while (true) { + val regex = "\\s|\\(|\\)".toRegex() + val testInputFileLine = input.readLine(); + val testResultFileLine = result.readLine(); + + if (testInputFileLine != null && testInputFileLine.isNotBlank() && testResultFileLine != null && testResultFileLine.isNotBlank()) { + testCases.add( + TestCase( + testInput = testInputFileLine.replace(regex, ""), + testResult = testResultFileLine.replace(regex, "") + ) + ) + totalLines++ + } else { + break + } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + System.err.format("TestCaseReader - exception reading test case $testInputFile and $testResultFile at LineNo: $totalLines: %s%n", e) + throw RuntimeException("Test failed, as test data could not be loaded by TestCaseReader") + } + } + + override fun hasNext(): Boolean { + return currentLine < totalLines + } + + override fun next(): TestCase { + if(!hasNext()) throw NoSuchElementException() + val testCase = testCases[currentLine] + currentLine++ + return testCase + } } \ No newline at end of file From 034933b340448929c64867e8409071ced0bb9c1b Mon Sep 17 00:00:00 2001 From: "Hille, Marlon" Date: Mon, 22 Jul 2024 11:13:13 +0200 Subject: [PATCH 5/5] Refactors TestCaseReader to use a simple list instead of implementing Iterator interface Cleanup FlexiblePolyline.Converter and FlexiblePolyline.ThirdDimension as there is no need to use the package name Cleanup of comments Using an Exception with more details in case loading a test case failed Signed-off-by: Hille, Marlon --- .../here/flexiblepolyline/FlexiblePolyline.kt | 34 ++++---- .../flexiblepolyline/FlexiblePolylineTest.kt | 78 +++++++++---------- 2 files changed, 49 insertions(+), 63 deletions(-) diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt index 55a6f08..5ee90e3 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt @@ -28,7 +28,7 @@ import kotlin.math.pow object FlexiblePolyline { const val VERSION = 1L - //Base64 URL-safe characters + // Base64 URL-safe characters private val ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() private val DECODING_TABLE = intArrayOf( 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, @@ -82,6 +82,7 @@ object FlexiblePolyline { /** * ThirdDimension type from the encoded input [String] + * * @param encoded URL-safe encoded coordinate triples [String] * @return type of [ThirdDimension] */ @@ -90,7 +91,7 @@ object FlexiblePolyline { return Decoder(encoded).thirdDimension } - //Decode a single char to the corresponding value + // Decode a single char to the corresponding value private fun decodeChar(charValue: Char): Int { val pos = charValue.code - 45 return if (pos < 0 || pos > 77) { @@ -98,9 +99,7 @@ object FlexiblePolyline { } else DECODING_TABLE[pos] } - /* - * Single instance for configuration, validation and encoding for an input request. - */ + // Single instance for configuration, validation and encoding for an input request. private class Encoder(precision: Int, private val thirdDimension: ThirdDimension, thirdDimPrecision: Int) { private val result: StringBuilder = StringBuilder() private val latConverter: Converter = Converter(precision) @@ -112,13 +111,11 @@ object FlexiblePolyline { } private fun encodeHeader(precision: Int, thirdDimensionValue: Int, thirdDimPrecision: Int) { - /* - * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char - */ + // Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char require(!(precision < 0 || precision > 15)) { "precision out of range" } require(!(thirdDimPrecision < 0 || thirdDimPrecision > 15)) { "thirdDimPrecision out of range" } require(!(thirdDimensionValue < 0 || thirdDimensionValue > 7)) { "thirdDimensionValue out of range" } - val res = (thirdDimPrecision shl 7 or (thirdDimensionValue shl 4) or precision).toLong() + val res = ((thirdDimPrecision shl 7) or (thirdDimensionValue shl 4) or precision).toLong() Converter.encodeUnsignedVarInt(VERSION, result) Converter.encodeUnsignedVarInt(res, result) } @@ -145,9 +142,7 @@ object FlexiblePolyline { } } - /* - * Single instance for decoding an input request. - */ + // Single instance for decoding an input request. private class Decoder(encoded: String) : Iterator { private val encoded: CharIterator = encoded.iterator() private val latConverter: Converter @@ -192,11 +187,13 @@ object FlexiblePolyline { } } - /* + /** * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request. * Instance should be specific to type of coordinates (e.g. Lat, Lng) * so that specific type delta is computed for encoding. * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) + * + * @param precision [Int] */ class Converter(precision: Int) { private val multiplier = (10.0.pow(precision.toDouble())).toLong() @@ -224,7 +221,7 @@ object FlexiblePolyline { encodeUnsignedVarInt(delta, result) } - //Decode single coordinate (say lat|lng|z) starting at index + // Decode single coordinate (say lat|lng|z) starting at index fun decodeValue(encoded: CharIterator): Double { var l = decodeUnsignedVarInt(encoded) if ((l and 1L) != 0L) { @@ -251,10 +248,10 @@ object FlexiblePolyline { var shift: Short = 0 var result: Long = 0 - encoded.forEach { - val value = decodeChar(it).toLong() + encoded.withIndex().forEach { + val value = decodeChar(it.value).toLong() if (value < 0) { - throw IllegalArgumentException("Unexpected value found :: '$it") + throw IllegalArgumentException("Unexpected value found :: '${it.value}' at ${it.index}") } result = result or ((value and 0x1FL) shl shift.toInt()) if ((value and 0x20L) == 0L) { @@ -288,9 +285,6 @@ object FlexiblePolyline { } } - /** - * Coordinate triple - */ /** * Coordinate triple */ diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt index 7d23503..e2f23b0 100644 --- a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * License-Filename: LICENSE */ -import com.here.flexiblepolyline.* +import com.here.flexiblepolyline.FlexiblePolyline import com.here.flexiblepolyline.FlexiblePolyline.Converter import com.here.flexiblepolyline.FlexiblePolyline.ThirdDimension import com.here.flexiblepolyline.FlexiblePolyline.LatLngZ @@ -18,13 +18,13 @@ import java.util.* class FlexiblePolylineTest { private fun testInvalidCoordinates() { - //Null coordinates + // Null coordinates assertThrows( IllegalArgumentException::class.java ) { FlexiblePolyline.encode(null, 5, ThirdDimension.ABSENT, 0) } - //Empty coordinates list test + // Empty coordinates list test assertThrows( IllegalArgumentException::class.java ) { FlexiblePolyline.encode(ArrayList(), 5, ThirdDimension.ABSENT, 0) } @@ -35,14 +35,14 @@ class FlexiblePolylineTest { pairs.add(LatLngZ(50.1022829, 8.6982122)) val invalid: ThirdDimension? = null - //Invalid Third Dimension + // Invalid Third Dimension assertThrows( IllegalArgumentException::class.java ) { FlexiblePolyline.encode(pairs, 5, invalid, 0) } } private fun testConvertValue() { - val conv: FlexiblePolyline.Converter = Converter(5) + val conv: Converter = Converter(5) val result = StringBuilder() conv.encodeValue(-179.98321, result) assertEquals(result.toString(), "h_wqiB") @@ -86,18 +86,19 @@ class FlexiblePolylineTest { val computed: String = FlexiblePolyline.encode(tuples, 5, ThirdDimension.ALTITUDE, 0) assertEquals(computed, expected) } - /** */ - /********** Decoder test starts */ - /** */ + + /** + * Decoder test starts + */ private fun testInvalidEncoderInput() { - //Null coordinates + // Null coordinates assertThrows( IllegalArgumentException::class.java ) { FlexiblePolyline.decode(null) } - //Empty coordinates list test + // Empty coordinates list test assertThrows( IllegalArgumentException::class.java ) { FlexiblePolyline.decode("") } @@ -164,7 +165,7 @@ class FlexiblePolylineTest { } private fun encodingSmokeTest() { - TestCaseReader("original.txt", "round_half_up/encoded.txt").iterator().forEach { + TestCaseReader("original.txt", "round_half_up/encoded.txt").testCases.forEach { val original = parseTestDataFromLine(it.testInput); val encodedComputed: String = FlexiblePolyline.encode(original.latLngZs, original.precision, original.thirdDimension, original.thirdDimensionPrecision) @@ -173,14 +174,14 @@ class FlexiblePolylineTest { } private fun decodingSmokeTest() { - TestCaseReader("round_half_up/encoded.txt", "round_half_up/decoded.txt").iterator().forEach { + TestCaseReader("round_half_up/encoded.txt", "round_half_up/decoded.txt").testCases.forEach { val expected = parseTestDataFromLine(it.testResult); - //Validate thirdDimension - val computedDimension: FlexiblePolyline.ThirdDimension = FlexiblePolyline.getThirdDimension(it.testInput)!! + // Validate thirdDimension + val computedDimension: ThirdDimension = FlexiblePolyline.getThirdDimension(it.testInput)!! assertEquals(computedDimension, expected.thirdDimension) - //Validate LatLngZ + // Validate LatLngZ val computedLatLngZs: List = FlexiblePolyline.decode(it.testInput) assertEquals(computedLatLngZs.size, expected.latLngZs?.size) @@ -211,13 +212,15 @@ class FlexiblePolylineTest { companion object { const val TEST_FILES_RELATIVE_PATH = "../test/" - // Helper for parsing DecodeLines file - // Line Format: {(precision, thirdDimPrecision?, thirdDim?); [(c1Lat, c1Lng, c1Alt), ]} + /* + * Helper for parsing DecodeLines file + * Line Format: {(precision, thirdDimPrecision?, thirdDim?); [(c1Lat, c1Lng, c1Alt), ]} + */ private fun parseTestDataFromLine(line: String): TestData { var precision = 0 var thirdDimensionPrecision = 0 var hasThirdDimension = false - var thirdDimension: FlexiblePolyline.ThirdDimension? = FlexiblePolyline.ThirdDimension.ABSENT + var thirdDimension: ThirdDimension? = FlexiblePolyline.ThirdDimension.ABSENT // .substring gets rid of { and } val splitBySemicolon = line.substring(1, line.length - 1).split(";").toTypedArray(); @@ -225,9 +228,9 @@ class FlexiblePolylineTest { val meta = leftPart.split(",").toTypedArray(); precision = Integer.valueOf(meta[0]) if (meta.size > 1) { - thirdDimension = FlexiblePolyline.ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) + thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) hasThirdDimension = true - thirdDimensionPrecision = Integer.valueOf(meta[1].trim { it <= ' ' }) + thirdDimensionPrecision = Integer.valueOf(meta[1].trim()) } val latLngZs = extractLatLngZ(splitBySemicolon[1], hasThirdDimension) @@ -256,10 +259,6 @@ class FlexiblePolylineTest { return latLngZs } - private fun isNullOrEmpty(str: String?): Boolean { - return str == null || str.trim().isEmpty() - } - private fun assertEquals(lhs: Any, rhs: Any?) { if (lhs !== rhs) { if (lhs != rhs) { @@ -306,7 +305,7 @@ class FlexiblePolylineTest { test.testLatLngZEncode() test.encodingSmokeTest() - //Decode test + // Decode test test.testInvalidEncoderInput() test.testThirdDimension() test.testDecodeConvertValue() @@ -322,7 +321,7 @@ class FlexiblePolylineTest { private data class TestData( val precision: Int = 0, val thirdDimensionPrecision: Int = 0, - val thirdDimension: FlexiblePolyline.ThirdDimension? = null, + val thirdDimension: ThirdDimension? = null, val latLngZs: List? = null ){} @@ -331,12 +330,12 @@ private data class TestCase( val testResult: String ){} -private class TestCaseReader(testInputFile: String, testResultFile: String) : Iterator { - private var totalLines = 0 - private var currentLine = 0 - private var testCases = mutableListOf() +private class TestCaseReader(testInputFile: String, testResultFile: String) { + val testCases: List init { + var totalLines = 0 + val testCaseList = mutableListOf() try { Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testInputFile)).use { input -> Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testResultFile)).use { result -> @@ -347,7 +346,7 @@ private class TestCaseReader(testInputFile: String, testResultFile: String) : It val testResultFileLine = result.readLine(); if (testInputFileLine != null && testInputFileLine.isNotBlank() && testResultFileLine != null && testResultFileLine.isNotBlank()) { - testCases.add( + testCaseList.add( TestCase( testInput = testInputFileLine.replace(regex, ""), testResult = testResultFileLine.replace(regex, "") @@ -355,6 +354,9 @@ private class TestCaseReader(testInputFile: String, testResultFile: String) : It ) totalLines++ } else { + if(totalLines==0) { + System.err.format("TestCaseReader - 0 test cases in file $testInputFile or $testResultFile:") + } break } } @@ -363,18 +365,8 @@ private class TestCaseReader(testInputFile: String, testResultFile: String) : It } catch (e: Exception) { e.printStackTrace() System.err.format("TestCaseReader - exception reading test case $testInputFile and $testResultFile at LineNo: $totalLines: %s%n", e) - throw RuntimeException("Test failed, as test data could not be loaded by TestCaseReader") + throw RuntimeException("TestCaseReader - exception reading test case $testInputFile and $testResultFile at LineNo: $totalLines", e) } - } - - override fun hasNext(): Boolean { - return currentLine < totalLines - } - - override fun next(): TestCase { - if(!hasNext()) throw NoSuchElementException() - val testCase = testCases[currentLine] - currentLine++ - return testCase + testCases = testCaseList.toList() } } \ No newline at end of file