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 8, 2024
1 parent 812654b commit 92e53b4
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 53 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)
public class Feature(
public val geometry: Geometry?,
public class Feature<out T : Geometry>(
public val geometry: T?,
properties: Map<String, JsonElement> = emptyMap(),
public val id: String? = null,
override val bbox: BoundingBox? = null
Expand Down Expand Up @@ -80,7 +80,7 @@ public 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 @@ public class Feature(
)
}}"""

public fun copy(
geometry: Geometry? = this.geometry,
public 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)

public companion object {
@JvmStatic
public fun fromJson(json: String): Feature = fromJson(Json.decodeFromString(JsonObject.serializer(), json))
public 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 @@ public 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)
public class FeatureCollection(
public val features: List<Feature> = emptyList(),
public val features: List<Feature<Geometry>> = emptyList(),
override val bbox: BoundingBox? = null
) : Collection<Feature> by features, GeoJson {
) : Collection<Feature<Geometry>> by features, GeoJson {

public constructor(vararg features: Feature, bbox: BoundingBox? = null) : this(features.toMutableList(), bbox)
public 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 @@ public class FeatureCollection(
override fun json(): String =
"""{"type":"FeatureCollection",${bbox.jsonProp()}"features":${features.jsonJoin { it.json() }}}"""

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

public companion object {
Expand All @@ -74,7 +77,7 @@ public 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
public class FeatureCollectionDsl(
private val features: MutableList<Feature> = mutableListOf(),
private val features: MutableList<Feature<Geometry>> = mutableListOf(),
public var bbox: BoundingBox? = null
) {
public operator fun Feature.unaryPlus() {
public operator fun Feature<Geometry>.unaryPlus() {
features.add(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public class PropertiesBuilder {
}

@GeoJsonDsl
public inline fun feature(
geometry: Geometry? = null,
public inline fun <reified T : Geometry> feature(
geometry: T? = null,
id: String? = null,
bbox: BoundingBox? = null,
properties: PropertiesBuilder.() -> Unit = {}
): Feature = Feature(geometry, PropertiesBuilder().apply(properties).build(), id, bbox)
): Feature<T> = Feature(geometry, PropertiesBuilder().apply(properties).build(), id, bbox)
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 @@ public 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 @@ public 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

public 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
public fun booleanPointInPolygon(point: Point, polygon: Polygon, ignoreBoundary: Boolean = false): Boolean {
val bbox = bbox(polygon)
Expand All @@ -38,6 +39,7 @@ public fun booleanPointInPolygon(point: Point, polygon: Polygon, ignoreBoundary:
* 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
public 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 @@ public 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 @@ -43,9 +44,9 @@ public 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public fun along(line: LineString, distance: Double, units: Units = Units.Kilome
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 @@ public fun along(line: LineString, distance: Double, units: Units = Units.Kilome
)
}
}

else -> travelled += distance(
coordinate,
line.coordinates[i + 1],
Expand All @@ -85,6 +87,7 @@ public 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 @@ public fun bbox(geometry: MultiPolygon): BoundingBox = computeBbox(geometry.coor
* @return A [BoundingBox] that covers the geometry.
*/
@ExperimentalTurfApi
public fun bbox(feature: Feature): BoundingBox = computeBbox(feature.coordAll() ?: emptyList())
public 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 @@ public fun midpoint(point1: Position, point2: Position): Position {
* @return A [Point] holding the center coordinates
*/
@ExperimentalTurfApi
public fun center(feature: Feature): Point {
public 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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public fun GeometryCollection.coordAll(): List<Position> =
geometries.fold(emptyList<Position>()) { acc, geometry -> acc + geometry.coordAll() }

@ExperimentalTurfApi
public fun Feature.coordAll(): List<Position>? = geometry?.coordAll()
public fun Feature<Geometry>.coordAll(): List<Position>? = geometry?.coordAll()

@ExperimentalTurfApi
public fun FeatureCollection.coordAll(): List<Position> =
Expand Down
Loading

0 comments on commit 92e53b4

Please sign in to comment.