Add GeoJson bindings
This commit is contained in:
parent
7e7cb0a260
commit
5da23b83c1
@ -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(
|
||||||
|
@ -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(),
|
||||||
|
@ -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(
|
||||||
|
17
maps-kt-geojson/build.gradle.kts
Normal file
17
maps-kt-geojson/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }))
|
||||||
|
}
|
||||||
|
)
|
@ -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) }
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user