Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gpx/api/gpx.api
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public final class org/maplibre/spatialk/gpx/Route {
public final fun getLink ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getNumber ()Ljava/lang/Integer;
public final fun getRoutePoints ()Ljava/util/List;
public final fun getPoints ()Ljava/util/List;
public final fun getSource ()Ljava/lang/String;
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
Expand Down
4 changes: 2 additions & 2 deletions gpx/api/gpx.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ final class org.maplibre.spatialk.gpx/Route { // org.maplibre.spatialk.gpx/Route
final fun <get-name>(): kotlin/String? // org.maplibre.spatialk.gpx/Route.name.<get-name>|<get-name>(){}[0]
final val number // org.maplibre.spatialk.gpx/Route.number|{}number[0]
final fun <get-number>(): kotlin/Int? // org.maplibre.spatialk.gpx/Route.number.<get-number>|<get-number>(){}[0]
final val routePoints // org.maplibre.spatialk.gpx/Route.routePoints|{}routePoints[0]
final fun <get-routePoints>(): kotlin.collections/List<org.maplibre.spatialk.gpx/Waypoint> // org.maplibre.spatialk.gpx/Route.routePoints.<get-routePoints>|<get-routePoints>(){}[0]
final val points // org.maplibre.spatialk.gpx/Route.points|{}points[0]
final fun <get-points>(): kotlin.collections/List<org.maplibre.spatialk.gpx/Waypoint> // org.maplibre.spatialk.gpx/Route.points.<get-points>|<get-points>(){}[0]
final val source // org.maplibre.spatialk.gpx/Route.source|{}source[0]
final fun <get-source>(): kotlin/String? // org.maplibre.spatialk.gpx/Route.source.<get-source>|<get-source>(){}[0]
final val type // org.maplibre.spatialk.gpx/Route.type|{}type[0]
Expand Down
1 change: 1 addition & 0 deletions gpx/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ kotlin {
implementation(libs.jetbrains.annotations)
implementation(libs.xmlutil.core)
implementation(libs.xmlutil.serialization)
implementation(libs.kotlinx.datetime)
}

commonTest.dependencies {
Expand Down
13 changes: 10 additions & 3 deletions gpx/src/commonMain/kotlin/org/maplibre/spatialk/gpx/Document.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package org.maplibre.spatialk.gpx

import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import org.maplibre.spatialk.gpx.serializers.UtcDefaultInstantSerializer

/**
* Represents the root element of a GPX file.
Expand All @@ -26,14 +29,15 @@ import nl.adaptivity.xmlutil.serialization.XmlSerialName
* @property tracks A list of tracks.
*/
@XmlSerialName("gpx", "http://www.topografix.com/GPX/1/1")
@OptIn(ExperimentalSerializationApi::class)
@Serializable
public data class Document(
@Required
@EncodeDefault
@XmlSerialName("schemaLocation", "http://www.w3.org/2001/XMLSchema-instance", "xsi")
val schemaLocation: String =
"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd",
@Required val version: String = "1.1",
@Required val creator: String = "org.maplibre.spatialk.gpx",
@EncodeDefault val creator: String = "org.maplibre.spatialk.gpx",
@XmlSerialName("metadata") @XmlElement val metadata: Metadata? = null,
@SerialName("tracks") @XmlSerialName("trk") @XmlElement val tracks: List<Track> = listOf(),
@SerialName("routes") @XmlSerialName("rte") @XmlElement val routes: List<Route> = listOf(),
Expand Down Expand Up @@ -68,7 +72,10 @@ constructor(
@SerialName("author") @XmlSerialName("author") @XmlElement val author: Author? = null,
@XmlSerialName("copyright") @XmlElement val copyright: Copyright? = null,
@XmlSerialName("link") @XmlElement val link: List<Link> = listOf(),
@SerialName("time") @XmlElement val timestamp: Instant? = null,
@Serializable(with = UtcDefaultInstantSerializer::class)
@SerialName("time")
@XmlElement
val timestamp: Instant? = null,
@XmlElement val keywords: String? = null,
@XmlSerialName("bounds") @XmlElement val bounds: Bounds? = null,
// val extensions: Extensions?,
Expand Down
8 changes: 4 additions & 4 deletions gpx/src/commonMain/kotlin/org/maplibre/spatialk/gpx/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import org.maplibre.spatialk.geojson.Point
* @property number A GPS route number.
* @property type The type of route. This is for categorizing the route and can be user-defined
* (e.g., "resupply", "scenic").
* @property routePoints A list of route points ([Waypoint]) which are the turning points,
* intersections, or other critical points in the route.
* @property points A list of route points ([Waypoint]) which are the turning points, intersections,
* or other critical points in the route.
*/
@Serializable
public data class Route(
Expand All @@ -35,7 +35,7 @@ public data class Route(
@SerialName("link") @XmlElement val link: String?,
@SerialName("number") @XmlElement val number: Int?,
@SerialName("type") @XmlElement val type: String?,
@SerialName("rtept") @XmlSerialName("rtept") @XmlElement val routePoints: List<Waypoint>,
@SerialName("rtept") @XmlSerialName("rtept") @XmlElement val points: List<Waypoint>,
// @XmlElement val extensions = null,
)

Expand All @@ -48,5 +48,5 @@ public data class Route(
* @return A GeoJSON [Feature] representing this route.
*/
public fun Route.toGeoJson(): Feature<GeometryCollection<Point>, Route> {
return Feature(GeometryCollection(routePoints.map { it.toGeoJson().geometry }), this)
return Feature(GeometryCollection(points.map { it.toGeoJson().geometry }), this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.Point
import org.maplibre.spatialk.geojson.Position
import org.maplibre.spatialk.gpx.serializers.UtcDefaultInstantSerializer

/**
* Represents a waypoint, point of interest, or named feature on a map. This corresponds to the
Expand Down Expand Up @@ -48,7 +49,10 @@ public data class Waypoint(
@SerialName("lat") val latitude: Double,
@SerialName("lon") val longitude: Double,
@SerialName("ele") @XmlElement val elevation: Double? = null,
@SerialName("time") @XmlElement val timestamp: Instant? = null,
@Serializable(with = UtcDefaultInstantSerializer::class)
@SerialName("time")
@XmlElement
val timestamp: Instant? = null,
@SerialName("magvar") @XmlElement val magneticVariation: Double? = null,
@SerialName("geoidheight") @XmlElement val geoIdHeight: Double? = null,
@XmlElement val name: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.maplibre.spatialk.gpx.serializers

import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@OptIn(ExperimentalTime::class)
internal object UtcDefaultInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(
"org.maplibre.spatialk.gpx.serializers.UtcDefaultInstantSerializer",
PrimitiveKind.STRING,
)

override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}

override fun deserialize(decoder: Decoder): Instant {
val text = decoder.decodeString()
return try {
Instant.parse(text)
} catch (_: Exception) {
LocalDateTime.parse(text).toInstant(TimeZone.UTC)
}
}
}
20 changes: 14 additions & 6 deletions gpx/src/commonTest/kotlin/org/maplibre/spatialk/gpx/GPX.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,32 @@ class GpxTest {
document.waypoints.toGeoJson(),
)

stripEquals(readResourceFile("out/waypoints.gpx"), Gpx.encodeToString(document))
assertEncodedEquals(readResourceFile("out/waypoints.gpx"), document)
}

@Test
fun testTrack() {
val document = Gpx.decodeFromString(readResourceFile("in/track.gpx"))
stripEquals(readResourceFile("out/track.gpx"), Gpx.encodeToString(document))
assertEncodedEquals(
readResourceFile("out/track.gpx"),
Gpx.decodeFromString(readResourceFile("in/track.gpx")),
)

assertEncodedEquals(
readResourceFile("out/track.gpx"),
Gpx.decodeFromString(readResourceFile("in/track_lenient.gpx"))
.copy(creator = "ChatGPT-GPX"),
)
}

@Test
fun testRoute() {
val document = Gpx.decodeFromString(readResourceFile("in/route.gpx"))
stripEquals(readResourceFile("out/route.gpx"), Gpx.encodeToString(document))
assertEncodedEquals(readResourceFile("out/route.gpx"), document)
}

fun stripEquals(expected: String, actual: String) {
fun assertEncodedEquals(expected: String, actual: Document) {
val expected = expected.replace(Regex("\\s+"), "")
val actual = actual.replace(Regex("\\s+"), "")
val actual = Gpx.encodeToString(actual).replace(Regex("\\s+"), "")

// dirty hack for different number formatting on nodeJs platform
if (!actual.contains(".0<")) {
Expand Down
104 changes: 104 additions & 0 deletions gpx/src/commonTest/resources/in/track_lenient.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?><!--
we tolerate minor standard violations
* no schemaLocation
* no creator
* timestamps without time zones
* unknown metadata element
-->
<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<metadata>
<name>Sample Track</name>
<desc>Example GPX file showing a multi-segment track.</desc>
<author>
<name>ChatGPT</name>
<email id="support" domain="example.com" />
</author>
<link href="https://example.com">
<text>Sample GPX Resource</text>
<type>text/html</type>
</link>
<time>2025-10-22T12:00:00</time>
<keywords>gpx,track,example,gps</keywords>
<bounds minlat="37.7694" minlon="-122.4862" maxlat="37.8021" maxlon="-122.4194" />
<foobar>baz</foobar>
</metadata>

<trk>
<name>City Loop Track</name>
<cmt>Sample track through San Francisco</cmt>
<desc>This track simulates a route starting downtown, going through a park, and ending at a
viewpoint.
</desc>
<src>Simulated GPS Data</src>
<link href="https://example.com/trackinfo">
<text>Track Info</text>
</link>
<number>1</number>
<type>Loop</type>

<trkseg>
<trkpt lat="37.7749" lon="-122.4194">
<ele>15.2</ele>
<time>2025-10-22T12:00:00</time>
<course>90.0</course>
<speed>1.4</speed>
<magvar>12.5</magvar>
<geoidheight>-29.0</geoidheight>
<sat>10</sat>
<hdop>0.9</hdop>
<vdop>1.2</vdop>
<pdop>1.6</pdop>
<fix>3d</fix>
</trkpt>

<trkpt lat="37.7765" lon="-122.4290">
<ele>22.8</ele>
<time>2025-10-22T12:05:00</time>
<course>92.0</course>
<speed>1.6</speed>
<fix>3d</fix>
<sat>9</sat>
</trkpt>

<trkpt lat="37.7780" lon="-122.4410">
<ele>35.6</ele>
<time>2025-10-22T12:10:00</time>
<course>100.0</course>
<speed>1.8</speed>
<fix>3d</fix>
<sat>8</sat>
</trkpt>
</trkseg>

<trkseg>
<trkpt lat="37.7694" lon="-122.4862">
<ele>48.3</ele>
<time>2025-10-22T12:20:00</time>
<course>110.0</course>
<speed>2.0</speed>
<fix>dgps</fix>
<sat>9</sat>
</trkpt>

<trkpt lat="37.7790" lon="-122.4600">
<ele>62.7</ele>
<time>2025-10-22T12:30:00</time>
<course>120.0</course>
<speed>2.2</speed>
<fix>3d</fix>
<sat>10</sat>
</trkpt>

<trkpt lat="37.8021" lon="-122.4488">
<ele>90.4</ele>
<time>2025-10-22T12:45:00</time>
<course>135.0</course>
<speed>0.0</speed>
<fix>3d</fix>
<sat>12</sat>
</trkpt>
</trkseg>

</trk>
</gpx>
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ kotlin = "2.2.20"
kotlinx-benchmark = "0.4.14"
kotlinx-kover = "0.9.3"
kotlinx-serialization = "1.9.0"
kotlinx-io = "0.8.0"
kotlinx-datetime = "0.7.1"
vanniktech-publish = "0.34.0"
dokka = "2.1.0"
kotlinx-io = "0.8.0"
semver = "0.8.0"
mkdocs-build = "4.0.1"
detekt = "2.0.0-alpha.0"
Expand All @@ -20,6 +21,7 @@ kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-seria
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
kotlinx-benchmark = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" }
kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
gradle-kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
gradle-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
Expand Down
5 changes: 5 additions & 0 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"

"@js-joda/[email protected]":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==

"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
Expand Down