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/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
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
new file mode 100644
index 0000000..dd1d716
--- /dev/null
+++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxConstant.kt
@@ -0,0 +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
new file mode 100644
index 0000000..83f1b31
--- /dev/null
+++ b/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt
@@ -0,0 +1,365 @@
+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)
+
+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 {
+ 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")
+ }
+ }
+
+ 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)
+ 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() != "$tagName>") {
+ container = readValue(dis, container, tagName)
+ }
+ xmlObject.value = container.buffer.asString().replace("$tagName>", "")
+ 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, buffer, "${tagName}>")
+
+ private fun readValue(dis: InputStream, buffer: Container, tagName: String): Container =
+ readUntil(dis, buffer, "$tagName>")
+
+ 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, SKIPPING_SET)
+ 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 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.of(container.byte!!, container.position)
+ }
+
+ private fun readExactly(dis: InputStream, buffer: Container, charSequence: CharSequence): Container {
+ var container = buffer
+ do {
+ container = readByte(dis, container)
+ 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
+ }
+
+ 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 of 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()
+ var value: String? = null
+
+ 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(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 378433b..4f251d4 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,36 @@ 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 {
+ 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 38095a1..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.GpxWriter.Companion.DTF
-import me.bvn13.sdk.android.gpx.GpxWriter.Companion.HEADER
+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.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()
@@ -262,14 +260,8 @@ 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"
-
- 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
new file mode 100644
index 0000000..a63057d
--- /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..95201d7 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")
)
@@ -432,7 +432,7 @@ class GpxWriterTest {
value1
-
+
10.0
3.0
@@ -462,7 +462,7 @@ class GpxWriterTest {
-
+
track 1