Add GeoJson bindings

This commit is contained in:
Alexander Nozik 2022-12-26 23:22:21 +03:00
parent 7e7cb0a260
commit 5da23b83c1
8 changed files with 304 additions and 13 deletions

View File

@ -68,9 +68,9 @@ fun App() {
val marker2 = rectangle(55.8 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta) 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) val marker3 = rectangle(56.0 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
draggableLine(marker1, marker2) draggableLine(marker1, marker2, color = Color.Blue)
draggableLine(marker2, marker3) draggableLine(marker2, marker3, color = Color.Blue)
draggableLine(marker3, marker1) draggableLine(marker3, marker1, color = Color.Blue)
points( points(
points = listOf( points = listOf(

View File

@ -111,14 +111,14 @@ public actual fun MapView(
clipRect { clipRect {
val tileSize = IntSize( val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(), ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt() ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
) )
mapTiles.forEach { (id, image) -> mapTiles.forEach { (id, image) ->
//converting back from tile index to screen offset //converting back from tile index to screen offset
val offset = IntOffset( val offset = IntOffset(
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.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.toFloat()).roundToPx() (canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
) )
drawImage( drawImage(
image = image.toComposeImageBitmap(), image = image.toComposeImageBitmap(),

View File

@ -27,6 +27,8 @@ public interface FeatureBuilder<T : Any> {
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F>
public fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?) public fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?)
public val defaultColor: Color get() = Color.Red
} }
public fun <T : Any, F : Feature<T>> FeatureBuilder<T>.feature(id: FeatureId<F>, feature: F): FeatureId<F> = public fun <T : Any, F : Feature<T>> FeatureBuilder<T>.feature(id: FeatureId<F>, feature: F): FeatureId<F> =
@ -167,7 +169,7 @@ public fun <T : Any> FeatureBuilder<T>.circle(
center: T, center: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
color: Color = Color.Red, color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<T>> = feature( ): FeatureId<CircleFeature<T>> = feature(
id, CircleFeature(coordinateSpace, center, zoomRange, size, color) id, CircleFeature(coordinateSpace, center, zoomRange, size, color)
@ -177,7 +179,7 @@ public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T, centerCoordinates: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red, color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<RectangleFeature<T>> = feature( ): FeatureId<RectangleFeature<T>> = feature(
id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color) id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color)
@ -197,7 +199,7 @@ public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T, aCoordinates: T,
bCoordinates: T, bCoordinates: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
color: Color = Color.Red, color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<T>> = feature( ): FeatureId<LineFeature<T>> = feature(
id, id,
@ -209,7 +211,7 @@ public fun <T : Any> FeatureBuilder<T>.arc(
startAngle: Float, startAngle: Float,
arcLength: Float, arcLength: Float,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
color: Color = Color.Red, color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<ArcFeature<T>> = feature( ): FeatureId<ArcFeature<T>> = feature(
id, id,
@ -220,7 +222,7 @@ public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>, points: List<T>,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f, stroke: Float = 2f,
color: Color = Color.Red, color: Color = defaultColor,
pointMode: PointMode = PointMode.Points, pointMode: PointMode = PointMode.Points,
id: String? = null, id: String? = null,
): FeatureId<PointsFeature<T>> = ): FeatureId<PointsFeature<T>> =
@ -268,7 +270,7 @@ public fun <T : Any> FeatureBuilder<T>.text(
position: T, position: T,
text: String, text: String,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
color: Color = Color.Red, color: Color = defaultColor,
font: FeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature<T>> = feature( ): FeatureId<TextFeature<T>> = feature(

View File

@ -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)
}
}
}
}

View File

@ -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<GeoJsonFeature> {
init {
require(type == "FeatureCollection") { "Not a GeoJson FeatureCollection" }
}
public val properties: JsonObject? get() = json[PROPERTIES_KEY]?.jsonObject
public val features: List<GeoJsonFeature>
get() = json[FEATURES_KEY]?.jsonArray?.map {
GeoJsonFeature(it.jsonObject)
} ?: error("Features not defined in GeoJson features collection")
override fun iterator(): Iterator<GeoJsonFeature> = 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<GeoJsonFeature>): GeoJsonFeatureCollection =
GeoJsonFeatureCollection(
buildJsonObject {
put(TYPE_KEY, "FeatureCollection")
put(FEATURES_KEY, JsonArray(features.map { it.json }))
}
)

View File

@ -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<Gmc>
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<Gmc>
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<List<Gmc>>
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<Gmc>
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<List<Gmc>>
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<GeoJsonGeometry> get() = json.jsonArray.map { GeoJsonGeometry(it.jsonObject) }
}

View File

@ -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<Gmc>.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<Gmc>.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<Gmc>.geoJson(
geoJson: GeoJsonFeatureCollection,
id: String? = null,
): FeatureId<FeatureGroup<Gmc>> = group(id = id) {
geoJson.features.forEach {
geoJsonFeature(it)
}
}

View File

@ -46,6 +46,7 @@ dependencyResolutionManagement {
include( include(
":maps-kt-core", ":maps-kt-core",
":maps-kt-geojson",
":maps-kt-features", ":maps-kt-features",
":maps-kt-compose", ":maps-kt-compose",
":demo:maps", ":demo:maps",