Skip to content

Commit

Permalink
Add bezierSpline transformation function (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTie authored May 5, 2024
1 parent 3e49a0d commit b54f691
Show file tree
Hide file tree
Showing 8 changed files with 1,752 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.github.dellisd.spatialk.turf

import io.github.dellisd.spatialk.geojson.LineString
import io.github.dellisd.spatialk.geojson.Position

/**
* Takes a [LineString] and returns a curved version by applying a Bezier spline algorithm.
*
* The bezier spline implementation is a port of the implementation by [Leszek Rybicki](http://leszek.rybicki.cc/)
* used in turfjs.
*
* @param line the input [LineString]
* @param duration time in milliseconds between points in the output data
* @param sharpness a measure of how curvy the path should be between splines
* @return A [LineString] containing a curved line around the positions of the input line
*/
@OptIn(ExperimentalTurfApi::class)
fun bezierSpline(line: LineString, duration: Int = 10_000, sharpness: Double = 0.85): LineString =
LineString(bezierSpline(line.coordAll(), duration, sharpness))

/**
* Takes a list of [Position] and returns a curved version by applying a Bezier spline algorithm.
*
* The bezier spline implementation is a port of the implementation by [Leszek Rybicki](http://leszek.rybicki.cc/)
* used in turfjs.
*
* @param coords the input list of [Position].
* @param duration time in milliseconds between points in the output data
* @param sharpness a measure of how curvy the path should be between splines
* @return A [List] containing [Position] of a curved line around the positions of the input line
*/
@Suppress("MagicNumber")
fun bezierSpline(coords: List<Position>, duration: Int = 10_000, sharpness: Double = 0.85): List<Position> {
// utility function to ensure a given altitude
fun Position.altitude() = altitude ?: 0.0

val controls = buildList {
val centers = (0..<coords.lastIndex).map { i ->
val p1 = coords[i]
val p2 = coords[i + 1]
Position(
longitude = (p1.longitude + p2.longitude) / 2,
latitude = (p1.latitude + p2.latitude) / 2,
altitude = (p1.altitude() + p2.altitude()) / 2,
)
}

add(
Pair(
coords[0],
coords[0]
)
)
for (i in 0..<centers.lastIndex) {
val dx = coords[i + 1].longitude - (centers[i].longitude + centers[i + 1].longitude) / 2
val dy = coords[i + 1].latitude - (centers[i].latitude + centers[i + 1].latitude) / 2
val dz = coords[i + 1].altitude() - (centers[i].altitude() + centers[i + 1].altitude()) / 2
add(
Pair(
Position(
longitude = (1.0 - sharpness) * coords[i + 1].longitude
+ sharpness * (centers[i].longitude + dx),
latitude = (1.0 - sharpness) * coords[i + 1].latitude
+ sharpness * (centers[i].latitude + dy),
altitude = (1.0 - sharpness) * coords[i + 1].altitude()
+ sharpness * (centers[i].altitude() + dz),
),
Position(
longitude = (1.0 - sharpness) * coords[i + 1].longitude
+ sharpness * (centers[i + 1].longitude + dx),
latitude = (1.0 - sharpness) * coords[i + 1].latitude
+ sharpness * (centers[i + 1].latitude + dy),
altitude = (1.0 - sharpness) * coords[i + 1].altitude()
+ sharpness * (centers[i + 1].altitude() + dz),
)
)
)

}
add(
Pair(
coords[coords.lastIndex],
coords[coords.lastIndex],
)
)
}

fun bezier(t: Double, p1: Position, c1: Position, c2: Position, p2: Position): Position {
val t2 = t * t
val t3 = t2 * t
val b = listOf(
t3,
3 * t2 * (1 - t),
3 * t * (1 - t) * (1 - t),
(1 - t) * (1 - t) * (1 - t),
)
return Position(
longitude = p2.longitude * b[0] + c2.longitude * b[1] + c1.longitude * b[2] + p1.longitude * b[3],
latitude = p2.latitude * b[0] + c2.latitude * b[1] + c1.latitude * b[2] + p1.latitude * b[3],
altitude = p2.altitude() * b[0] + c2.altitude() * b[1] + c1.altitude() * b[2] + p1.altitude() * b[3],
)
}

fun pos(time: Int): Position {
var t = time.coerceAtLeast(0)
if (t > duration) {
t = duration - 1
}

val t2 = t.toDouble() / duration
if (t2 >= 1) {
return coords[coords.lastIndex]
}

val n = (coords.lastIndex * t2).toInt()
val t1 = coords.lastIndex * t2 - n
return bezier(
t1,
coords[n],
controls[n].second,
controls[n + 1].first,
coords[n + 1]
)
}

val positions = (0..<duration step 10).plus(duration).mapNotNull { i ->
if ((i / 100) % 2 != 0) {
return@mapNotNull null
}

val pos = pos(i)
Position(pos.longitude, pos.latitude)
}

return positions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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.turf.utils.readResource
import kotlin.test.Test
import kotlin.test.assertEquals

@ExperimentalTurfApi
class TransformationTest {

@Test
fun testBezierSplineIn() {
val feature = Feature.fromJson(readResource("transformation/bezierspline/in/bezierIn.json"))
val expectedOut = Feature.fromJson(readResource("transformation/bezierspline/out/bezierIn.json"))

assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString))
}

@Test
fun testBezierSplineSimple() {
val feature = Feature.fromJson(readResource("transformation/bezierspline/in/simple.json"))
val expectedOut = Feature.fromJson(readResource("transformation/bezierspline/out/simple.json"))

assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString))
}

/**
* This test is designed to draw a bezierSpline across the 180th Meridian
*
* @see <a href="https://github.com/Turfjs/turf/issues/1063">
*/
@Test
fun testBezierSplineAcrossPacific() {
val feature = Feature.fromJson(readResource("transformation/bezierspline/in/issue-#1063.json"))
val expectedOut = Feature.fromJson(readResource("transformation/bezierspline/out/issue-#1063.json"))

assertEquals(expectedOut.geometry, bezierSpline(feature.geometry as LineString))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[-80.08724212646484, 32.77428536643231],
[-80.03746032714844, 32.84007757059952],
[-80.01548767089844, 32.74512501406368],
[-79.95368957519531, 32.850461360442424],
[-79.9361801147461, 32.75349876580794],
[-79.9310302734375, 32.79997320569839],
[-79.91043090820312, 32.78409957394813],
[-79.90528106689453, 32.8490192400596],
[-79.79919433593749, 32.76995522487643],
[-79.82494354248047, 32.810361684869015],
[-79.78683471679688, 32.83373132321818],
[-79.76554870605469, 32.80430188623444],
[-79.8098373413086, 32.726931048100624]
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[237.581, 37.7749],
[139.7731286197, 35.6669502038]
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[121.025390625, -22.91792293614603],
[130.6494140625, -19.394067895396613],
[138.33984375, -25.681137335685307],
[138.3837890625, -32.026706293336126]
]
}
}
Loading

0 comments on commit b54f691

Please sign in to comment.