Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -4,6 +4,7 @@ import org.maplibre.spatialk.geojson.LineString
import org.maplibre.spatialk.geojson.Point
import org.maplibre.spatialk.geojson.Polygon
import org.maplibre.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 @@ -159,3 +160,122 @@ public fun circle(center: Point, radius: Double, steps: Int = 64, units: Units =
bbox = computeBbox(ring)
)
}

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 @@ -4,10 +4,12 @@ import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.LineString
import org.maplibre.spatialk.geojson.Point
import org.maplibre.spatialk.geojson.Position
import org.maplibre.spatialk.turf.utils.assertPositionEquals
import org.maplibre.spatialk.turf.utils.readResource
import kotlinx.serialization.json.double
import kotlinx.serialization.json.jsonPrimitive
import kotlin.math.roundToInt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
Expand Down Expand Up @@ -63,4 +65,18 @@ class TransformationTest {
assertPositionEquals(position, circle.coordAll()[i])
}
}

@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