From 5da23b83c1d5d1908887bf5a51e397aebfdb117c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 26 Dec 2022 23:22:21 +0300 Subject: [PATCH] Add GeoJson bindings --- demo/maps/src/jvmMain/kotlin/Main.kt | 6 +- .../center/sciprog/maps/compose/MapViewJvm.kt | 8 +- .../maps/features/FeatureCollection.kt | 14 ++- maps-kt-geojson/build.gradle.kts | 17 +++ .../center/sciprog/maps/geojson/GeoJson.kt | 74 ++++++++++++ .../sciprog/maps/geojson/GeoJsonGeometry.kt | 113 ++++++++++++++++++ .../sciprog/maps/geojson/geoJsonFeature.kt | 84 +++++++++++++ settings.gradle.kts | 1 + 8 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 maps-kt-geojson/build.gradle.kts create mode 100644 maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt create mode 100644 maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt create mode 100644 maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 908008a..8146799 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -68,9 +68,9 @@ fun App() { val marker2 = rectangle(55.8 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta) val marker3 = rectangle(56.0 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta) - draggableLine(marker1, marker2) - draggableLine(marker2, marker3) - draggableLine(marker3, marker1) + draggableLine(marker1, marker2, color = Color.Blue) + draggableLine(marker2, marker3, color = Color.Blue) + draggableLine(marker3, marker1, color = Color.Blue) points( points = listOf( diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index ee7d75d..3554475 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -111,14 +111,14 @@ public actual fun MapView( clipRect { val tileSize = IntSize( - ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(), - ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt() + ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(), + ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt() ) mapTiles.forEach { (id, image) -> //converting back from tile index to screen offset val offset = IntOffset( - (canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(), - (canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx() + (canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(), + (canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx() ) drawImage( image = image.toComposeImageBitmap(), diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt index 4473813..e16cb50 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt @@ -27,6 +27,8 @@ public interface FeatureBuilder { public fun > feature(id: String?, feature: F): FeatureId public fun , V> setAttribute(id: FeatureId, key: Feature.Attribute, value: V?) + + public val defaultColor: Color get() = Color.Red } public fun > FeatureBuilder.feature(id: FeatureId, feature: F): FeatureId = @@ -167,7 +169,7 @@ public fun FeatureBuilder.circle( center: T, zoomRange: FloatRange = defaultZoomRange, size: Dp = 5.dp, - color: Color = Color.Red, + color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( id, CircleFeature(coordinateSpace, center, zoomRange, size, color) @@ -177,7 +179,7 @@ public fun FeatureBuilder.rectangle( centerCoordinates: T, zoomRange: FloatRange = defaultZoomRange, size: DpSize = DpSize(5.dp, 5.dp), - color: Color = Color.Red, + color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color) @@ -197,7 +199,7 @@ public fun FeatureBuilder.line( aCoordinates: T, bCoordinates: T, zoomRange: FloatRange = defaultZoomRange, - color: Color = Color.Red, + color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( id, @@ -209,7 +211,7 @@ public fun FeatureBuilder.arc( startAngle: Float, arcLength: Float, zoomRange: FloatRange = defaultZoomRange, - color: Color = Color.Red, + color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( id, @@ -220,7 +222,7 @@ public fun FeatureBuilder.points( points: List, zoomRange: FloatRange = defaultZoomRange, stroke: Float = 2f, - color: Color = Color.Red, + color: Color = defaultColor, pointMode: PointMode = PointMode.Points, id: String? = null, ): FeatureId> = @@ -268,7 +270,7 @@ public fun FeatureBuilder.text( position: T, text: String, zoomRange: FloatRange = defaultZoomRange, - color: Color = Color.Red, + color: Color = defaultColor, font: FeatureFont.() -> Unit = { size = 16f }, id: String? = null, ): FeatureId> = feature( diff --git a/maps-kt-geojson/build.gradle.kts b/maps-kt-geojson/build.gradle.kts new file mode 100644 index 0000000..8d9cced --- /dev/null +++ b/maps-kt-geojson/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.mapsKtCore) + api(projects.mapsKtFeatures) + api(spclibs.kotlinx.serialization.json) + } + } + } +} \ No newline at end of file diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt new file mode 100644 index 0000000..989f40c --- /dev/null +++ b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt @@ -0,0 +1,74 @@ +package center.sciprog.maps.geojson + +import center.sciprog.maps.geojson.GeoJson.Companion.PROPERTIES_KEY +import center.sciprog.maps.geojson.GeoJson.Companion.TYPE_KEY +import center.sciprog.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY +import kotlinx.serialization.json.* +import kotlin.jvm.JvmInline + +/** + * A utility class to work with GeoJson (https://geojson.org/) + */ +public sealed interface GeoJson { + public val json: JsonObject + public val type: String get() = json[TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson") + + public companion object { + public const val TYPE_KEY: String = "type" + public const val PROPERTIES_KEY: String = "properties" + } +} + +@JvmInline +public value class GeoJsonFeature(override val json: JsonObject) : GeoJson { + init { + require(type == "Feature") { "Not a GeoJson Feature" } + } + + public val properties: JsonObject? get() = json[PROPERTIES_KEY]?.jsonObject + + public val geometry: GeoJsonGeometry? get() = json[GEOMETRY_KEY]?.jsonObject?.let { GeoJsonGeometry(it) } + + public fun getProperty(propertyName: String): JsonElement? = properties?.get(propertyName) + + public fun getString(propertyName: String): String? = getProperty(propertyName)?.jsonPrimitive?.contentOrNull + + public companion object{ + public const val GEOMETRY_KEY: String = "geometry" + } +} + +@JvmInline +public value class GeoJsonFeatureCollection(override val json: JsonObject) : GeoJson, Iterable { + init { + require(type == "FeatureCollection") { "Not a GeoJson FeatureCollection" } + } + + public val properties: JsonObject? get() = json[PROPERTIES_KEY]?.jsonObject + + public val features: List + get() = json[FEATURES_KEY]?.jsonArray?.map { + GeoJsonFeature(it.jsonObject) + } ?: error("Features not defined in GeoJson features collection") + + override fun iterator(): Iterator = features.iterator() + + public companion object { + + public const val FEATURES_KEY: String = "features" + public fun parse(string: String): GeoJsonFeatureCollection = GeoJsonFeatureCollection( + Json.parseToJsonElement(string).jsonObject + ) + } +} + +/** + * Combine a collection of features to a new [GeoJsonFeatureCollection] + */ +public fun GeoJsonFeatureCollection(features: Collection): GeoJsonFeatureCollection = + GeoJsonFeatureCollection( + buildJsonObject { + put(TYPE_KEY, "FeatureCollection") + put(FEATURES_KEY, JsonArray(features.map { it.json })) + } + ) \ No newline at end of file diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt new file mode 100644 index 0000000..99009c1 --- /dev/null +++ b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt @@ -0,0 +1,113 @@ +package center.sciprog.maps.geojson + +import center.sciprog.maps.coordinates.Gmc +import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY +import kotlinx.serialization.json.* +import kotlin.jvm.JvmInline + +public sealed interface GeoJsonGeometry : GeoJson { + public companion object { + public const val COORDINATES_KEY: String = "coordinates" + } +} + +public fun GeoJsonGeometry(json: JsonObject): GeoJsonGeometry { + return when (val type = json[GeoJson.TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson object")) { + "Point" -> GeoJsonPoint(json) + "MultiPoint" -> GeoJsonMultiPoint(json) + "LineString" -> GeoJsonLineString(json) + "MultiLineString" -> GeoJsonMultiLineString(json) + "Polygon" -> GeoJsonPolygon(json) + "MultiPolygon" -> GeoJsonMultiPolygon(json) + "GeometryCollection" -> GeoJsonGeometryCollection(json) + else -> error("Type '$type' is not recognised as a geometry type") + } +} + +internal fun JsonElement.toGmc() = jsonArray.run { + Gmc.ofDegrees(get(1).jsonPrimitive.double, get(0).jsonPrimitive.double) +} + +@JvmInline +public value class GeoJsonPoint(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "Point") { "Not a GeoJson Point geometry" } + } + + public val coordinates: Gmc + get() = json[COORDINATES_KEY]?.toGmc() + ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "MultiPoint") { "Not a GeoJson MultiPoint geometry" } + } + + public val coordinates: List + get() = json[COORDINATES_KEY]?.jsonArray + ?.map { it.toGmc() } + ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "LineString") { "Not a GeoJson LineString geometry" } + } + + public val coordinates: List + get() = json[COORDINATES_KEY]?.jsonArray + ?.map { it.toGmc() } + ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "MultiLineString") { "Not a GeoJson MultiLineString geometry" } + } + + public val coordinates: List> + get() = json[COORDINATES_KEY]?.jsonArray?.map { lineJson -> + lineJson.jsonArray.map { + it.toGmc() + } + } ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "Polygon") { "Not a GeoJson Polygon geometry" } + } + + public val coordinates: List + get() = json[COORDINATES_KEY]?.jsonArray + ?.map { it.toGmc() } + ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "MultiPolygon") { "Not a GeoJson MultiPolygon geometry" } + } + + public val coordinates: List> + get() = json[COORDINATES_KEY]?.jsonArray?.map { lineJson -> + lineJson.jsonArray.map { + it.toGmc() + } + } ?: error("Coordinates are not provided") +} + +@JvmInline +public value class GeoJsonGeometryCollection(override val json: JsonObject) : GeoJsonGeometry { + init { + require(type == "GeometryCollection") { "Not a GeoJson GeometryCollection geometry" } + } + + public val geometries: List get() = json.jsonArray.map { GeoJsonGeometry(it.jsonObject) } +} \ No newline at end of file diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt new file mode 100644 index 0000000..8557478 --- /dev/null +++ b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt @@ -0,0 +1,84 @@ +package center.sciprog.maps.geojson + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PointMode +import center.sciprog.maps.coordinates.Gmc +import center.sciprog.maps.features.* +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive + + +/** + * Add a single Json geometry to a feature builder + */ +public fun FeatureBuilder.geoJsonGeometry( + geometry: GeoJsonGeometry, + color: Color = defaultColor, + id: String? = null, +): FeatureId<*> = when (geometry) { + is GeoJsonLineString -> points( + geometry.coordinates, + color = color, + pointMode = PointMode.Lines + ) + + is GeoJsonMultiLineString -> group(id = id) { + geometry.coordinates.forEach { + points( + it, + color = color, + pointMode = PointMode.Lines + ) + } + } + + is GeoJsonMultiPoint -> points( + geometry.coordinates, + color = color, + pointMode = PointMode.Points + ) + + is GeoJsonMultiPolygon -> group(id = id) { + geometry.coordinates.forEach { + points( + it, + color = color, + pointMode = PointMode.Polygon + ) + } + } + + is GeoJsonPoint -> circle(geometry.coordinates, color = color, id = id) + is GeoJsonPolygon -> points( + geometry.coordinates, + color = color, + pointMode = PointMode.Polygon + ) + + is GeoJsonGeometryCollection -> group(id = id) { + geometry.geometries.forEach { + geoJsonGeometry(it) + } + } +} + +public fun FeatureBuilder.geoJsonFeature( + geoJson: GeoJsonFeature, + color: Color = defaultColor, + id: String? = null, +) { + val geometry = geoJson.geometry ?: return + val idOverride = geoJson.properties?.get("id")?.jsonPrimitive?.contentOrNull ?: id + val colorOverride = geoJson.properties?.get("color")?.jsonPrimitive?.intOrNull?.let { Color(it) } ?: color + geoJsonGeometry(geometry, colorOverride, idOverride) +} + +public fun FeatureBuilder.geoJson( + geoJson: GeoJsonFeatureCollection, + id: String? = null, +): FeatureId> = group(id = id) { + geoJson.features.forEach { + geoJsonFeature(it) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9af02e2..6892f42 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,6 +46,7 @@ dependencyResolutionManagement { include( ":maps-kt-core", + ":maps-kt-geojson", ":maps-kt-features", ":maps-kt-compose", ":demo:maps",