GpxAndroidSdk/src/main/kotlin/me/bvn13/sdk/android/gpx/GpxReader.kt

414 lines
18 KiB
Kotlin

package me.bvn13.sdk.android.gpx
import me.bvn13.sdk.android.gpx.GpxConstant.Companion.DTF
import java.io.InputStream
import java.time.OffsetDateTime
import java.util.stream.Collectors
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')
val signaturePrepared = container.buffer.asString()
.trim()
.replace("'", "\"")
.replace(" ?", "?")
if (!signaturePrepared.startsWith("<?xml") || !signaturePrepared.endsWith("?>")) {
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) },
extensions = findObjectOrNull(it.nested, "extensions")?.let { assembleExtensionType(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
?: "",
authorName = findObjectOrNull(it.nested, "author").let { author ->
findObjectOrNull(author?.nested, "name")?.value
?: ""
}
)
}
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 {
val nested: List<ExtensionType>? = obj.nested?.stream()
?.map { assembleExtensionType(it) }
?.collect(Collectors.toList())
return ExtensionType(
nodeName = obj.type,
value = if (obj.value == "") null else obj.value,
parameters = if (obj.attributes.isEmpty()) null else obj.attributes.toSortedMap(),
nested = nested
)
}
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)
checker?.invoke(container)
val xmlObject = XmlObject(tagName)
if (container.byte!!.toInt() != '>'.code) {
container = readAttributes(dis, container)
xmlObject.attributes = container.attributes
}
if (!container.isShortClosing) {
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 {
if (buffer.isShortClosing) {
return buffer
}
return 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<String, String>()
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, attributeContainer.isShortClosing)
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)
var closingContainer = readSkipping(dis, result, SKIPPING_SET)
var isShortClosing = false
if (closingContainer.byte!!.toInt() == '/'.code) {
val endingContainer = readByte(dis, closingContainer)
if (endingContainer.byte!!.toInt() != '>'.code) {
throw IllegalArgumentException("There must be valid closing tag at ${endingContainer.position}")
}
closingContainer = endingContainer
isShortClosing = true
}
val nextBlockContainer = Container.of(closingContainer.byte!!, closingContainer.position, isShortClosing)
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<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
}
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.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 at ${container.position}\nUnparsed data: " + String(container.buffer))
} else if (ba.size != 1) {
throw InterruptedException("Reading of 1 byte returns ${ba.size} bytes at ${container.position}")
}
return Container(container.position + 1, ba[0], container.buffer.plus(ba))
}
class Container(
val position: Long = 0,
val byte: Byte?,
val buffer: ByteArray,
val isShortClosing: Boolean = false
) {
var objects: List<XmlObject>? = null
var attributes: Map<String, String> = HashMap()
var value: String? = null
companion object {
fun empty(): Container = empty(0)
fun empty(position: Long, isShortClosing: Boolean = false) = Container(position, null, ByteArray(0), isShortClosing)
fun emptyWithAttributes(position: Long, attributes: Map<String, String>, isShortClosing: Boolean = false): Container {
val container = Container.empty(position, isShortClosing)
container.attributes = attributes
return container
}
fun of(b: Byte, position: Long, isShortClosing: Boolean = false) =
Container(position, b, ByteArray(1) { _ -> b }, isShortClosing)
fun isShortClosing(c: Container, buffer: ByteArray): Container {
val container = Container(c.position, c.byte, buffer, isShortClosing = true)
container.objects = c.objects
container.attributes = c.attributes
container.value = c.value
return container
}
}
override fun toString(): String = this.buffer.asString()
}
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
}
}
companion object {
val SKIPPING_SET = setOf(' ', '\n', '\r', '\t')
}
}