Skip to content

Commit

Permalink
Add generic type parameter to Feature as Geometry
Browse files Browse the repository at this point in the history
  • Loading branch information
elcolto committed May 6, 2024
1 parent b54f691 commit 2d0e351
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import kotlin.jvm.JvmStatic
*/
@Suppress("TooManyFunctions")
@Serializable(with = FeatureSerializer::class)
class Feature(
val geometry: Geometry?,
class Feature<out T : Geometry>(
val geometry: T?,
properties: Map<String, JsonElement> = emptyMap(),
val id: String? = null,
override val bbox: BoundingBox? = null
Expand Down Expand Up @@ -80,7 +80,7 @@ class Feature(
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Feature
other as Feature<*>

if (geometry != other.geometry) return false
if (id != other.id) return false
Expand Down Expand Up @@ -115,26 +115,27 @@ class Feature(
)
}}"""

fun copy(
geometry: Geometry? = this.geometry,
fun <T : Geometry> copy(
geometry: T? = this.geometry as T,
properties: Map<String, JsonElement> = this.properties,
id: String? = this.id,
bbox: BoundingBox? = this.bbox
): Feature = Feature(geometry, properties, id, bbox)
): Feature<T> = Feature(geometry, properties, id, bbox)

companion object {
@JvmStatic
fun fromJson(json: String): Feature = fromJson(Json.decodeFromString(JsonObject.serializer(), json))
fun <T : Geometry> fromJson(json: String): Feature<T> =
fromJson(Json.decodeFromString(JsonObject.serializer(), json))

@JvmStatic
public fun fromJsonOrNull(json: String): Feature? = try {
public fun <T : Geometry> fromJsonOrNull(json: String): Feature<T>? = try {
fromJson(json)
} catch (_: Exception) {
null
}

@JvmStatic
public fun fromJson(json: JsonObject): Feature {
public fun <T : Geometry> fromJson(json: JsonObject): Feature<T> {
require(json.getValue("type").jsonPrimitive.content == "Feature") {
"Object \"type\" is not \"Feature\"."
}
Expand All @@ -143,7 +144,7 @@ class Feature(
val id = json["id"]?.jsonPrimitive?.content

val geom = json["geometry"]?.jsonObject
val geometry: Geometry? = if (geom != null) Geometry.fromJson(geom) else null
val geometry: T? = if (geom != null) Geometry.fromJson(geom) as? T else null

return Feature(geometry, json["properties"]?.jsonObject ?: emptyMap(), id, bbox)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ import kotlin.jvm.JvmStatic
*/
@Serializable(with = FeatureCollectionSerializer::class)
class FeatureCollection(
val features: List<Feature> = emptyList(),
val features: List<Feature<Geometry>> = emptyList(),
override val bbox: BoundingBox? = null
) : Collection<Feature> by features, GeoJson {
) : Collection<Feature<Geometry>> by features, GeoJson {

constructor(vararg features: Feature, bbox: BoundingBox? = null) : this(features.toMutableList(), bbox)
constructor(vararg features: Feature<Geometry>, bbox: BoundingBox? = null) : this(
features.toMutableList(),
bbox
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand All @@ -52,7 +55,7 @@ class FeatureCollection(
override fun json(): String =
"""{"type":"FeatureCollection",${bbox.jsonProp()}"features":${features.jsonJoin { it.json() }}}"""

operator fun component1(): List<Feature> = features
operator fun component1(): List<Feature<*>> = features
operator fun component2(): BoundingBox? = bbox

companion object {
Expand All @@ -74,7 +77,7 @@ class FeatureCollection(
}

val bbox = json["bbox"]?.jsonArray?.toBbox()
val features = json.getValue("features").jsonArray.map { Feature.fromJson(it.jsonObject) }
val features = json.getValue("features").jsonArray.map { Feature.fromJson<Geometry>(it.jsonObject) }

return FeatureCollection(features, bbox)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import kotlin.jvm.JvmName

@GeoJsonDsl
class FeatureCollectionDsl(
private val features: MutableList<Feature> = mutableListOf(),
private val features: MutableList<Feature<Geometry>> = mutableListOf(),
var bbox: BoundingBox? = null
) {
operator fun Feature.unaryPlus() {
operator fun Feature<Geometry>.unaryPlus() {
features.add(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package io.github.dellisd.spatialk.geojson.serialization

import io.github.dellisd.spatialk.geojson.FeatureCollection
import io.github.dellisd.spatialk.geojson.serialization.BoundingBoxSerializer.toJsonArray
import io.github.dellisd.spatialk.geojson.serialization.FeatureSerializer.toJsonObject
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.json.JsonDecoder
Expand All @@ -16,7 +15,7 @@ object FeatureCollectionSerializer : JsonSerializer<FeatureCollection> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FeatureCollection")

override fun deserialize(input: JsonDecoder): FeatureCollection {
return FeatureCollection.Companion.fromJson(input.decodeJsonElement().jsonObject)
return FeatureCollection.fromJson(input.decodeJsonElement().jsonObject)
}

override fun serialize(output: JsonEncoder, value: FeatureCollection) {
Expand All @@ -26,7 +25,7 @@ object FeatureCollectionSerializer : JsonSerializer<FeatureCollection> {
put(
"features",
buildJsonArray {
value.features.forEach { add(it.toJsonObject()) }
value.features.forEach { feature -> add(feature.toJsonObject()) }
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.dellisd.spatialk.geojson.serialization

import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Geometry
import io.github.dellisd.spatialk.geojson.serialization.BoundingBoxSerializer.toJsonArray
import io.github.dellisd.spatialk.geojson.serialization.GeometrySerializer.toJsonObject
import kotlinx.serialization.descriptors.SerialDescriptor
Expand All @@ -12,21 +13,22 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put

object FeatureSerializer : JsonSerializer<Feature> {
internal class FeatureSerializer<T : Geometry> : JsonSerializer<Feature<T>> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Feature")

override fun deserialize(input: JsonDecoder): Feature = Feature.fromJson(input.decodeJsonElement().jsonObject)
override fun deserialize(input: JsonDecoder): Feature<T> = Feature.fromJson(input.decodeJsonElement().jsonObject)

override fun serialize(output: JsonEncoder, value: Feature) {
override fun serialize(output: JsonEncoder, value: Feature<T>) {
output.encodeJsonElement(value.toJsonObject())
}

internal fun Feature.toJsonObject() = buildJsonObject {
put("type", "Feature")
bbox?.let { put("bbox", it.toJsonArray()) }
geometry?.let { put("geometry", it.toJsonObject()) }
id?.let { put("id", it) }
}

put("properties", JsonObject(properties))
}
internal fun <T : Geometry> Feature<T>.toJsonObject() = buildJsonObject {
put("type", "Feature")
bbox?.let { put("bbox", it.toJsonArray()) }
geometry?.let { put("geometry", it.toJsonObject()) }
id?.let { put("id", it) }

put("properties", JsonObject(properties))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.double
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive

internal fun DoubleArray.jsonJoin(transform: ((Double) -> CharSequence)? = null) =
Expand All @@ -17,7 +15,7 @@ internal fun <T> Iterable<T>.jsonJoin(transform: ((T) -> CharSequence)? = null)

internal fun BoundingBox?.jsonProp(): String = if (this == null) "" else """"bbox":${this.json()},"""

internal fun Feature.idProp(): String = if (this.id == null) "" else """"id":"${this.id}","""
internal fun Feature<*>.idProp(): String = if (this.id == null) "" else """"id":"${this.id}","""

internal fun JsonArray.toPosition(): Position =
Position(this[0].jsonPrimitive.double, this[1].jsonPrimitive.double, this.getOrNull(2)?.jsonPrimitive?.double)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.dellisd.spatialk.geojson.serialization

import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.FeatureCollection
import io.github.dellisd.spatialk.geojson.LineString
import io.github.dellisd.spatialk.geojson.Position
import io.github.dellisd.spatialk.geojson.Point
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -35,6 +36,30 @@ class FeatureCollectionSerializationTests {
assertEquals(json, Json.encodeToString(collection), "FeatureCollection (kotlinx)")
}

@Test
fun testSerializePolymorphicFeatureCollection() {
val position = Position(12.3, 45.6)
val point = Point(position)
val featurePoint = Feature(
point, mapOf(
"size" to JsonPrimitive(45.1),
"name" to JsonPrimitive("Nowhere")
)
)
val line = LineString(coordinates = listOf(position, Position(position.component2(), position.component1())))
val featureLine = Feature(line)
val collection = FeatureCollection(featurePoint, featureLine)
val json =
"""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":
|[12.3,45.6]},"properties":{"size":45.1,"name":"Nowhere"}},{"type":"Feature","geometry":{"type":
|"LineString","coordinates":[[12.3,45.6],[45.6,12.3]]},"properties":{}}]}"""
.trimMargin()
.replace("\n", "")
assertEquals(json, collection.json(), "FeatureCollection (fast)")
assertEquals(json, Json.encodeToString(collection), "FeatureCollection (kotlinx)")
}


@Test
fun testDeserializeFeatureCollection() {
val geometry = Point(Position(12.3, 45.6))
Expand All @@ -57,4 +82,29 @@ class FeatureCollectionSerializationTests {
)
)
}

@Test
fun testDeerializePolymorphicFeatureCollection() {
val position = Position(12.3, 45.6)
val point = Point(position)
val featurePoint = Feature(
point, mapOf(
"size" to JsonPrimitive(45.1),
"name" to JsonPrimitive("Nowhere")
)
)
val line = LineString(coordinates = listOf(position, Position(position.component2(), position.component1())))
val featureLine = Feature(line)
val collection = FeatureCollection(featurePoint, featureLine)
assertEquals(
collection,
FeatureCollection.fromJson(
"""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":
|[12.3,45.6]},"properties":{"size":45.1,"name":"Nowhere"}},{"type":"Feature","geometry":{"type":
|"LineString","coordinates":[[12.3,45.6],[45.6,12.3]]},"properties":{}}]}"""
.trimMargin()
.replace("\n", "")
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlin.jvm.JvmOverloads
* the point is inside the polygon otherwise false.
* @return `true` if the Position is inside the Polygon; `false` if the Position is not inside the Polygon
*/
@ExperimentalTurfApi
@JvmOverloads
fun booleanPointInPolygon(point: Point, polygon: Polygon, ignoreBoundary: Boolean = false): Boolean {
val bbox = bbox(polygon)
Expand All @@ -38,6 +39,7 @@ fun booleanPointInPolygon(point: Point, polygon: Polygon, ignoreBoundary: Boolea
* the point is inside the polygon otherwise false.
* @return `true` if the Position is inside the Polygon; `false` if the Position is not inside the Polygon
*/
@OptIn(ExperimentalTurfApi::class)
@JvmOverloads
fun booleanPointInPolygon(point: Point, polygon: MultiPolygon, ignoreBoundary: Boolean = false): Boolean {
val bbox = bbox(polygon)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.dellisd.spatialk.turf
import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.FeatureCollection
import io.github.dellisd.spatialk.geojson.Geometry
import io.github.dellisd.spatialk.geojson.Polygon
import io.github.dellisd.spatialk.geojson.Position
import kotlin.math.abs
Expand All @@ -24,7 +25,7 @@ fun squareGrid(
cellHeight: Double,
units: Units = Units.Kilometers
): FeatureCollection {
val featureList = mutableListOf<Feature>()
val featureList = mutableListOf<Feature<Geometry>>()
val west = bbox.southwest.longitude
val south = bbox.southwest.latitude
val east = bbox.northeast.longitude
Expand All @@ -33,8 +34,8 @@ fun squareGrid(
val bboxWidth = east - west
val cellWidthDeg = convertLength(cellWidth, units, Units.Degrees)

val bboxHeight = north - south;
val cellHeightDeg = convertLength(cellHeight, units, Units.Degrees);
val bboxHeight = north - south
val cellHeightDeg = convertLength(cellHeight, units, Units.Degrees)

val columns = floor(abs(bboxWidth) / cellWidthDeg)
val rows = floor(abs(bboxHeight) / cellHeightDeg)
Expand All @@ -43,9 +44,9 @@ fun squareGrid(
val deltaY = (bboxHeight - rows * cellHeightDeg) / 2

var currentX = west + deltaX
repeat (columns.toInt()) {
repeat(columns.toInt()) {
var currentY = south + deltaY
repeat (rows.toInt()) {
repeat(rows.toInt()) {
val positions = mutableListOf<Position>().apply {
add(Position(currentX, currentY))
add(Position(currentX, currentY + cellHeightDeg))
Expand All @@ -58,9 +59,9 @@ fun squareGrid(
}.also {
featureList.add(Feature(Polygon(it)))
}
currentY += cellHeightDeg;
currentY += cellHeightDeg
}
currentX += cellWidthDeg;
currentX += cellWidthDeg
}
return FeatureCollection(featureList)
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ fun along(line: LineString, distance: Double, units: Units = Units.Kilometers):
when {
distance >= travelled && i == line.coordinates.size - 1 -> {
}

travelled >= distance -> {
val overshot = distance - travelled
return if (overshot == 0.0) coordinate
Expand All @@ -60,6 +61,7 @@ fun along(line: LineString, distance: Double, units: Units = Units.Kilometers):
)
}
}

else -> travelled += distance(
coordinate,
line.coordinates[i + 1],
Expand All @@ -85,6 +87,7 @@ fun area(geometry: Geometry): Double {
geom
)
}

else -> calculateArea(geometry)
}
}
Expand All @@ -97,6 +100,7 @@ private fun calculateArea(geometry: Geometry): Double {
coords
)
}

else -> 0.0
}
}
Expand Down Expand Up @@ -140,11 +144,13 @@ private fun ringArea(coordinates: List<Position>): Double {
middleIndex = coordinates.size - 1
upperIndex = 0
}

coordinates.size - 1 -> {
lowerIndex = coordinates.size - 1
middleIndex = 0
upperIndex = 1
}

else -> {
lowerIndex = i
middleIndex = i + 1
Expand Down Expand Up @@ -231,7 +237,7 @@ fun bbox(geometry: MultiPolygon) = computeBbox(geometry.coordAll())
* @return A [BoundingBox] that covers the geometry.
*/
@ExperimentalTurfApi
fun bbox(feature: Feature): BoundingBox = computeBbox(feature.coordAll() ?: emptyList())
fun bbox(feature: Feature<Geometry>): BoundingBox = computeBbox(feature.coordAll() ?: emptyList())

/**
* Takes a feature collection and calculates a bbox that covers all features in the collection.
Expand Down Expand Up @@ -472,7 +478,7 @@ fun midpoint(point1: Position, point2: Position): Position {
* @return A [Point] holding the center coordinates
*/
@ExperimentalTurfApi
fun center(feature: Feature): Point {
fun center(feature: Feature<Geometry>): Point {
val ext = bbox(feature)
val x = (ext.southwest.longitude + ext.northeast.longitude) / 2
val y = (ext.southwest.latitude + ext.northeast.latitude) / 2
Expand Down
Loading

0 comments on commit 2d0e351

Please sign in to comment.