From 68cf83f408759d78378d7f552551e66f5f77047f Mon Sep 17 00:00:00 2001 From: bvn13 Date: Thu, 29 Sep 2022 20:42:56 +0300 Subject: [PATCH 1/3] Update for next development version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6254e0b..b80527b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ GpxAndroidSdk me.bvn13.sdk.android.gpx - 1.3 + 1.4-SNAPSHOT jar From d2c728dc901fa1abdae6dfad569dfa6196dcf3d9 Mon Sep 17 00:00:00 2001 From: bvn13 Date: Thu, 15 Dec 2022 19:27:01 +0300 Subject: [PATCH 2/3] before refactoring --- GpxAndroidSdk.iml | 36 +- .../me/bvn13/sdk/android/gpx/GpxConstant.kt | 8 + .../me/bvn13/sdk/android/gpx/GpxReader.kt | 183 ++++++++ .../me/bvn13/sdk/android/gpx/GpxType.kt | 10 +- .../me/bvn13/sdk/android/gpx/GpxWriter.kt | 3 +- .../me/bvn13/sdk/android/gpx/GpxReaderTest.kt | 412 ++++++++++++++++++ .../me/bvn13/sdk/android/gpx/GpxWriterTest.kt | 2 +- 7 files changed, 615 insertions(+), 39 deletions(-) create mode 100644 src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt create mode 100644 src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt create mode 100644 src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt diff --git a/GpxAndroidSdk.iml b/GpxAndroidSdk.iml index 80463c4..2e806b0 100644 --- a/GpxAndroidSdk.iml +++ b/GpxAndroidSdk.iml @@ -40,42 +40,8 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt new file mode 100644 index 0000000..bff4080 --- /dev/null +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt @@ -0,0 +1,8 @@ +package me.bvn13.sdk.android.gpx + +class GpxConstant { + companion object { + const val HEADER = "" + const val VERSION = "1.1" + } +} \ No newline at end of file diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt new file mode 100644 index 0000000..1064dc9 --- /dev/null +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt @@ -0,0 +1,183 @@ +package me.bvn13.sdk.android.gpx + +import java.io.InputStream + +fun GpxType.Companion.read(dis: InputStream) = GpxReader().read(dis) + +fun ByteArray.asString() = String(this) + +class GpxReader { + fun read(dis: InputStream): GpxType { + val container = readNotSpace(dis, Container.empty()) + return when (container.byte!!.toInt()) { + '<'.code -> readBeginning(dis, container) + else -> throw IllegalArgumentException("Not a GPX/XML?") + } + } + + private fun readBeginning(dis: InputStream, buffer: Container): GpxType { + val container = readByte(dis, buffer) + return when (container.byte!!.toInt()) { + '?'.code -> readSignature(dis, container) + '<'.code -> readGpx(dis, container) + else -> throw IllegalArgumentException("Wrong symbol at ${container.position} from the very beginning") + } + } + + private fun readSignature(dis: InputStream, buffer: Container): GpxType { + val container = readUntil(dis, buffer, '\n') + if ("${GpxConstant.HEADER}\n" != container.buffer.asString()) { + throw IllegalArgumentException("Wrong xml signature!") + } + return readBeginning(dis, Container.empty(container.position)) + } + + private fun readGpx(dis: InputStream, buffer: Container): GpxType { + val container = readUntil(dis, buffer, setOf(' ', '\n')) + val tagName = container.buffer.asString().substring(1, container.buffer.size - 1).lowercase() + if ("gpx" != tagName) { + throw IllegalArgumentException("There must be GPX tag in given InputStream") + } + val xmlObject = XmlObject(tagName) + val attributesContainer = readAttributes(dis, container) + xmlObject.attributes = attributesContainer.attributes + val nestedContainer = readNestedObjects(dis, attributesContainer) + val finishingContainer = readFinishingTag(dis, nestedContainer, tagName) + return GpxType( + + MetadataType("test") + ) + } + + private fun readObject(dis: InputStream, buffer: Container, checker: (Container) -> Unit): Container { + val container = readUntil(dis, buffer, setOf(' ', '\n')) + val tagName = container.buffer.asString().substring(1, container.buffer.size - 1).lowercase() + checker.invoke(container) + if ("gpx" != tagName) { + throw IllegalArgumentException("There must be GPX tag in given InputStream") + } + val xmlObject = XmlObject(tagName) + val attributesContainer = readAttributes(dis, container) + xmlObject.attributes = attributesContainer.attributes + val nestedContainer = readNestedObjects(dis, attributesContainer) + val finishingContainer = readFinishingTag(dis, nestedContainer, tagName) + } + + private fun readNestedObjects(dis: InputStream, buffer: Container): Container { + + } + + private fun readFinishingTag(dis: InputStream, buffer: Container, tagName: String): Container = + readExactly(dis, Container.empty(buffer.position), "") + + private fun readAttributes(dis: InputStream, buffer: Container): Container { + if (buffer.byte!!.toInt() == '>'.code) { + return Container.empty(buffer.position) + } + val attributes = HashMap() + var attributeContainer: Container = Container.empty(buffer.position) + do { + attributeContainer = readAttribute(dis, attributeContainer) + attributes.putAll(attributeContainer.attributes) + } while (attributeContainer.attributes.isNotEmpty() && attributeContainer.byte!!.toInt() != '>'.code) + val result = Container.emptyWithAttributes(attributeContainer.position, attributes) + return result + } + + private fun readAttribute(dis: InputStream, buffer: Container): Container { + val nameContainer = readUntil(dis, buffer, setOf(' ', '\n', '=', '\t')) + val nameAsString = nameContainer.buffer.asString() + val name = nameAsString.substring(0, nameAsString.length - 1) + val equalsContainer = readUntil(dis, Container.empty(nameContainer.position), '"') + val valueContainer = readUntil(dis, Container.empty(equalsContainer.position), '"') + val valueAsString = valueContainer.buffer.asString() + val value = valueAsString.substring(0, valueAsString.length - 1) + val result = Container.empty(valueContainer.position) + val closingContainer = readSkipping(dis, result, setOf(' ', '\n', '\t')) + val nextBlockContainer = Container.of(closingContainer.byte!!, closingContainer.position) + nextBlockContainer.attributes = mapOf(name to value) + return nextBlockContainer + } + + private fun readUntil(dis: InputStream, buffer: Container, ch: Char): Container { + var container = buffer + do { + container = readByte(dis, container) + } while (container.byte!!.toInt() != ch.code) + return container + } + + private fun readUntil(dis: InputStream, buffer: Container, chs: Set): Container { + val chars = chs.map { c -> c.code } + var container = buffer + do { + container = readByte(dis, container) + } while (!chars.contains(container.byte!!.toInt())) + return container + } + + private fun readSkipping(dis: InputStream, buffer: Container, chs: Set): Container { + val chars = chs.map { c -> c.code } + var container = buffer + do { + container = readByte(dis, container) + } while (chars.contains(container.byte!!.toInt())) + return container + } + + private fun readExactly(dis: InputStream, buffer: Container, charSequence: CharSequence): Container { + var container = buffer + charSequence.forEach { + container = readByte(dis, container) + if (container.byte!!.toInt() != it.code) { + throw IllegalArgumentException("Expected closing tag $charSequence at position ${buffer.position}") + } + } + return container + } + + private fun readNotSpace(dis: InputStream, buffer: Container): Container { + val container = readByte(dis, buffer) + if (container.byte!!.toInt() == ' '.code + || container.byte.toInt() == '\n'.code + ) { + return readByte(dis, container) + } else { + return container + } + } + + private fun readByte(dis: InputStream, container: Container): Container { + val ba = ByteArray(1); + if (-1 == dis.read(ba, 0, 1)) { + throw InterruptedException("EOF") + } else if (ba.size != 1) { + throw InterruptedException("Reading 1 byte returns ${ba.size} bytes") + } + return Container(container.position + 1, ba[0], container.buffer.plus(ba)) + } + + class Container(val position: Long = 0, val byte: Byte?, val buffer: ByteArray) { + var objects: List? = null + var attributes: Map = HashMap() + + companion object { + fun empty(): Container = empty(0) + fun empty(position: Long) = Container(position, null, ByteArray(0)) + fun emptyWithAttributes(position: Long, attributes: Map): Container { + val container = Container.empty(position) + container.attributes = attributes + return container + } + fun of(b: Byte, position: Long) = Container(position, b, ByteArray(1) {_ -> b}) + } + + override fun toString(): String = this.buffer.asString() + } + + class XmlObject(val type: String) { + var attributes: Map = HashMap() + var nested: List? = null + } + +} \ No newline at end of file diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt index 378433b..5076f9a 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt @@ -77,6 +77,8 @@ limitations under the License. package me.bvn13.sdk.android.gpx +import me.bvn13.sdk.android.gpx.GpxConstant.Companion.VERSION + /** * GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements to the extensions section of the GPX document. * @@ -100,5 +102,11 @@ class GpxType( val trk: List? = null, val extensions: ExtensionType? = null ) { - val version: String = "1.1" + val version: String = VERSION + + override fun equals(other: Any?): Boolean { + // TODO implement it + return super.equals(other) + } + companion object { } } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt index 38095a1..7c89e05 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt @@ -77,8 +77,8 @@ limitations under the License. package me.bvn13.sdk.android.gpx +import me.bvn13.sdk.android.gpx.GpxConstant.Companion.HEADER import me.bvn13.sdk.android.gpx.GpxWriter.Companion.DTF -import me.bvn13.sdk.android.gpx.GpxWriter.Companion.HEADER import me.bvn13.sdk.android.gpx.GpxWriter.Companion.SCHEMA_LOCATION import me.bvn13.sdk.android.gpx.GpxWriter.Companion.XMLNS import me.bvn13.sdk.android.gpx.GpxWriter.Companion.XMLNS_XSI @@ -262,7 +262,6 @@ private fun String.removeEmptyStrings() = this.lineSequence().map { class GpxWriter { companion object { - const val HEADER = "" const val XMLNS = "http://www.topografix.com/GPX/1/1" const val XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance" const val SCHEMA_LOCATION = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" diff --git a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt new file mode 100644 index 0000000..c505e88 --- /dev/null +++ b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt @@ -0,0 +1,412 @@ +package me.bvn13.sdk.android.gpx + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.* +import kotlin.test.assertEquals + +class GpxReaderTest { + + @DisplayName("test GPX Reader") + @Test + fun testReader() { + val clock = Clock.fixed( + LocalDateTime.of(2022, 9, 24, 15, 4, 0, 0).toInstant(ZoneOffset.ofHours(3)), ZoneId.of("Europe/Moscow") + ) + + val gpxType = GpxType( + MetadataType("test name", description = "test description", authorName = "bvn13"), + wpt = listOf( + WptType( + lat = 14.64736838389662, + lon = 7.93212890625, + ele = 10.toDouble(), + time = OffsetDateTime.now(clock), + magvar = 3.toDouble(), + geoidheight = 45.toDouble(), + name = "test point 1", + cmt = "comment 1", + desc = "description of point 1", + link = listOf( + LinkType( + href = "http://link-to.site.href", + text = "text", + type = "hyperlink" + ), + LinkType( + href = "http://link2-to.site.href", + text = "text2", + type = "hyperlink2" + ) + ), + src = "source 1", + sym = "sym 1", + type = "type 1", + fix = FixType.DGPS, + sat = 1, + hdop = 55.toDouble(), + vdop = 66.toDouble(), + pdop = 77.toDouble(), + ageofgpsdata = 44, + dgpsid = 88, + extensions = listOf( + ExtensionType( + "extension1", + parameters = mapOf(Pair("first", "second"), Pair("third", "fours")) + ), + ExtensionType( + "extension2", + parameters = mapOf(Pair("aa", "bb"), Pair("cc", "dd")) + ) + ) + ) + ), + rte = listOf( + RteType( + name = "rte name", + cmt = "cmt", + desc = "desc", + src = "src", + link = listOf( + LinkType( + href = "https://new.link.rte", + text = "new text rte", + type = "hyperlink" + ) + ), + number = 1234, + type = "route", + extensions = listOf( + ExtensionType( + "ext-1", + value = "value1" + ) + ), + rtept = listOf( + WptType( + lat = 14.64736838389662, + lon = 7.93212890625, + ele = 10.toDouble(), + time = OffsetDateTime.now(clock), + magvar = 3.toDouble(), + geoidheight = 45.toDouble(), + name = "test point 1", + cmt = "comment 1", + desc = "description of point 1", + link = listOf( + LinkType( + href = "http://link-to.site.href", + text = "text", + type = "hyperlink" + ), + LinkType( + href = "http://link2-to.site.href", + text = "text2", + type = "hyperlink2" + ) + ), + src = "source 1", + sym = "sym 1", + type = "type 1", + fix = FixType.DGPS, + sat = 1, + hdop = 55.toDouble(), + vdop = 66.toDouble(), + pdop = 77.toDouble(), + ageofgpsdata = 44, + dgpsid = 88, + extensions = listOf( + ExtensionType( + "extension1", + parameters = mapOf(Pair("first", "second"), Pair("third", "fours")) + ), + ExtensionType( + "extension2", + parameters = mapOf(Pair("aa", "bb"), Pair("cc", "dd")) + ) + ) + ) + ) + ) + ), + trk = listOf( + TrkType( + name = "track 1", + cmt = "comment track 1", + desc = "desc track 1", + src = "src track 1", + number = 1234, + type = "type 1", + trkseg = listOf( + TrksegType( + listOf( + WptType( + lat = 14.64736838389662, + lon = 7.93212890625, + ele = 10.toDouble(), + time = OffsetDateTime.now(clock), + magvar = 3.toDouble(), + geoidheight = 45.toDouble(), + name = "test point 1", + cmt = "comment 1", + desc = "description of point 1", + link = listOf( + LinkType( + href = "http://link-to.site.href", + text = "text", + type = "hyperlink" + ), + LinkType( + href = "http://link2-to.site.href", + text = "text2", + type = "hyperlink2" + ) + ), + src = "source 1", + sym = "sym 1", + type = "type 1", + fix = FixType.DGPS, + sat = 1, + hdop = 55.toDouble(), + vdop = 66.toDouble(), + pdop = 77.toDouble(), + ageofgpsdata = 44, + dgpsid = 88, + extensions = listOf( + ExtensionType( + "extension1", + parameters = mapOf(Pair("first", "second"), Pair("third", "fours")) + ), + ExtensionType( + "extension2", + parameters = mapOf(Pair("aa", "bb"), Pair("cc", "dd")) + ) + ) + ), + WptType( + lat = 14.64736838389662, + lon = 7.93212890625, + ele = 10.toDouble(), + time = OffsetDateTime.now(clock), + magvar = 3.toDouble(), + geoidheight = 45.toDouble(), + name = "test point 1", + cmt = "comment 1", + desc = "description of point 1", + link = listOf( + LinkType( + href = "http://link-to.site.href", + text = "text", + type = "hyperlink" + ), + LinkType( + href = "http://link2-to.site.href", + text = "text2", + type = "hyperlink2" + ) + ), + src = "source 1", + sym = "sym 1", + type = "type 1", + fix = FixType.DGPS, + sat = 1, + hdop = 55.toDouble(), + vdop = 66.toDouble(), + pdop = 77.toDouble(), + ageofgpsdata = 44, + dgpsid = 88, + extensions = listOf( + ExtensionType( + "extension1", + parameters = mapOf(Pair("first", "second"), Pair("third", "fours")) + ), + ExtensionType( + "extension2", + parameters = mapOf(Pair("aa", "bb"), Pair("cc", "dd")) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + val gpxString = """ + + + + + test name + test description + + bvn13 + + + + 10.0 + + 3.0 + 45.0 + test point 1 + comment 1 + description of point 1 + source 1 + + text + hyperlink + + + text2 + hyperlink2 + + sym 1 + type 1 + dgps + 1 + 55.0 + 66.0 + 77.0 + 44 + 88 + + + + + + + rte name + cmt + desc + src + + new text rte + hyperlink + + 1234 + route + + value1 + + + 10.0 + + 3.0 + 45.0 + test point 1 + comment 1 + description of point 1 + source 1 + + text + hyperlink + + + text2 + hyperlink2 + + sym 1 + type 1 + dgps + 1 + 55.0 + 66.0 + 77.0 + 44 + 88 + + + + + + + + track 1 + comment track 1 + desc track 1 + src track 1 + 1234 + type 1 + + + 10.0 + + 3.0 + 45.0 + test point 1 + comment 1 + description of point 1 + source 1 + + text + hyperlink + + + text2 + hyperlink2 + + sym 1 + type 1 + dgps + 1 + 55.0 + 66.0 + 77.0 + 44 + 88 + + + + + + + 10.0 + + 3.0 + 45.0 + test point 1 + comment 1 + description of point 1 + source 1 + + text + hyperlink + + + text2 + hyperlink2 + + sym 1 + type 1 + dgps + 1 + 55.0 + 66.0 + 77.0 + 44 + 88 + + + + + + + + + """.trim() + .lineSequence() + .map { + it.trim() + } + .joinToString("\n") + + val gpx = GpxType.read(gpxString.byteInputStream()) + assertEquals(gpxType, gpx) + } +} \ No newline at end of file diff --git a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt index 21a683f..5172ee8 100644 --- a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt +++ b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt @@ -146,7 +146,7 @@ class GpxWriterTest { @DisplayName("test maximum") @Test - fun maximumTest() { + fun testMaximum() { val clock = Clock.fixed( LocalDateTime.of(2022, 9, 24, 15, 4, 0, 0).toInstant(ZoneOffset.ofHours(3)), ZoneId.of("Europe/Moscow") ) From 2e2f02ca62143cea9eeb4bd0f0ff22c9efc0eb37 Mon Sep 17 00:00:00 2001 From: bvn13 Date: Sun, 18 Dec 2022 13:02:13 +0300 Subject: [PATCH 3/3] fixed #1 - reader implemented --- .../me/bvn13/sdk/android/gpx/ExtensionType.kt | 22 ++ .../me/bvn13/sdk/android/gpx/GpxConstant.kt | 7 + .../me/bvn13/sdk/android/gpx/GpxReader.kt | 248 +++++++++++++++--- .../me/bvn13/sdk/android/gpx/GpxType.kt | 29 +- .../me/bvn13/sdk/android/gpx/GpxWriter.kt | 11 +- .../me/bvn13/sdk/android/gpx/LinkType.kt | 19 ++ .../me/bvn13/sdk/android/gpx/MetadataType.kt | 21 ++ .../me/bvn13/sdk/android/gpx/RteType.kt | 34 +++ .../me/bvn13/sdk/android/gpx/TrkType.kt | 34 +++ .../me/bvn13/sdk/android/gpx/TrksegType.kt | 17 ++ .../me/bvn13/sdk/android/gpx/WptType.kt | 58 ++++ .../me/bvn13/sdk/android/gpx/GpxReaderTest.kt | 4 +- .../me/bvn13/sdk/android/gpx/GpxWriterTest.kt | 4 +- 13 files changed, 460 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/ExtensionType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/ExtensionType.kt index dd120fd..fa6dc89 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/ExtensionType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/ExtensionType.kt @@ -98,4 +98,26 @@ class ExtensionType(val nodeName: String, val value: String? = null, val paramet "value or parameters must be specified" } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExtensionType + + if (nodeName != other.nodeName) return false + if (value != other.value) return false + if (parameters != other.parameters) return false + + return true + } + + override fun hashCode(): Int { + var result = nodeName.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + result = 31 * result + (parameters?.hashCode() ?: 0) + return result + } + + } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt index bff4080..dd1d716 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt @@ -1,8 +1,15 @@ package me.bvn13.sdk.android.gpx +import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME +import java.time.format.DateTimeFormatterBuilder + class GpxConstant { companion object { const val HEADER = "" const val VERSION = "1.1" + val DTF = + DateTimeFormatterBuilder().append(ISO_LOCAL_DATE_TIME) // use the existing formatter for date time + .appendOffset("+HH:MM", "+00:00") // set 'noOffsetText' to desired '+00:00' + .toFormatter() } } \ No newline at end of file diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt index 1064dc9..83f1b31 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt @@ -1,6 +1,8 @@ package me.bvn13.sdk.android.gpx +import me.bvn13.sdk.android.gpx.GpxConstant.Companion.DTF import java.io.InputStream +import java.time.OffsetDateTime fun GpxType.Companion.read(dis: InputStream) = GpxReader().read(dis) @@ -33,42 +35,197 @@ class GpxReader { } private fun readGpx(dis: InputStream, buffer: Container): GpxType { - val container = readUntil(dis, buffer, setOf(' ', '\n')) - val tagName = container.buffer.asString().substring(1, container.buffer.size - 1).lowercase() - if ("gpx" != tagName) { - throw IllegalArgumentException("There must be GPX tag in given InputStream") + var tagName: String? = null + val container = readObject(dis, buffer) { + tagName = it.buffer.asString().substring(1, it.buffer.size - 1).lowercase() + if ("gpx" != tagName) { + throw IllegalArgumentException("There must be GPX tag in given InputStream") + } + } + tagName?.let { + return assembleGpxType(container) + } ?: run { + throw IllegalArgumentException("Unable to read content") } - val xmlObject = XmlObject(tagName) - val attributesContainer = readAttributes(dis, container) - xmlObject.attributes = attributesContainer.attributes - val nestedContainer = readNestedObjects(dis, attributesContainer) - val finishingContainer = readFinishingTag(dis, nestedContainer, tagName) - return GpxType( - - MetadataType("test") - ) } - private fun readObject(dis: InputStream, buffer: Container, checker: (Container) -> Unit): Container { - val container = readUntil(dis, buffer, setOf(' ', '\n')) - val tagName = container.buffer.asString().substring(1, container.buffer.size - 1).lowercase() - checker.invoke(container) - if ("gpx" != tagName) { - throw IllegalArgumentException("There must be GPX tag in given InputStream") + private fun assembleGpxType(container: Container): GpxType { + if (container.objects == null) { + throw IllegalArgumentException("It was not parsed properly") } + if (container.objects!!.size != 1) { + throw IllegalArgumentException("It was parsed ${container.objects!!.size} objects, but only 1 is expected") + } + return findObject(container.objects, "gpx").let { + return GpxType( + metadata = assembleMetadataType(it.nested), + creator = findAttributeOrNull(it.attributes, "creator") ?: throw IllegalArgumentException("Gpx.Creator not found"), + wpt = findObjectsOrNull(it.nested ,"wpt")?.map { assembleWptType(it) }, + rte = findObjectsOrNull(it.nested, "rte")?.map { assembleRteType(it) }, + trk = findObjectsOrNull(it.nested, "trk")?.map { assembleTrkType(it) } + ) + } + } + + private fun assembleMetadataType(objects: List?): MetadataType = + findObject(objects, "metadata") + .let { + return MetadataType( + name = findObjectOrNull(it.nested, "name")?.value ?: throw IllegalArgumentException("Gpx.Metadata.Name not found"), + description = findObjectOrNull(it.nested, "desc")?.value ?: throw IllegalArgumentException("Gpx.Metadata.Description not found"), + authorName = findObject(it.nested, "author").let { author -> + findObject(author.nested, "name").value ?: throw IllegalArgumentException("Gpx.Metadata.Author.Name not found") + } + ) + } + + private fun assembleWptType(obj: XmlObject): WptType = + WptType( + lat = findAttribute(obj.attributes, "lat").toDouble(), + lon = findAttribute(obj.attributes, "lon").toDouble(), + ele = findObjectOrNull(obj.nested, "ele")?.value?.toDouble(), + time = findObjectOrNull(obj.nested, "time")?.value?.let { OffsetDateTime.parse(it, DTF) }, + magvar = findObjectOrNull(obj.nested, "magvar")?.value?.toDouble(), + geoidheight = findObjectOrNull(obj.nested, "geoidheight")?.value?.toDouble(), + name = findObjectOrNull(obj.nested, "name")?.value, + cmt = findObjectOrNull(obj.nested, "cmt")?.value, + desc = findObjectOrNull(obj.nested, "desc")?.value, + src = findObjectOrNull(obj.nested, "src")?.value, + link = findObjectsOrNull(obj.nested, "link")?.map { assembleLinkType(it) }, + sym = findObjectOrNull(obj.nested, "sym")?.value, + type = findObjectOrNull(obj.nested, "type")?.value, + fix = findObjectOrNull(obj.nested, "fix")?.value?.let { assembleFixType(it) }, + sat = findObjectOrNull(obj.nested, "sat")?.value?.toInt(), + hdop = findObjectOrNull(obj.nested, "hdop")?.value?.toDouble(), + vdop = findObjectOrNull(obj.nested, "vdop")?.value?.toDouble(), + pdop = findObjectOrNull(obj.nested, "pdop")?.value?.toDouble(), + ageofgpsdata = findObjectOrNull(obj.nested, "ageofgpsdata")?.value?.toInt(), + dgpsid = findObjectOrNull(obj.nested, "dgpsid")?.value?.toInt(), + extensions = findObjectOrNull(obj.nested, "extensions")?.nested?.map { assembleExtensionType(it) } + ) + + private fun assembleRteType(obj: XmlObject): RteType = + RteType( + name = findObjectOrNull(obj.nested, "name")?.value, + cmt = findObjectOrNull(obj.nested, "cmt")?.value, + desc = findObjectOrNull(obj.nested, "desc")?.value, + src = findObjectOrNull(obj.nested, "src")?.value, + link = findObjectsOrNull(obj.nested, "link")?.map { assembleLinkType(it) }, + number = findObjectOrNull(obj.nested, "number")?.value?.toInt(), + type = findObjectOrNull(obj.nested, "type")?.value, + extensions = findObjectOrNull(obj.nested, "extensions")?.nested?.map { assembleExtensionType(it) }, + rtept = findObjectsOrNull(obj.nested, "rtept")?.map { assembleWptType(it) } + ) + + private fun assembleTrkType(obj: XmlObject): TrkType = + TrkType( + name = findObjectOrNull(obj.nested, "name")?.value, + cmt = findObjectOrNull(obj.nested, "cmt")?.value, + desc = findObjectOrNull(obj.nested, "desc")?.value, + src = findObjectOrNull(obj.nested, "src")?.value, + link = findObjectsOrNull(obj.nested, "link")?.let { list -> if (list.isNotEmpty()) list.map { assembleLinkType(it) } else null }, + number = findObjectOrNull(obj.nested, "number")?.value?.toInt(), + type = findObjectOrNull(obj.nested, "type")?.value, + extensions = findObjectOrNull(obj.nested, "extensions")?.let { assembleExtensionType(it) }, + trkseg = findObjectsOrNull(obj.nested, "trkseg")?.map { assembleTrksegType(it) } + ) + + private fun assembleLinkType(obj: XmlObject): LinkType = + LinkType( + href = findAttribute(obj.attributes, "href"), + text = findObjectOrNull(obj.nested, "text")?.value, + type = findObjectOrNull(obj.nested, "type")?.value + ) + + private fun assembleFixType(value: String): FixType = + FixType.valueOf(value.uppercase()) + + private fun assembleExtensionType(obj: XmlObject): ExtensionType = + ExtensionType( + nodeName = obj.type, + value = if (obj.value == "") null else obj.value, + parameters = if (obj.attributes.isEmpty()) null else obj.attributes.toSortedMap() + ) + + private fun assembleTrksegType(obj: XmlObject): TrksegType = + TrksegType( + trkpt = findObjectsOrNull(obj.nested, "trkpt")?.map { assembleWptType(it) }, + extensions = findObjectOrNull(obj.nested, "extensions")?.let { assembleExtensionType(it) } + ) + + private fun findObjectOrNull(objects: List?, name: String): XmlObject? = + objects?.firstOrNull { o -> + o.type.lowercase() == name.lowercase() + } + + private fun findObject(objects: List?, name: String): XmlObject = + findObjectOrNull(objects, name) + ?: throw IllegalArgumentException("$name not found") + + private fun findObjectsOrNull(objects: List?, name: String): List? = + objects?.filter { o -> + o.type.lowercase() == name.lowercase() + } + + private fun findAttributeOrNull(attributes: Map?, name: String): String? = + attributes?.firstNotNullOf { e -> if (e.key.lowercase() == name.lowercase()) e.value else null } + + private fun findAttribute(attributes: Map?, name: String): String = + findAttributeOrNull(attributes, name) + ?: throw IllegalArgumentException("Unable to find attribute $name") + + private fun readObject(dis: InputStream, buffer: Container, checker: ((Container) -> Unit)? = null): Container { + if (buffer.buffer[0].toInt() != '<'.code) { + throw IllegalArgumentException("Not a tag at position ${buffer.position}") + } + var container = readUntil(dis, buffer, setOf(' ', '\n', '>')) + val tagName = container.buffer.asString().substring(1, container.buffer.size - 1).lowercase() + checker?.invoke(container) val xmlObject = XmlObject(tagName) - val attributesContainer = readAttributes(dis, container) - xmlObject.attributes = attributesContainer.attributes - val nestedContainer = readNestedObjects(dis, attributesContainer) - val finishingContainer = readFinishingTag(dis, nestedContainer, tagName) + if (container.byte!!.toInt() != '>'.code) { + container = readAttributes(dis, container) + xmlObject.attributes = container.attributes + } + container = readSkipping(dis, container, SKIPPING_SET) + if (container.byte!!.toInt() == '<'.code) { + container = readNestedObjects(dis, container) + xmlObject.nested = container.objects + container = readFinishingTag(dis, container, tagName) + } + if (container.buffer.asString() != "") { + container = readValue(dis, container, tagName) + } + xmlObject.value = container.buffer.asString().replace("", "") + container.objects = listOf(xmlObject) + return container } private fun readNestedObjects(dis: InputStream, buffer: Container): Container { - + val list = ArrayList() + var container: Container = readByte(dis, buffer) + if (container.byte!!.toInt() == '/'.code) { + return container + } + while (true) { + val objectContainer = readObject(dis, container) + val skippingContainer = readSkipping(dis, objectContainer, SKIPPING_SET) + container = readByte(dis, skippingContainer) + objectContainer.objects?.forEach { + list.add(it) + } + if (container.byte!!.toInt() == '/'.code) { + break; + } + } + container.objects = list + return container } private fun readFinishingTag(dis: InputStream, buffer: Container, tagName: String): Container = - readExactly(dis, Container.empty(buffer.position), "") + readExactly(dis, buffer, "") + + private fun readValue(dis: InputStream, buffer: Container, tagName: String): Container = + readUntil(dis, buffer, "") private fun readAttributes(dis: InputStream, buffer: Container): Container { if (buffer.byte!!.toInt() == '>'.code) { @@ -93,7 +250,7 @@ class GpxReader { val valueAsString = valueContainer.buffer.asString() val value = valueAsString.substring(0, valueAsString.length - 1) val result = Container.empty(valueContainer.position) - val closingContainer = readSkipping(dis, result, setOf(' ', '\n', '\t')) + val closingContainer = readSkipping(dis, result, SKIPPING_SET) val nextBlockContainer = Container.of(closingContainer.byte!!, closingContainer.position) nextBlockContainer.attributes = mapOf(name to value) return nextBlockContainer @@ -116,23 +273,37 @@ class GpxReader { return container } + private fun readUntil(dis: InputStream, buffer: Container, charSequence: CharSequence): Container { + var container = Container.of(buffer.byte!!, buffer.position) + var pos = 0 + do { + container = readByte(dis, container) + if (container.byte!!.toInt() == charSequence[pos].code) { + pos++ + } else { + pos = 0 + } + } while (pos < charSequence.length) + return container + } + private fun readSkipping(dis: InputStream, buffer: Container, chs: Set): Container { val chars = chs.map { c -> c.code } var container = buffer do { container = readByte(dis, container) } while (chars.contains(container.byte!!.toInt())) - return container + return Container.of(container.byte!!, container.position) } private fun readExactly(dis: InputStream, buffer: Container, charSequence: CharSequence): Container { var container = buffer - charSequence.forEach { + do { container = readByte(dis, container) - if (container.byte!!.toInt() != it.code) { + if (charSequence.substring(0, container.buffer.size) != container.buffer.asString()) { throw IllegalArgumentException("Expected closing tag $charSequence at position ${buffer.position}") } - } + } while (container.buffer.size < charSequence.length) return container } @@ -152,7 +323,7 @@ class GpxReader { if (-1 == dis.read(ba, 0, 1)) { throw InterruptedException("EOF") } else if (ba.size != 1) { - throw InterruptedException("Reading 1 byte returns ${ba.size} bytes") + throw InterruptedException("Reading of 1 byte returns ${ba.size} bytes") } return Container(container.position + 1, ba[0], container.buffer.plus(ba)) } @@ -160,6 +331,7 @@ class GpxReader { class Container(val position: Long = 0, val byte: Byte?, val buffer: ByteArray) { var objects: List? = null var attributes: Map = HashMap() + var value: String? = null companion object { fun empty(): Container = empty(0) @@ -169,15 +341,25 @@ class GpxReader { container.attributes = attributes return container } - fun of(b: Byte, position: Long) = Container(position, b, ByteArray(1) {_ -> b}) + + fun of(b: Byte, position: Long) = Container(position, b, ByteArray(1) { _ -> b }) } override fun toString(): String = this.buffer.asString() } - class XmlObject(val type: String) { + class XmlObject(type: String) { + val type: String var attributes: Map = HashMap() var nested: List? = null + var value: String? = null + + init { + this.type = type.lowercase() + } } + companion object { + val SKIPPING_SET = setOf(' ', '\n', '\r', '\t') + } } \ No newline at end of file diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt index 5076f9a..4f251d4 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxType.kt @@ -104,9 +104,34 @@ class GpxType( ) { val version: String = VERSION + override fun equals(other: Any?): Boolean { - // TODO implement it - return super.equals(other) + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GpxType + + if (metadata != other.metadata) return false + if (creator != other.creator) return false + if (wpt != other.wpt) return false + if (rte != other.rte) return false + if (trk != other.trk) return false + if (extensions != other.extensions) return false + if (version != other.version) return false + + return true } + + override fun hashCode(): Int { + var result = metadata.hashCode() + result = 31 * result + creator.hashCode() + result = 31 * result + (wpt?.hashCode() ?: 0) + result = 31 * result + (rte?.hashCode() ?: 0) + result = 31 * result + (trk?.hashCode() ?: 0) + result = 31 * result + (extensions?.hashCode() ?: 0) + result = 31 * result + version.hashCode() + return result + } + companion object { } } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt index 7c89e05..6160f32 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxWriter.kt @@ -77,15 +77,13 @@ limitations under the License. package me.bvn13.sdk.android.gpx +import me.bvn13.sdk.android.gpx.GpxConstant.Companion.DTF import me.bvn13.sdk.android.gpx.GpxConstant.Companion.HEADER -import me.bvn13.sdk.android.gpx.GpxWriter.Companion.DTF import me.bvn13.sdk.android.gpx.GpxWriter.Companion.SCHEMA_LOCATION import me.bvn13.sdk.android.gpx.GpxWriter.Companion.XMLNS import me.bvn13.sdk.android.gpx.GpxWriter.Companion.XMLNS_XSI import java.time.Clock import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME -import java.time.format.DateTimeFormatterBuilder fun GpxType.toXmlString(): String = this.toXmlString(null) @@ -156,7 +154,7 @@ fun RteType.toXmlString() = """ ${toXmlString(this.number, "number")} ${toXmlString(this.type, "type")} ${this.extensions?.toXmlString() ?: ""} - ${this.rtept?.toXmlString() ?: ""} + ${this.rtept?.toXmlString("rtept") ?: ""} """.trim().removeEmptyStrings() @@ -265,10 +263,5 @@ class GpxWriter { const val XMLNS = "http://www.topografix.com/GPX/1/1" const val XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance" const val SCHEMA_LOCATION = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" - - internal val DTF = - DateTimeFormatterBuilder().append(ISO_LOCAL_DATE_TIME) // use the existing formatter for date time - .appendOffset("+HH:MM", "+00:00") // set 'noOffsetText' to desired '+00:00' - .toFormatter() } } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/LinkType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/LinkType.kt index 4f20c40..f047716 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/LinkType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/LinkType.kt @@ -87,4 +87,23 @@ package me.bvn13.sdk.android.gpx * [type] Mime type of content (image/jpeg) */ class LinkType(val href: String, val text: String? = null, val type: String? = null) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LinkType + + if (href != other.href) return false + if (text != other.text) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = href.hashCode() + result = 31 * result + (text?.hashCode() ?: 0) + result = 31 * result + (type?.hashCode() ?: 0) + return result + } } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/MetadataType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/MetadataType.kt index e0d880c..a192ea4 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/MetadataType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/MetadataType.kt @@ -82,4 +82,25 @@ package me.bvn13.sdk.android.gpx * Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. */ class MetadataType(val name: String, val description: String = "", val authorName: String = "") { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MetadataType + + if (name != other.name) return false + if (description != other.description) return false + if (authorName != other.authorName) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + authorName.hashCode() + return result + } + } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/RteType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/RteType.kt index 94d6fbc..2e63b26 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/RteType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/RteType.kt @@ -114,4 +114,38 @@ class RteType( "number must be non negative Integer" } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RteType + + if (name != other.name) return false + if (cmt != other.cmt) return false + if (desc != other.desc) return false + if (src != other.src) return false + if (link != other.link) return false + if (number != other.number) return false + if (type != other.type) return false + if (extensions != other.extensions) return false + if (rtept != other.rtept) return false + + return true + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (cmt?.hashCode() ?: 0) + result = 31 * result + (desc?.hashCode() ?: 0) + result = 31 * result + (src?.hashCode() ?: 0) + result = 31 * result + (link?.hashCode() ?: 0) + result = 31 * result + (number ?: 0) + result = 31 * result + (type?.hashCode() ?: 0) + result = 31 * result + (extensions?.hashCode() ?: 0) + result = 31 * result + (rtept?.hashCode() ?: 0) + return result + } + + } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/TrkType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/TrkType.kt index 8d86136..fdea173 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/TrkType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/TrkType.kt @@ -116,4 +116,38 @@ class TrkType( "number must be non negative Integer" } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TrkType + + if (name != other.name) return false + if (cmt != other.cmt) return false + if (desc != other.desc) return false + if (src != other.src) return false + if (link != other.link) return false + if (number != other.number) return false + if (type != other.type) return false + if (extensions != other.extensions) return false + if (trkseg != other.trkseg) return false + + return true + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (cmt?.hashCode() ?: 0) + result = 31 * result + (desc?.hashCode() ?: 0) + result = 31 * result + (src?.hashCode() ?: 0) + result = 31 * result + (link?.hashCode() ?: 0) + result = 31 * result + (number ?: 0) + result = 31 * result + (type?.hashCode() ?: 0) + result = 31 * result + (extensions?.hashCode() ?: 0) + result = 31 * result + (trkseg?.hashCode() ?: 0) + return result + } + + } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/TrksegType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/TrksegType.kt index 8d7ac81..7c5f9db 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/TrksegType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/TrksegType.kt @@ -88,4 +88,21 @@ class TrksegType( val trkpt: List? = null, val extensions: ExtensionType? = null ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TrksegType + + if (trkpt != other.trkpt) return false + if (extensions != other.extensions) return false + + return true + } + + override fun hashCode(): Int { + var result = trkpt?.hashCode() ?: 0 + result = 31 * result + (extensions?.hashCode() ?: 0) + return result + } } diff --git a/src/main/kotlin/me/bvn13/sdk/android/gpx/WptType.kt b/src/main/kotlin/me/bvn13/sdk/android/gpx/WptType.kt index ed1e6d1..e42a1bd 100644 --- a/src/main/kotlin/me/bvn13/sdk/android/gpx/WptType.kt +++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/WptType.kt @@ -162,4 +162,62 @@ class WptType( "dgpsid must be in 0..1023" } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WptType + + if (lat != other.lat) return false + if (lon != other.lon) return false + if (ele != other.ele) return false + if (time != other.time) return false + if (magvar != other.magvar) return false + if (geoidheight != other.geoidheight) return false + if (name != other.name) return false + if (cmt != other.cmt) return false + if (desc != other.desc) return false + if (src != other.src) return false + if (link != other.link) return false + if (sym != other.sym) return false + if (type != other.type) return false + if (fix != other.fix) return false + if (sat != other.sat) return false + if (hdop != other.hdop) return false + if (vdop != other.vdop) return false + if (pdop != other.pdop) return false + if (ageofgpsdata != other.ageofgpsdata) return false + if (dgpsid != other.dgpsid) return false + if (extensions != other.extensions) return false + + return true + } + + override fun hashCode(): Int { + var result = lat.hashCode() + result = 31 * result + lon.hashCode() + result = 31 * result + (ele?.hashCode() ?: 0) + result = 31 * result + (time?.hashCode() ?: 0) + result = 31 * result + (magvar?.hashCode() ?: 0) + result = 31 * result + (geoidheight?.hashCode() ?: 0) + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + (cmt?.hashCode() ?: 0) + result = 31 * result + (desc?.hashCode() ?: 0) + result = 31 * result + (src?.hashCode() ?: 0) + result = 31 * result + (link?.hashCode() ?: 0) + result = 31 * result + (sym?.hashCode() ?: 0) + result = 31 * result + (type?.hashCode() ?: 0) + result = 31 * result + (fix?.hashCode() ?: 0) + result = 31 * result + (sat ?: 0) + result = 31 * result + (hdop?.hashCode() ?: 0) + result = 31 * result + (vdop?.hashCode() ?: 0) + result = 31 * result + (pdop?.hashCode() ?: 0) + result = 31 * result + (ageofgpsdata ?: 0) + result = 31 * result + (dgpsid ?: 0) + result = 31 * result + (extensions?.hashCode() ?: 0) + return result + } + + } diff --git a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt index c505e88..a63057d 100644 --- a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt +++ b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxReaderTest.kt @@ -294,7 +294,7 @@ class GpxReaderTest { value1 - + 10.0 3.0 @@ -324,7 +324,7 @@ class GpxReaderTest { - + track 1 diff --git a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt index 5172ee8..95201d7 100644 --- a/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt +++ b/src/test/kotlin/me/bvn13/sdk/android/gpx/GpxWriterTest.kt @@ -432,7 +432,7 @@ class GpxWriterTest { value1 - + 10.0 3.0 @@ -462,7 +462,7 @@ class GpxWriterTest { - + track 1