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 @@ -18,13 +18,16 @@ import org.maplibre.spatialk.geojson.Polygon
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmSynthetic
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.tan

/**
* Takes a [LineString] and returns a [position][Position] at a specified distance along the line.
Expand Down Expand Up @@ -652,3 +655,92 @@ public fun greatCircle(start: Position, end: Position, pointCount: Int = 100, an
}
}

/**
* Calculates the distance between a given point and the nearest point on a line.
*
* @param point point to calculate from
* @param line line to calculate to
* @param units units of [distance]
*/
@ExperimentalTurfApi
public fun pointToLineDistance(point: Position, line: LineString, units: Units = Units.Kilometers): Double {
var distance = Double.MAX_VALUE

line.coordinates.drop(1).mapIndexed { idx, position ->
line.coordinates[idx] to position
}.forEach { (prev, cur) ->
val d = distanceToSegment(point, prev, cur, units)
if (d < distance) distance = d
}

return distance
}


@OptIn(ExperimentalTurfApi::class)
private fun distanceToSegment(
point: Position,
start: Position,
end: Position,
units: Units = Units.Meters
): Double {
fun dot(u: Position, v: Position): Double {
return u.longitude * v.longitude + u.latitude * v.latitude;
}

val segmentVector = Position(end.longitude - start.longitude, end.latitude - start.latitude)
val pointVector = Position(point.longitude - start.longitude, point.latitude - start.latitude)

val projectionLengthSquared = dot(pointVector, segmentVector)
if (projectionLengthSquared <= 0) {
return rhumbDistance(point, start, units);
}
val segmentLengthSquared = dot(segmentVector, segmentVector)
if (segmentLengthSquared <= projectionLengthSquared) {
return rhumbDistance(point, end, units);
}

val projectionRatio = projectionLengthSquared / segmentLengthSquared;
val projectedPoint = Position(
start.longitude + projectionRatio * segmentVector.longitude,
start.latitude + projectionRatio * segmentVector.latitude
)

return rhumbDistance(point, projectedPoint, units);
}

/**
* Calculates the distance along a rhumb line between two points.
*/
@OptIn(ExperimentalTurfApi::class)
public fun rhumbDistance(
origin: Position,
destination: Position,
units: Units = Units.Meters
): Double {
// compensate the crossing of the 180th meridian
val destination = Position(
destination.longitude + when {
destination.longitude - origin.longitude > 180 -> -360
origin.longitude - destination.longitude > 180 -> 360
else -> 0
}, destination.latitude
)

val phi1 = origin.latitude * PI / 180
val phi2 = destination.latitude * PI / 180
val deltaPhi = phi2 - phi1
var deltaLambda = abs(destination.longitude - origin.longitude) * PI / 180

if (deltaLambda > PI) {
deltaLambda -= 2 * PI
}

val deltaPsi = ln(tan(phi2 / 2 + PI / 4) / tan(phi1 / 2 + PI / 4))
val q = if (abs(deltaPsi) > 10e-12) deltaPhi / deltaPsi else cos(phi1)

val delta = sqrt(deltaPhi * deltaPhi + q * q * deltaLambda * deltaLambda)
val dist = delta * units.factor

return dist
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,29 @@ class TurfMeasurementTest {
greatCircle(start, antipodal)
}
}

@Test
fun testPointToLineDistance() {
val point = Position(-0.54931640625, 0.7470491450051796)
val line = LineString(
Position(1.0, 3.0),
Position(2.0, 2.0),
Position(2.0, 0.0),
Position(-1.5, -1.5)
)

val distance = pointToLineDistance(point, line)
assertEquals(188.01568693725255, distance)
}

@Test
fun testRhumbDistance() {
val distance = rhumbDistance(
Position(-75.343, 39.984),
Position(-75.534, 39.123),
Units.Kilometers
)

assertEquals(97.12923942772163, distance)
}
}