GeoJson builders

This commit is contained in:
Alexander Nozik 2023-01-12 14:50:10 +03:00
parent 0f00abb1b2
commit b6a3ce0fe7
14 changed files with 201 additions and 51 deletions

View File

@ -8,11 +8,7 @@ plugins {
allprojects {
group = "center.sciprog"
version = "0.2.1-dev-2"
}
apiValidation{
validationDisabled = true
version = "0.2.1-dev-3"
}
ksciencePublish{

View File

@ -23,7 +23,7 @@ internal data class GmcRectangle(
}
public val Rectangle<Gmc>.center: GeodeticMapCoordinates
get() = GeodeticMapCoordinates(
get() = GeodeticMapCoordinates.normalized(
(a.latitude + b.latitude) / 2,
(a.longitude + b.longitude) / 2
)
@ -51,8 +51,8 @@ public val Rectangle<Gmc>.bottom: Angle get() = minOf(a.latitude, b.latitude)
public val Rectangle<Gmc>.longitudeDelta: Angle get() = abs(a.longitude - b.longitude)
public val Rectangle<Gmc>.latitudeDelta: Angle get() = abs(a.latitude - b.latitude)
public val Rectangle<Gmc>.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
public val Rectangle<Gmc>.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right)
public val Rectangle<Gmc>.topLeft: Gmc get() = Gmc.normalized(top, left)
public val Rectangle<Gmc>.bottomRight: Gmc get() = Gmc.normalized(bottom, right)
//public fun GmcRectangle.enlarge(
// top: Distance,

View File

@ -3,7 +3,6 @@ package center.sciprog.maps.compose
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.WebMercatorProjection
import center.sciprog.maps.coordinates.radians
import center.sciprog.maps.features.ViewPoint
/**
@ -16,6 +15,6 @@ internal data class MapViewPoint(
val scaleFactor: Float by lazy { WebMercatorProjection.scaleFactor(zoom) }
public companion object{
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
public val globe: MapViewPoint = MapViewPoint(Gmc.ofRadians(0.0, 0.0), 1f)
}
}

View File

@ -66,7 +66,7 @@ public class MapViewScope internal constructor(
override fun ViewPoint<Gmc>.moveBy(x: Dp, y: Dp): ViewPoint<Gmc> {
val deltaX = x.value / tileScale
val deltaY = y.value / tileScale
val newCoordinates = GeodeticMapCoordinates(
val newCoordinates = Gmc.normalized(
(focus.latitude + (deltaY / scaleFactor).radians).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE

View File

@ -29,7 +29,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
val newCoordinates = GeodeticMapCoordinates(
val newCoordinates = Gmc.normalized(
(focus.latitude + delta.latitude).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE
@ -43,7 +43,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f))
} else {
val difScale = (1 - 2f.pow(-zoomDelta))
val newCenter = GeodeticMapCoordinates(
val newCenter = Gmc.normalized(
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
focus.longitude + (invariant.longitude - focus.longitude) * difScale
)
@ -60,7 +60,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
val maxLat = maxOf { it.top }
val minLong = minOf { it.left }
val maxLong = maxOf { it.right }
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
}
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
@ -70,7 +70,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
val maxLat = maxOf { it.latitude }
val minLong = minOf { it.longitude }
val maxLong = maxOf { it.longitude }
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
}
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
@ -122,11 +122,11 @@ public fun CoordinateSpace<Gmc>.Rectangle(
height: Angle,
width: Angle,
): Rectangle<Gmc> {
val a = GeodeticMapCoordinates(
val a = Gmc.normalized(
center.latitude - (height / 2),
center.longitude - (width / 2)
)
val b = GeodeticMapCoordinates(
val b = Gmc.normalized(
center.latitude + (height / 2),
center.longitude + (width / 2)
)

View File

@ -5,13 +5,12 @@ package center.sciprog.maps.coordinates
*/
public class GeodeticMapCoordinates(
public val latitude: Angle,
longitude: Angle,
public val elevation: Distance = 0.kilometers
public val longitude: Angle,
public val elevation: Distance = 0.kilometers,
) {
public val longitude: Angle = longitude.normalized(Angle.zero)
init {
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" }
require(longitude in (-Angle.pi..Angle.pi)) { "Longitude $longitude is not in (-PI..PI) range" }
}
override fun equals(other: Any?): Boolean {
@ -38,14 +37,29 @@ public class GeodeticMapCoordinates(
public companion object {
public fun ofRadians(latitude: Double, longitude: Double): GeodeticMapCoordinates =
GeodeticMapCoordinates(latitude.radians, longitude.radians)
public fun normalized(
latitude: Angle,
longitude: Angle,
elevation: Distance = 0.kilometers,
): GeodeticMapCoordinates = GeodeticMapCoordinates(
latitude, longitude.normalized(Angle.zero), elevation
)
public fun ofDegrees(latitude: Double, longitude: Double): GeodeticMapCoordinates =
GeodeticMapCoordinates(latitude.degrees.radians, longitude.degrees.radians)
public fun ofRadians(
latitude: Double,
longitude: Double,
elevation: Distance = 0.kilometers,
): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation)
public fun ofDegrees(
latitude: Double,
longitude: Double,
elevation: Distance = 0.kilometers,
): GeodeticMapCoordinates = normalized(latitude.degrees.radians, longitude.degrees.radians, elevation)
}
}
/**
* Short name for GeodeticMapCoordinates
*/

View File

@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
}
return GmcCurve(
forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi),
backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero),
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) zero else pi),
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) pi else zero),
distance = s
)
}
@ -77,8 +77,8 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
val right = toLongitude > fromLongitude
return GmcCurve(
forward = GmcPose(Gmc(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
)
}
@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
val L = lambda - (1 - C) * f * sinAlpha *
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
val endPoint = Gmc(phi2, start.longitude + L.radians)
val endPoint = Gmc.normalized(phi2, start.longitude + L.radians)
// eq. 12

View File

@ -13,4 +13,10 @@ kotlin {
}
}
}
}
kscience{
useSerialization {
json()
}
}

View File

@ -3,12 +3,14 @@ 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.Serializable
import kotlinx.serialization.json.*
import kotlin.jvm.JvmInline
/**
* A utility class to work with GeoJson (https://geojson.org/)
*/
@Serializable(GeoJsonSerializer::class)
public sealed interface GeoJson {
public val json: JsonObject
public val type: String get() = json[TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson")
@ -34,6 +36,23 @@ public value class GeoJsonFeature(override val json: JsonObject) : GeoJson {
}
}
/**
* A builder function for [GeoJsonFeature]
*/
public fun GeoJsonFeature(
geometry: GeoJsonGeometry?,
properties: JsonObject? = null,
builder: JsonObjectBuilder.() -> Unit = {},
): GeoJsonFeature = GeoJsonFeature(
buildJsonObject {
put(TYPE_KEY, "Feature")
geometry?.json?.let { put(GeoJsonFeature.GEOMETRY_KEY, it) }
properties?.let { put(PROPERTIES_KEY, it) }
builder()
}
)
public fun GeoJsonFeature.getProperty(key: String): JsonElement? = json[key] ?: properties?.get(key)
@JvmInline
@ -60,20 +79,33 @@ public value class GeoJsonFeatureCollection(override val json: JsonObject) : Geo
}
}
/**
* A builder for [GeoJsonFeatureCollection]
*/
public fun GeoJsonFeatureCollection(
features: List<GeoJsonFeature>,
properties: JsonObject? = null,
builder: JsonObjectBuilder.() -> Unit = {},
): GeoJsonFeatureCollection = GeoJsonFeatureCollection(
buildJsonObject {
put(TYPE_KEY, "FeatureCollection")
putJsonArray(FEATURES_KEY) {
features.forEach {
add(it.json)
}
}
properties?.let { put(PROPERTIES_KEY, it) }
builder()
}
)
/**
* Generic Json to GeoJson converter
*/
public fun GeoJson(json: JsonObject): GeoJson =
when (json[TYPE_KEY]?.jsonPrimitive?.contentOrNull ?: error("Not a GeoJson")) {
"Feature" -> GeoJsonFeature(json)
"FeatureCollection" -> GeoJsonFeatureCollection(json)
else -> GeoJsonGeometry(json)
}
/**
* 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

@ -1,6 +1,8 @@
package center.sciprog.maps.geojson
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.kilometers
import center.sciprog.maps.coordinates.meters
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
import kotlinx.serialization.json.*
import kotlin.jvm.JvmInline
@ -25,7 +27,31 @@ public fun GeoJsonGeometry(json: JsonObject): GeoJsonGeometry {
}
internal fun JsonElement.toGmc() = jsonArray.run {
Gmc.ofDegrees(get(1).jsonPrimitive.double, get(0).jsonPrimitive.double)
Gmc.ofDegrees(
get(1).jsonPrimitive.double,
get(0).jsonPrimitive.double,
get(2).jsonPrimitive.doubleOrNull?.meters ?: 0.kilometers
)
}
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
add(longitude.degrees.value)
add(latitude.degrees.value)
if (elevation.kilometers != 0.0) {
add(elevation.meters)
}
}
private fun List<Gmc>.listToJsonArray(): JsonArray = buildJsonArray {
forEach {
add(it.toJsonArray())
}
}
private fun List<List<Gmc>>.listOfListsToJsonArray(): JsonArray = buildJsonArray {
forEach {
add(it.listToJsonArray())
}
}
@JvmInline
@ -39,6 +65,13 @@ public value class GeoJsonPoint(override val json: JsonObject) : GeoJsonGeometry
?: error("Coordinates are not provided")
}
public fun GeoJsonPoint(coordinates: Gmc): GeoJsonPoint = GeoJsonPoint(
buildJsonObject {
put(GeoJson.TYPE_KEY, "Point")
put(COORDINATES_KEY, coordinates.toJsonArray())
}
)
@JvmInline
public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeometry {
init {
@ -51,6 +84,13 @@ public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeo
?: error("Coordinates are not provided")
}
public fun GeoJsonMultiPoint(coordinates: List<Gmc>): GeoJsonMultiPoint = GeoJsonMultiPoint(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiPoint")
put(COORDINATES_KEY, coordinates.listToJsonArray())
}
)
@JvmInline
public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeometry {
init {
@ -63,6 +103,13 @@ public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeo
?: error("Coordinates are not provided")
}
public fun GeoJsonLineString(coordinates: List<Gmc>): GeoJsonLineString = GeoJsonLineString(
buildJsonObject {
put(GeoJson.TYPE_KEY, "LineString")
put(COORDINATES_KEY, coordinates.listToJsonArray())
}
)
@JvmInline
public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJsonGeometry {
init {
@ -77,6 +124,13 @@ public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJs
} ?: error("Coordinates are not provided")
}
public fun GeoJsonMultiLineString(coordinates: List<List<Gmc>>): GeoJsonMultiLineString = GeoJsonMultiLineString(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiLineString")
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
}
)
@JvmInline
public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeometry {
init {
@ -91,6 +145,13 @@ public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeomet
} ?: error("Coordinates are not provided")
}
public fun GeoJsonPolygon(coordinates: List<List<Gmc>>): GeoJsonPolygon = GeoJsonPolygon(
buildJsonObject {
put(GeoJson.TYPE_KEY, "Polygon")
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
}
)
@JvmInline
public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonGeometry {
init {
@ -107,11 +168,31 @@ public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonG
} ?: error("Coordinates are not provided")
}
public fun GeoJsonMultiPolygon(coordinates: List<List<List<Gmc>>>): GeoJsonMultiPolygon = GeoJsonMultiPolygon(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiPolygon")
put(COORDINATES_KEY, buildJsonArray { coordinates.forEach { add(it.listOfListsToJsonArray()) } })
}
)
@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) }
}
public val geometries: List<GeoJsonGeometry>
get() = json["geometries"]?.jsonArray?.map { GeoJsonGeometry(it.jsonObject) } ?: emptyList()
}
public fun GeoJsonGeometryCollection(geometries: List<GeoJsonGeometry>): GeoJsonGeometryCollection =
GeoJsonGeometryCollection(
buildJsonObject {
put(GeoJson.TYPE_KEY, "GeometryCollection")
put("geometries", buildJsonArray {
geometries.forEach {
add(it.json)
}
})
}
)

View File

@ -1,6 +0,0 @@
package center.sciprog.maps.geojson
import center.sciprog.attributes.Attribute
import kotlinx.serialization.json.JsonObject
public object GeoJsonPropertiesAttribute : Attribute<JsonObject>

View File

@ -0,0 +1,7 @@
package center.sciprog.maps.geojson
import center.sciprog.attributes.SerializableAttribute
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.serializer
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())

View File

@ -0,0 +1,21 @@
package center.sciprog.maps.geojson
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject
public object GeoJsonSerializer : KSerializer<GeoJson> {
private val serializer = JsonObject.serializer()
override val descriptor: SerialDescriptor
get() = serializer.descriptor
override fun deserialize(decoder: Decoder): GeoJson = GeoJson(serializer.deserialize(decoder))
override fun serialize(encoder: Encoder, value: GeoJson) {
serializer.serialize(encoder, value.json)
}
}