Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -134,3 +135,123 @@ public fun bezierSpline(coords: List<Position>, 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<Position>, sqTolerance: Double): List<Position> {
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<Position>,
first: Int,
last: Int,
sqTolerance: Double,
simplified: MutableList<Position>
) {
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<Position>, sqTolerance: Double): List<Position> {
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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
}
Loading