fixed #1 - reader implemented

pull/2/head
bvn13 2022-12-18 13:02:13 +03:00
parent d2c728dc90
commit 2e2f02ca62
13 changed files with 460 additions and 48 deletions

View File

@ -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
}
}

View File

@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
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()
}
}

View File

@ -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<XmlObject>?): 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<XmlObject>?, name: String): XmlObject? =
objects?.firstOrNull { o ->
o.type.lowercase() == name.lowercase()
}
private fun findObject(objects: List<XmlObject>?, name: String): XmlObject =
findObjectOrNull(objects, name)
?: throw IllegalArgumentException("$name not found")
private fun findObjectsOrNull(objects: List<XmlObject>?, name: String): List<XmlObject>? =
objects?.filter { o ->
o.type.lowercase() == name.lowercase()
}
private fun findAttributeOrNull(attributes: Map<String, String>?, name: String): String? =
attributes?.firstNotNullOf { e -> if (e.key.lowercase() == name.lowercase()) e.value else null }
private fun findAttribute(attributes: Map<String, String>?, 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() != "</$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<XmlObject>()
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), "</${tagName}>")
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) {
@ -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<Char>): 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<XmlObject>? = null
var attributes: Map<String, String> = 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<String, String> = HashMap()
var nested: List<XmlObject>? = null
var value: String? = null
init {
this.type = type.lowercase()
}
}
companion object {
val SKIPPING_SET = setOf(' ', '\n', '\r', '\t')
}
}

View File

@ -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 { }
}

View File

@ -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") ?: ""}
</rte>
""".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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -88,4 +88,21 @@ class TrksegType(
val trkpt: List<WptType>? = 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
}
}

View File

@ -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
}
}

View File

@ -294,7 +294,7 @@ class GpxReaderTest {
<extensions>
<ext-1>value1</ext-1>
</extensions>
<wpt lat="14.64736838389662" lon="7.93212890625">
<rtept lat="14.64736838389662" lon="7.93212890625">
<ele>10.0</ele>
<time>2022-09-24T15:04:00+03:00</time>
<magvar>3.0</magvar>
@ -324,7 +324,7 @@ class GpxReaderTest {
<extension1 first="second" third="fours"></extension1>
<extension2 aa="bb" cc="dd"></extension2>
</extensions>
</wpt>
</rtept>
</rte>
<trk>
<name>track 1</name>

View File

@ -432,7 +432,7 @@ class GpxWriterTest {
<extensions>
<ext-1>value1</ext-1>
</extensions>
<wpt lat="14.64736838389662" lon="7.93212890625">
<rtept lat="14.64736838389662" lon="7.93212890625">
<ele>10.0</ele>
<time>2022-09-24T15:04:00+03:00</time>
<magvar>3.0</magvar>
@ -462,7 +462,7 @@ class GpxWriterTest {
<extension1 first="second" third="fours"></extension1>
<extension2 aa="bb" cc="dd"></extension2>
</extensions>
</wpt>
</rtept>
</rte>
<trk>
<name>track 1</name>