From 6960d8d1d5e7ee87fc1ef61600c969f8d9101385 Mon Sep 17 00:00:00 2001 From: Jakob Gillich Date: Sat, 30 Aug 2025 06:11:25 +0200 Subject: [PATCH 1/4] Add simplify --- .../dellisd/spatialk/turf/Transformation.kt | 121 +++++++ .../spatialk/turf/TransformationTest.kt | 16 + .../simplify/in/linestring.json | 329 ++++++++++++++++++ .../simplify/out/linestring.json | 105 ++++++ 4 files changed, 571 insertions(+) create mode 100644 turf/src/commonTest/resources/transformation/simplify/in/linestring.json create mode 100644 turf/src/commonTest/resources/transformation/simplify/out/linestring.json diff --git a/turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt b/turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt index 7d394388..dc0a57d9 100644 --- a/turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt +++ b/turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt @@ -2,6 +2,7 @@ package io.github.dellisd.spatialk.turf import io.github.dellisd.spatialk.geojson.LineString import io.github.dellisd.spatialk.geojson.Position +import kotlin.math.pow /** * Takes a [LineString] and returns a curved version by applying a Bezier spline algorithm. @@ -134,3 +135,123 @@ public fun bezierSpline(coords: List, duration: Int = 10_000, sharpnes return positions } + + +private fun getSqDist(p1: Position, p2: Position): Double { + val dx = p1.longitude - p2.longitude + val dy = p1.latitude - p2.latitude + return dx * dx + dy * dy +} + +private fun getSqSegDist(p: Position, p1: Position, p2: Position): Double { + var x = p1.longitude + var y = p1.latitude + var dx = p2.longitude - x + var dy = p2.latitude - y + + if (dx != 0.0 || dy != 0.0) { + val t = ((p.longitude - x) * dx + (p.latitude - y) * dy) / (dx * dx + dy * dy) + + if (t > 1) { + x = p2.longitude + y = p2.latitude + } else if (t > 0) { + x += dx * t + y += dy * t + } + } + + dx = p.longitude - x + dy = p.latitude - y + + return dx * dx + dy * dy +} + +private fun simplifyRadialDist(points: List, sqTolerance: Double): List { + if (points.isEmpty()) return emptyList() + + var prevPoint = points[0] + val newPoints = mutableListOf(prevPoint) + var point: Position? = null + + for (i in 1 until points.size) { + point = points[i] + + if (getSqDist(point, prevPoint) > sqTolerance) { + newPoints.add(point) + prevPoint = point + } + } + point?.let { + if (prevPoint != it) newPoints.add(it) + } + + return newPoints +} + +private fun simplifyDPStep( + points: List, + first: Int, + last: Int, + sqTolerance: Double, + simplified: MutableList +) { + var maxSqDist = sqTolerance + var index: Int? = null + + for (i in first + 1 until last) { + val sqDist = getSqSegDist(points[i], points[first], points[last]) + + if (sqDist > maxSqDist) { + index = i + maxSqDist = sqDist + } + } + + index?.let { + if (maxSqDist > sqTolerance) { + if (it - first > 1) simplifyDPStep(points, first, it, sqTolerance, simplified) + simplified.add(points[it]) + if (last - it > 1) simplifyDPStep(points, it, last, sqTolerance, simplified) + } + } +} + +private fun simplifyDouglasPeucker(points: List, sqTolerance: Double): List { + if (points.isEmpty()) return emptyList() + val last = points.size - 1 + + val simplified = mutableListOf(points[0]) + simplifyDPStep(points, 0, last, sqTolerance, simplified) + simplified.add(points[last]) + + return simplified +} + +/** + * Reduces the number of points in a LineString while preserving its general shape. + * + * @param lineString The LineString to simplify. + * @param tolerance The tolerance for simplification (in the units of the coordinates). + * A higher tolerance results in more simplification (fewer points). + * If `null`, a default tolerance of `1.0` is used. + * @param highestQuality If `true`, the radial distance simplification step is skipped, + * potentially resulting in a higher quality simplification at the cost of performance. + * @return A new, simplified LineString. + */ +public fun simplify( + lineString: LineString, + tolerance: Double? = null, + highestQuality: Boolean = false +): LineString { + if (lineString.coordinates.size <= 2) return lineString + + val sqTolerance = tolerance?.pow(2) ?: 1.0 + + val simplifiedPoints = if (highestQuality) lineString.coordinates else simplifyRadialDist( + lineString.coordinates, + sqTolerance + ) + + return LineString(simplifyDouglasPeucker(simplifiedPoints, sqTolerance)) +} \ No newline at end of file diff --git a/turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt b/turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt index 2843f1e1..5e045a9b 100644 --- a/turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt +++ b/turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt @@ -2,7 +2,9 @@ package io.github.dellisd.spatialk.turf import io.github.dellisd.spatialk.geojson.Feature import io.github.dellisd.spatialk.geojson.LineString +import io.github.dellisd.spatialk.geojson.Position import io.github.dellisd.spatialk.turf.utils.readResource +import kotlin.math.roundToInt import kotlin.test.Test import kotlin.test.assertEquals @@ -37,4 +39,18 @@ class TransformationTest { assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString)) } + + @Test + fun testSimplifyLineString() { + val feature = Feature.fromJson(readResource("transformation/simplify/in/linestring.json")) + val expected = Feature.fromJson(readResource("transformation/simplify/out/linestring.json")) + val simplified = simplify(feature.geometry as LineString, 0.01, false) + val roundedSimplified = LineString(simplified.coordinates.map { position -> + Position( + (position.longitude * 1000000).roundToInt() / 1000000.0, + (position.latitude * 1000000).roundToInt() / 1000000.0 + ) + }) + assertEquals(expected.geometry as LineString, roundedSimplified) + } } diff --git a/turf/src/commonTest/resources/transformation/simplify/in/linestring.json b/turf/src/commonTest/resources/transformation/simplify/in/linestring.json new file mode 100644 index 00000000..df7741be --- /dev/null +++ b/turf/src/commonTest/resources/transformation/simplify/in/linestring.json @@ -0,0 +1,329 @@ +{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -80.51399230957031, + 28.069556808283608 + ], + [ + -80.51193237304688, + 28.057438520876673 + ], + [ + -80.49819946289062, + 28.05622661698537 + ], + [ + -80.5023193359375, + 28.04471284867091 + ], + [ + -80.48583984375, + 28.042288740362853 + ], + [ + -80.50575256347656, + 28.028349057505775 + ], + [ + -80.50163269042969, + 28.02168161433489 + ], + [ + -80.49476623535156, + 28.021075462659883 + ], + [ + -80.48652648925781, + 28.021075462659883 + ], + [ + -80.47691345214844, + 28.021075462659883 + ], + [ + -80.46936035156249, + 28.015619944017807 + ], + [ + -80.47760009765624, + 28.007133032319448 + ], + [ + -80.49201965332031, + 27.998039170620494 + ], + [ + -80.46730041503906, + 27.962262536875905 + ], + [ + -80.46524047851562, + 27.91980029694533 + ], + [ + -80.40550231933594, + 27.930114089618602 + ], + [ + -80.39657592773438, + 27.980455528671527 + ], + [ + -80.41305541992188, + 27.982274659104082 + ], + [ + -80.42953491210938, + 27.990763528690582 + ], + [ + -80.4144287109375, + 28.00955793247135 + ], + [ + -80.3594970703125, + 27.972572275562527 + ], + [ + -80.36224365234375, + 27.948919060105453 + ], + [ + -80.38215637207031, + 27.913732900444284 + ], + [ + -80.41786193847656, + 27.881570017022806 + ], + [ + -80.40550231933594, + 27.860932192608534 + ], + [ + -80.39382934570312, + 27.85425440786446 + ], + [ + -80.37803649902344, + 27.86336037597851 + ], + [ + -80.38215637207031, + 27.880963078302393 + ], + [ + -80.36842346191405, + 27.888246118437756 + ], + [ + -80.35743713378906, + 27.882176952341734 + ], + [ + -80.35469055175781, + 27.86882358965466 + ], + [ + -80.3594970703125, + 27.8421119273228 + ], + [ + -80.37940979003906, + 27.83300417483936 + ], + [ + -80.39932250976561, + 27.82511017099003 + ], + [ + -80.40069580078125, + 27.79352841586229 + ], + [ + -80.36155700683594, + 27.786846483587688 + ], + [ + -80.35537719726562, + 27.794743268514615 + ], + [ + -80.36705017089844, + 27.800209937418252 + ], + [ + -80.36889553070068, + 27.801918215058347 + ], + [ + -80.3690242767334, + 27.803930152059845 + ], + [ + -80.36713600158691, + 27.805942051806845 + ], + [ + -80.36584854125977, + 27.805524490772143 + ], + [ + -80.36563396453857, + 27.80465140342285 + ], + [ + -80.36619186401367, + 27.803095012921272 + ], + [ + -80.36623477935791, + 27.801842292177923 + ], + [ + -80.36524772644043, + 27.80127286888392 + ], + [ + -80.36224365234375, + 27.801158983867033 + ], + [ + -80.36065578460693, + 27.802639479776524 + ], + [ + -80.36138534545898, + 27.803740348273823 + ], + [ + -80.36220073699951, + 27.804803245204976 + ], + [ + -80.36190032958984, + 27.806625330038287 + ], + [ + -80.3609561920166, + 27.80742248254359 + ], + [ + -80.35932540893555, + 27.806853088493792 + ], + [ + -80.35889625549315, + 27.806321651354835 + ], + [ + -80.35902500152588, + 27.805448570411585 + ], + [ + -80.35863876342773, + 27.804461600896783 + ], + [ + -80.35739421844482, + 27.804461600896783 + ], + [ + -80.35700798034668, + 27.805334689771293 + ], + [ + -80.35696506500244, + 27.80673920932572 + ], + [ + -80.35726547241211, + 27.80772615814989 + ], + [ + -80.35808086395264, + 27.808295547623707 + ], + [ + -80.3585958480835, + 27.80928248230861 + ], + [ + -80.35653591156006, + 27.80943431761813 + ], + [ + -80.35572052001953, + 27.808637179875486 + ], + [ + -80.3555917739868, + 27.80772615814989 + ], + [ + -80.3555917739868, + 27.806055931810487 + ], + [ + -80.35572052001953, + 27.803778309057556 + ], + [ + -80.35537719726562, + 27.801804330717825 + ], + [ + -80.3554630279541, + 27.799564581098746 + ], + [ + -80.35670757293701, + 27.799564581098746 + ], + [ + -80.35499095916748, + 27.796831264786892 + ], + [ + -80.34610748291016, + 27.79478123244122 + ], + [ + -80.34404754638672, + 27.802070060660014 + ], + [ + -80.34748077392578, + 27.804955086774896 + ], + [ + -80.3433609008789, + 27.805790211616266 + ], + [ + -80.34353256225586, + 27.8101555324401 + ], + [ + -80.33499240875244, + 27.810079615315917 + ], + [ + -80.33383369445801, + 27.805676331334084 + ], + [ + -80.33022880554199, + 27.801652484744796 + ], + [ + -80.32872676849365, + 27.80848534345178 + ] + ] + } +} diff --git a/turf/src/commonTest/resources/transformation/simplify/out/linestring.json b/turf/src/commonTest/resources/transformation/simplify/out/linestring.json new file mode 100644 index 00000000..64406419 --- /dev/null +++ b/turf/src/commonTest/resources/transformation/simplify/out/linestring.json @@ -0,0 +1,105 @@ +{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -80.513992, + 28.069557 + ], + [ + -80.48584, + 28.042289 + ], + [ + -80.505753, + 28.028349 + ], + [ + -80.476913, + 28.021075 + ], + [ + -80.49202, + 27.998039 + ], + [ + -80.4673, + 27.962263 + ], + [ + -80.46524, + 27.9198 + ], + [ + -80.405502, + 27.930114 + ], + [ + -80.396576, + 27.980456 + ], + [ + -80.429535, + 27.990764 + ], + [ + -80.414429, + 28.009558 + ], + [ + -80.359497, + 27.972572 + ], + [ + -80.382156, + 27.913733 + ], + [ + -80.417862, + 27.88157 + ], + [ + -80.393829, + 27.854254 + ], + [ + -80.368423, + 27.888246 + ], + [ + -80.354691, + 27.868824 + ], + [ + -80.359497, + 27.842112 + ], + [ + -80.399323, + 27.82511 + ], + [ + -80.400696, + 27.793528 + ], + [ + -80.361557, + 27.786846 + ], + [ + -80.359325, + 27.806853 + ], + [ + -80.354991, + 27.796831 + ], + [ + -80.328727, + 27.808485 + ] + ] + } +} From 69295866239e882af95e68f9de1928a0ab64527c Mon Sep 17 00:00:00 2001 From: Sargun Vohra Date: Tue, 23 Sep 2025 02:09:35 -0700 Subject: [PATCH 2/4] oops --- .../maplibre/spatialk/turf/Transformation.kt | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/turf/src/commonMain/kotlin/org/maplibre/spatialk/turf/Transformation.kt b/turf/src/commonMain/kotlin/org/maplibre/spatialk/turf/Transformation.kt index 6a47163f..ce585927 100644 --- a/turf/src/commonMain/kotlin/org/maplibre/spatialk/turf/Transformation.kt +++ b/turf/src/commonMain/kotlin/org/maplibre/spatialk/turf/Transformation.kt @@ -138,7 +138,28 @@ public fun bezierSpline(coords: List, duration: Int = 10_000, sharpnes return positions } -<<<<<<< HEAD:turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt +/** + * Takes a [Point] and calculates the circle polygon given a radius in degrees, radians, miles, or kilometers; and steps + * for precision. + * + * @param center center point of circle + * @param radius radius of the circle defined in [units] + * @param steps number of steps, must be at least four. Default is 64 + * @param units unit of [radius], default is [Units.Kilometers] + */ +@ExperimentalTurfApi +public fun circle(center: Point, radius: Double, steps: Int = 64, units: Units = Units.Kilometers): Polygon { + require(steps >= 4) { "circle needs to have four or more coordinates." } + require(radius > 0) { "radius must be a positive value" } + val coordinates = (0..steps).map { step -> + destination(center.coordinates, radius, (step * -360) / steps.toDouble(), units) + } + val ring = coordinates.plus(coordinates.first()) + return Polygon( + coordinates = listOf(ring), + bbox = computeBbox(ring) + ) +} private fun getSqDist(p1: Position, p2: Position): Double { val dx = p1.longitude - p2.longitude @@ -258,28 +279,3 @@ public fun simplify( return LineString(simplifyDouglasPeucker(simplifiedPoints, sqTolerance)) } -||||||| 812654b:turf/src/commonMain/kotlin/io/github/dellisd/spatialk/turf/Transformation.kt -======= -/** - * Takes a [Point] and calculates the circle polygon given a radius in degrees, radians, miles, or kilometers; and steps - * for precision. - * - * @param center center point of circle - * @param radius radius of the circle defined in [units] - * @param steps number of steps, must be at least four. Default is 64 - * @param units unit of [radius], default is [Units.Kilometers] - */ -@ExperimentalTurfApi -public fun circle(center: Point, radius: Double, steps: Int = 64, units: Units = Units.Kilometers): Polygon { - require(steps >= 4) { "circle needs to have four or more coordinates." } - require(radius > 0) { "radius must be a positive value" } - val coordinates = (0..steps).map { step -> - destination(center.coordinates, radius, (step * -360) / steps.toDouble(), units) - } - val ring = coordinates.plus(coordinates.first()) - return Polygon( - coordinates = listOf(ring), - bbox = computeBbox(ring) - ) -} ->>>>>>> main:turf/src/commonMain/kotlin/org/maplibre/spatialk/turf/Transformation.kt From b3f393510b8de9619a901afa0ca8116ccd071735 Mon Sep 17 00:00:00 2001 From: Sargun Vohra Date: Tue, 23 Sep 2025 02:10:18 -0700 Subject: [PATCH 3/4] oops --- .../spatialk/turf/TransformationTest.kt | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt b/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt index ee141797..e8b500fe 100644 --- a/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt +++ b/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt @@ -46,21 +46,6 @@ class TransformationTest { assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString)) } - @Test -<<<<<<< HEAD:turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt - fun testSimplifyLineString() { - val feature = Feature.fromJson(readResource("transformation/simplify/in/linestring.json")) - val expected = Feature.fromJson(readResource("transformation/simplify/out/linestring.json")) - val simplified = simplify(feature.geometry as LineString, 0.01, false) - val roundedSimplified = LineString(simplified.coordinates.map { position -> - Position( - (position.longitude * 1000000).roundToInt() / 1000000.0, - (position.latitude * 1000000).roundToInt() / 1000000.0 - ) - }) - assertEquals(expected.geometry as LineString, roundedSimplified) -||||||| 812654b:turf/src/commonTest/kotlin/io/github/dellisd/spatialk/turf/TransformationTest.kt -======= fun testCircle() { val point = Feature.fromJson(readResource("transformation/circle/in/circle1.json")) val expectedOut = FeatureCollection.fromJson(readResource("transformation/circle/out/circle1.json")) @@ -78,6 +63,19 @@ class TransformationTest { allCoordinates.forEachIndexed { i, position -> assertPositionEquals(position, circle.coordAll()[i]) } ->>>>>>> main:turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt + } + + @Test + fun testSimplifyLineString() { + val feature = Feature.fromJson(readResource("transformation/simplify/in/linestring.json")) + val expected = Feature.fromJson(readResource("transformation/simplify/out/linestring.json")) + val simplified = simplify(feature.geometry as LineString, 0.01, false) + val roundedSimplified = LineString(simplified.coordinates.map { position -> + Position( + (position.longitude * 1000000).roundToInt() / 1000000.0, + (position.latitude * 1000000).roundToInt() / 1000000.0 + ) + }) + assertEquals(expected.geometry as LineString, roundedSimplified) } } From 77b342e6160aed935e0b7e99108c872729492e35 Mon Sep 17 00:00:00 2001 From: Sargun Vohra Date: Tue, 23 Sep 2025 02:10:43 -0700 Subject: [PATCH 4/4] oops --- .../kotlin/org/maplibre/spatialk/turf/TransformationTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt b/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt index e8b500fe..afd70fac 100644 --- a/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt +++ b/turf/src/commonTest/kotlin/org/maplibre/spatialk/turf/TransformationTest.kt @@ -46,6 +46,7 @@ class TransformationTest { assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString)) } + @Test fun testCircle() { val point = Feature.fromJson(readResource("transformation/circle/in/circle1.json")) val expectedOut = FeatureCollection.fromJson(readResource("transformation/circle/out/circle1.json"))