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), "${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, 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")
)