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("")) { 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?): 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? = 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?, 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) 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() != "") { 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 { if (buffer.isShortClosing) { return buffer } return 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) { 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, 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): 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 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? = null var attributes: Map = 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, 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 = HashMap() var nested: List? = null var value: String? = null init { this.type = type } } companion object { val SKIPPING_SET = setOf(' ', '\n', '\r', '\t') } }