[WIP] refactor CircleTrajectory2D logic to be more correct
This commit is contained in:
parent
8bc1987acf
commit
614ca8d6f3
@ -1,6 +1,8 @@
|
|||||||
package space.kscience.kmath.geometry
|
package space.kscience.kmath.geometry
|
||||||
|
|
||||||
import space.kscience.kmath.operations.DoubleField.pow
|
import space.kscience.kmath.operations.DoubleField.pow
|
||||||
|
import space.kscience.trajectory.Pose2D
|
||||||
|
import space.kscience.trajectory.Trajectory2D
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
|
|
||||||
public fun Euclidean2DSpace.circle(x: Number, y: Number, radius: Number): Circle2D =
|
public fun Euclidean2DSpace.circle(x: Number, y: Number, radius: Number): Circle2D =
|
||||||
@ -47,3 +49,17 @@ public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute tangent pose to a circle
|
||||||
|
*
|
||||||
|
* @param bearing is counted the same way as in [Pose2D], from positive y clockwise
|
||||||
|
*/
|
||||||
|
public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Euclidean2DSpace) {
|
||||||
|
val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing))
|
||||||
|
val tangentAngle = when (direction) {
|
||||||
|
Trajectory2D.R -> bearing + Angle.piDiv2
|
||||||
|
Trajectory2D.L -> bearing - Angle.piDiv2
|
||||||
|
}.normalized()
|
||||||
|
Pose2D(coordinates, tangentAngle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,11 @@ import space.kscience.kmath.geometry.Euclidean2DSpace.distanceTo
|
|||||||
import space.kscience.trajectory.Trajectory2D.*
|
import space.kscience.trajectory.Trajectory2D.*
|
||||||
import kotlin.math.acos
|
import kotlin.math.acos
|
||||||
|
|
||||||
internal fun DubinsPose2D.getLeftCircle(radius: Double): Circle2D = getTangentCircles(radius).first
|
internal fun Pose2D.getLeftCircle(radius: Double): Circle2D = getTangentCircles(radius).first
|
||||||
|
|
||||||
internal fun DubinsPose2D.getRightCircle(radius: Double): Circle2D = getTangentCircles(radius).second
|
internal fun Pose2D.getRightCircle(radius: Double): Circle2D = getTangentCircles(radius).second
|
||||||
|
|
||||||
internal fun DubinsPose2D.getTangentCircles(radius: Double): Pair<Circle2D, Circle2D> = with(Euclidean2DSpace) {
|
internal fun Pose2D.getTangentCircles(radius: Double): Pair<Circle2D, Circle2D> = with(Euclidean2DSpace) {
|
||||||
val dX = radius * cos(bearing)
|
val dX = radius * cos(bearing)
|
||||||
val dY = radius * sin(bearing)
|
val dY = radius * sin(bearing)
|
||||||
return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius)
|
return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius)
|
||||||
@ -102,8 +102,8 @@ public object DubinsPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun all(
|
public fun all(
|
||||||
start: DubinsPose2D,
|
start: Pose2D,
|
||||||
end: DubinsPose2D,
|
end: Pose2D,
|
||||||
turningRadius: Double,
|
turningRadius: Double,
|
||||||
): List<CompositeTrajectory2D> = listOfNotNull(
|
): List<CompositeTrajectory2D> = listOfNotNull(
|
||||||
rlr(start, end, turningRadius),
|
rlr(start, end, turningRadius),
|
||||||
@ -114,10 +114,10 @@ public object DubinsPath {
|
|||||||
lsr(start, end, turningRadius)
|
lsr(start, end, turningRadius)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun shortest(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D =
|
public fun shortest(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D =
|
||||||
all(start, end, turningRadius).minBy { it.length }
|
all(start, end, turningRadius).minBy { it.length }
|
||||||
|
|
||||||
public fun rlr(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D? =
|
public fun rlr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? =
|
||||||
with(Euclidean2DSpace) {
|
with(Euclidean2DSpace) {
|
||||||
val c1 = start.getRightCircle(turningRadius)
|
val c1 = start.getRightCircle(turningRadius)
|
||||||
val c2 = end.getRightCircle(turningRadius)
|
val c2 = end.getRightCircle(turningRadius)
|
||||||
@ -135,9 +135,9 @@ public object DubinsPath {
|
|||||||
dX = turningRadius * sin(theta)
|
dX = turningRadius * sin(theta)
|
||||||
dY = turningRadius * cos(theta)
|
dY = turningRadius * cos(theta)
|
||||||
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, p1, R)
|
val a1 = CircleTrajectory2D(c1.center, start, p1, R)
|
||||||
val a2 = CircleTrajectory2D.of(e.center, p1, p2, L)
|
val a2 = CircleTrajectory2D(e.center, p1, p2, L)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, p2, end, R)
|
val a3 = CircleTrajectory2D(c2.center, p2, end, R)
|
||||||
CompositeTrajectory2D(a1, a2, a3)
|
CompositeTrajectory2D(a1, a2, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,16 +152,16 @@ public object DubinsPath {
|
|||||||
dX = turningRadius * sin(theta)
|
dX = turningRadius * sin(theta)
|
||||||
dY = turningRadius * cos(theta)
|
dY = turningRadius * cos(theta)
|
||||||
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, p1, R)
|
val a1 = CircleTrajectory2D(c1.center, start, p1, R)
|
||||||
val a2 = CircleTrajectory2D.of(e.center, p1, p2, L)
|
val a2 = CircleTrajectory2D(e.center, p1, p2, L)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, p2, end, R)
|
val a3 = CircleTrajectory2D(c2.center, p2, end, R)
|
||||||
CompositeTrajectory2D(a1, a2, a3)
|
CompositeTrajectory2D(a1, a2, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (firstVariant.length < secondVariant.length) firstVariant else secondVariant
|
return if (firstVariant.length < secondVariant.length) firstVariant else secondVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun lrl(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D? =
|
public fun lrl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? =
|
||||||
with(Euclidean2DSpace) {
|
with(Euclidean2DSpace) {
|
||||||
val c1 = start.getLeftCircle(turningRadius)
|
val c1 = start.getLeftCircle(turningRadius)
|
||||||
val c2 = end.getLeftCircle(turningRadius)
|
val c2 = end.getLeftCircle(turningRadius)
|
||||||
@ -179,9 +179,9 @@ public object DubinsPath {
|
|||||||
dX = turningRadius * sin(theta)
|
dX = turningRadius * sin(theta)
|
||||||
dY = turningRadius * cos(theta)
|
dY = turningRadius * cos(theta)
|
||||||
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, p1, L)
|
val a1 = CircleTrajectory2D(c1.center, start, p1, L)
|
||||||
val a2 = CircleTrajectory2D.of(e.center, p1, p2, R)
|
val a2 = CircleTrajectory2D(e.center, p1, p2, R)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, p2, end, L)
|
val a3 = CircleTrajectory2D(c2.center, p2, end, L)
|
||||||
CompositeTrajectory2D(a1, a2, a3)
|
CompositeTrajectory2D(a1, a2, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,52 +196,52 @@ public object DubinsPath {
|
|||||||
dX = turningRadius * sin(theta)
|
dX = turningRadius * sin(theta)
|
||||||
dY = turningRadius * cos(theta)
|
dY = turningRadius * cos(theta)
|
||||||
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
val p2 = vector(e.center.x + dX, e.center.y + dY)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, p1, L)
|
val a1 = CircleTrajectory2D(c1.center, start, p1, L)
|
||||||
val a2 = CircleTrajectory2D.of(e.center, p1, p2, R)
|
val a2 = CircleTrajectory2D(e.center, p1, p2, R)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, p2, end, L)
|
val a3 = CircleTrajectory2D(c2.center, p2, end, L)
|
||||||
CompositeTrajectory2D(a1, a2, a3)
|
CompositeTrajectory2D(a1, a2, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (firstVariant.length < secondVariant.length) firstVariant else secondVariant
|
return if (firstVariant.length < secondVariant.length) firstVariant else secondVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun rsr(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D {
|
public fun rsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D {
|
||||||
val c1 = start.getRightCircle(turningRadius)
|
val c1 = start.getRightCircle(turningRadius)
|
||||||
val c2 = end.getRightCircle(turningRadius)
|
val c2 = end.getRightCircle(turningRadius)
|
||||||
val s = outerTangent(c1, c2, L)
|
val s = outerTangent(c1, c2, L)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, s.begin, R)
|
val a1 = CircleTrajectory2D(c1.center, start, s.begin, R)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, s.end, end, R)
|
val a3 = CircleTrajectory2D(c2.center, s.end, end, R)
|
||||||
return CompositeTrajectory2D(a1, s, a3)
|
return CompositeTrajectory2D(a1, s, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun lsl(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D {
|
public fun lsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D {
|
||||||
val c1 = start.getLeftCircle(turningRadius)
|
val c1 = start.getLeftCircle(turningRadius)
|
||||||
val c2 = end.getLeftCircle(turningRadius)
|
val c2 = end.getLeftCircle(turningRadius)
|
||||||
val s = outerTangent(c1, c2, R)
|
val s = outerTangent(c1, c2, R)
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, s.begin, L)
|
val a1 = CircleTrajectory2D(c1.center, start, s.begin, L)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, s.end, end, L)
|
val a3 = CircleTrajectory2D(c2.center, s.end, end, L)
|
||||||
return CompositeTrajectory2D(a1, s, a3)
|
return CompositeTrajectory2D(a1, s, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun rsl(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D? {
|
public fun rsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? {
|
||||||
val c1 = start.getRightCircle(turningRadius)
|
val c1 = start.getRightCircle(turningRadius)
|
||||||
val c2 = end.getLeftCircle(turningRadius)
|
val c2 = end.getLeftCircle(turningRadius)
|
||||||
val s = innerTangent(c1, c2, R)
|
val s = innerTangent(c1, c2, R)
|
||||||
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
|
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
|
||||||
|
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, s.begin, R)
|
val a1 = CircleTrajectory2D(c1.center, start, s.begin, R)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, s.end, end, L)
|
val a3 = CircleTrajectory2D(c2.center, s.end, end, L)
|
||||||
return CompositeTrajectory2D(a1, s, a3)
|
return CompositeTrajectory2D(a1, s, a3)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun lsr(start: DubinsPose2D, end: DubinsPose2D, turningRadius: Double): CompositeTrajectory2D? {
|
public fun lsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? {
|
||||||
val c1 = start.getLeftCircle(turningRadius)
|
val c1 = start.getLeftCircle(turningRadius)
|
||||||
val c2 = end.getRightCircle(turningRadius)
|
val c2 = end.getRightCircle(turningRadius)
|
||||||
val s = innerTangent(c1, c2, L)
|
val s = innerTangent(c1, c2, L)
|
||||||
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
|
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
|
||||||
|
|
||||||
val a1 = CircleTrajectory2D.of(c1.center, start, s.begin, L)
|
val a1 = CircleTrajectory2D(c1.center, start, s.begin, L)
|
||||||
val a3 = CircleTrajectory2D.of(c2.center, s.end, end, R)
|
val a3 = CircleTrajectory2D(c2.center, s.end, end, R)
|
||||||
return CompositeTrajectory2D(a1, s, a3)
|
return CompositeTrajectory2D(a1, s, a3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,14 +48,14 @@ private class ObstacleImpl(override val circles: List<Circle2D>) : Obstacle {
|
|||||||
|
|
||||||
if (circles.size == 1) {
|
if (circles.size == 1) {
|
||||||
// a circumvention consisting of a single circle, starting on top
|
// a circumvention consisting of a single circle, starting on top
|
||||||
val circle = circles.first()
|
// val circle = circles.first()
|
||||||
val top = vector(circle.center.x + circle.radius, circle.center.y)
|
// val top = vector(circle.center.x + circle.radius, circle.center.y)
|
||||||
val startEnd = DubinsPose2D(
|
// val start = DubinsPose2D(
|
||||||
top,
|
// top,
|
||||||
Angle.piDiv2
|
// Angle.piDiv2
|
||||||
)
|
// )
|
||||||
return@lazy CompositeTrajectory2D(
|
return@lazy CompositeTrajectory2D(
|
||||||
CircleTrajectory2D(circle, startEnd, startEnd)
|
CircleTrajectory2D(circles.first(), Angle.zero, Angle.zero)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,14 +158,15 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
|
|||||||
//cutting first and last arcs to accommodate connection points
|
//cutting first and last arcs to accommodate connection points
|
||||||
val first = circumvention.first() as CircleTrajectory2D
|
val first = circumvention.first() as CircleTrajectory2D
|
||||||
val last = circumvention.last() as CircleTrajectory2D
|
val last = circumvention.last() as CircleTrajectory2D
|
||||||
|
//arc between end of the nangent and end of previous arc (begin of the the next one)
|
||||||
circumvention[0] = CircleTrajectory2D(
|
circumvention[0] = CircleTrajectory2D(
|
||||||
first.circle,
|
first.circle,
|
||||||
tangent1.tangentTrajectory.endPose,
|
tangent1.tangentTrajectory.endPose,
|
||||||
first.end
|
first.endPose,
|
||||||
)
|
)
|
||||||
circumvention[circumvention.lastIndex] = CircleTrajectory2D(
|
circumvention[circumvention.lastIndex] = CircleTrajectory2D(
|
||||||
last.circle,
|
last.circle,
|
||||||
last.begin,
|
last.beginPose,
|
||||||
tangent2.tangentTrajectory.beginPose
|
tangent2.tangentTrajectory.beginPose
|
||||||
)
|
)
|
||||||
return CompositeTrajectory2D(circumvention)
|
return CompositeTrajectory2D(circumvention)
|
||||||
@ -271,10 +272,10 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun constructTangentCircles(
|
private fun constructTangentCircles(
|
||||||
pose: DubinsPose2D,
|
pose: Pose2D,
|
||||||
r: Double,
|
r: Double,
|
||||||
): LR<Circle2D> = with(Euclidean2DSpace) {
|
): LR<Circle2D> = with(Euclidean2DSpace) {
|
||||||
val direction = DubinsPose2D.bearingToVector(pose.bearing)
|
val direction = Pose2D.bearingToVector(pose.bearing)
|
||||||
//TODO optimize to use bearing
|
//TODO optimize to use bearing
|
||||||
val center1 = pose + normalVectors(direction, r).first
|
val center1 = pose + normalVectors(direction, r).first
|
||||||
val center2 = pose + normalVectors(direction, r).second
|
val center2 = pose + normalVectors(direction, r).second
|
||||||
@ -293,8 +294,8 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun avoidObstacles(
|
public fun avoidObstacles(
|
||||||
start: DubinsPose2D,
|
start: Pose2D,
|
||||||
finish: DubinsPose2D,
|
finish: Pose2D,
|
||||||
startingRadius: Double,
|
startingRadius: Double,
|
||||||
obstacleList: List<Obstacle>,
|
obstacleList: List<Obstacle>,
|
||||||
finalRadius: Double = startingRadius,
|
finalRadius: Double = startingRadius,
|
||||||
@ -328,15 +329,15 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun avoidObstacles(
|
public fun avoidObstacles(
|
||||||
start: DubinsPose2D,
|
start: Pose2D,
|
||||||
finish: DubinsPose2D,
|
finish: Pose2D,
|
||||||
trajectoryRadius: Double,
|
trajectoryRadius: Double,
|
||||||
vararg obstacles: Obstacle,
|
vararg obstacles: Obstacle,
|
||||||
): List<Trajectory2D> = avoidObstacles(start, finish, trajectoryRadius, obstacles.toList())
|
): List<Trajectory2D> = avoidObstacles(start, finish, trajectoryRadius, obstacles.toList())
|
||||||
|
|
||||||
public fun avoidPolygons(
|
public fun avoidPolygons(
|
||||||
start: DubinsPose2D,
|
start: Pose2D,
|
||||||
finish: DubinsPose2D,
|
finish: Pose2D,
|
||||||
trajectoryRadius: Double,
|
trajectoryRadius: Double,
|
||||||
vararg polygons: Polygon<Double>,
|
vararg polygons: Polygon<Double>,
|
||||||
): List<Trajectory2D> {
|
): List<Trajectory2D> {
|
||||||
@ -348,8 +349,8 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
|
|||||||
|
|
||||||
|
|
||||||
public fun avoidPolygons(
|
public fun avoidPolygons(
|
||||||
start: DubinsPose2D,
|
start: Pose2D,
|
||||||
finish: DubinsPose2D,
|
finish: Pose2D,
|
||||||
trajectoryRadius: Double,
|
trajectoryRadius: Double,
|
||||||
polygons: Collection<Polygon<Double>>,
|
polygons: Collection<Polygon<Double>>,
|
||||||
): List<Trajectory2D> {
|
): List<Trajectory2D> {
|
||||||
|
@ -19,15 +19,15 @@ import kotlin.math.atan2
|
|||||||
/**
|
/**
|
||||||
* Combination of [Vector] and its view angle (clockwise from positive y-axis direction)
|
* Combination of [Vector] and its view angle (clockwise from positive y-axis direction)
|
||||||
*/
|
*/
|
||||||
@Serializable(DubinsPose2DSerializer::class)
|
@Serializable(Pose2DSerializer::class)
|
||||||
public interface DubinsPose2D : DoubleVector2D {
|
public interface Pose2D : DoubleVector2D {
|
||||||
public val coordinates: DoubleVector2D
|
public val coordinates: DoubleVector2D
|
||||||
public val bearing: Angle
|
public val bearing: Angle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse the direction of this pose to the opposite, keeping other parameters the same
|
* Reverse the direction of this pose to the opposite, keeping other parameters the same
|
||||||
*/
|
*/
|
||||||
public fun reversed(): DubinsPose2D
|
public fun reversed(): Pose2D
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public fun bearingToVector(bearing: Angle): Vector2D<Double> =
|
public fun bearingToVector(bearing: Angle): Vector2D<Double> =
|
||||||
@ -45,45 +45,62 @@ public interface DubinsPose2D : DoubleVector2D {
|
|||||||
public class PhaseVector2D(
|
public class PhaseVector2D(
|
||||||
override val coordinates: DoubleVector2D,
|
override val coordinates: DoubleVector2D,
|
||||||
public val velocity: DoubleVector2D,
|
public val velocity: DoubleVector2D,
|
||||||
) : DubinsPose2D, DoubleVector2D by coordinates {
|
) : Pose2D, DoubleVector2D by coordinates {
|
||||||
override val bearing: Angle get() = atan2(velocity.x, velocity.y).radians
|
override val bearing: Angle get() = atan2(velocity.x, velocity.y).radians
|
||||||
|
|
||||||
override fun reversed(): DubinsPose2D = with(Euclidean2DSpace) { PhaseVector2D(coordinates, -velocity) }
|
override fun reversed(): Pose2D = with(Euclidean2DSpace) { PhaseVector2D(coordinates, -velocity) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("DubinsPose2D")
|
@SerialName("DubinsPose2D")
|
||||||
private class DubinsPose2DImpl(
|
private class Pose2DImpl(
|
||||||
override val coordinates: DoubleVector2D,
|
override val coordinates: DoubleVector2D,
|
||||||
override val bearing: Angle,
|
override val bearing: Angle,
|
||||||
) : DubinsPose2D, DoubleVector2D by coordinates {
|
) : Pose2D, DoubleVector2D by coordinates {
|
||||||
|
|
||||||
|
override fun reversed(): Pose2D = Pose2DImpl(coordinates, bearing.plus(Angle.pi).normalized())
|
||||||
|
|
||||||
|
|
||||||
override fun reversed(): DubinsPose2D = DubinsPose2DImpl(coordinates, bearing.plus(Angle.pi).normalized())
|
|
||||||
|
|
||||||
override fun toString(): String = "Pose2D(x=$x, y=$y, bearing=$bearing)"
|
override fun toString(): String = "Pose2D(x=$x, y=$y, bearing=$bearing)"
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as Pose2DImpl
|
||||||
|
|
||||||
|
if (coordinates != other.coordinates) return false
|
||||||
|
return bearing == other.bearing
|
||||||
}
|
}
|
||||||
|
|
||||||
public object DubinsPose2DSerializer : KSerializer<DubinsPose2D> {
|
override fun hashCode(): Int {
|
||||||
private val proxySerializer = DubinsPose2DImpl.serializer()
|
var result = coordinates.hashCode()
|
||||||
|
result = 31 * result + bearing.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Pose2DSerializer : KSerializer<Pose2D> {
|
||||||
|
private val proxySerializer = Pose2DImpl.serializer()
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor
|
override val descriptor: SerialDescriptor
|
||||||
get() = proxySerializer.descriptor
|
get() = proxySerializer.descriptor
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): DubinsPose2D {
|
override fun deserialize(decoder: Decoder): Pose2D {
|
||||||
return decoder.decodeSerializableValue(proxySerializer)
|
return decoder.decodeSerializableValue(proxySerializer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: DubinsPose2D) {
|
override fun serialize(encoder: Encoder, value: Pose2D) {
|
||||||
val pose = value as? DubinsPose2DImpl ?: DubinsPose2DImpl(value.coordinates, value.bearing)
|
val pose = value as? Pose2DImpl ?: Pose2DImpl(value.coordinates, value.bearing)
|
||||||
encoder.encodeSerializableValue(proxySerializer, pose)
|
encoder.encodeSerializableValue(proxySerializer, pose)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun DubinsPose2D(coordinate: DoubleVector2D, bearing: Angle): DubinsPose2D =
|
public fun Pose2D(coordinate: DoubleVector2D, bearing: Angle): Pose2D =
|
||||||
DubinsPose2DImpl(coordinate, bearing)
|
Pose2DImpl(coordinate, bearing)
|
||||||
|
|
||||||
public fun DubinsPose2D(point: DoubleVector2D, direction: DoubleVector2D): DubinsPose2D =
|
public fun Pose2D(point: DoubleVector2D, direction: DoubleVector2D): Pose2D =
|
||||||
DubinsPose2D(point, DubinsPose2D.vectorToBearing(direction))
|
Pose2D(point, Pose2D.vectorToBearing(direction))
|
||||||
|
|
||||||
public fun DubinsPose2D(x: Number, y: Number, bearing: Angle): DubinsPose2D =
|
public fun Pose2D(x: Number, y: Number, bearing: Angle): Pose2D =
|
||||||
DubinsPose2DImpl(Euclidean2DSpace.vector(x, y), bearing)
|
Pose2DImpl(Euclidean2DSpace.vector(x, y), bearing)
|
@ -18,8 +18,8 @@ import kotlin.math.atan2
|
|||||||
public sealed interface Trajectory2D {
|
public sealed interface Trajectory2D {
|
||||||
public val length: Double
|
public val length: Double
|
||||||
|
|
||||||
public val beginPose: DubinsPose2D
|
public val beginPose: Pose2D
|
||||||
public val endPose: DubinsPose2D
|
public val endPose: Pose2D
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce a trajectory with reversed order of points
|
* Produce a trajectory with reversed order of points
|
||||||
@ -60,8 +60,8 @@ public data class StraightTrajectory2D(
|
|||||||
|
|
||||||
public val bearing: Angle get() = (end - begin).bearing
|
public val bearing: Angle get() = (end - begin).bearing
|
||||||
|
|
||||||
override val beginPose: DubinsPose2D get() = DubinsPose2D(begin, bearing)
|
override val beginPose: Pose2D get() = Pose2D(begin, bearing)
|
||||||
override val endPose: DubinsPose2D get() = DubinsPose2D(end, bearing)
|
override val endPose: Pose2D get() = Pose2D(end, bearing)
|
||||||
|
|
||||||
override fun reversed(): StraightTrajectory2D = StraightTrajectory2D(end, begin)
|
override fun reversed(): StraightTrajectory2D = StraightTrajectory2D(end, begin)
|
||||||
}
|
}
|
||||||
@ -76,70 +76,86 @@ public fun StraightTrajectory2D(segment: LineSegment2D): StraightTrajectory2D =
|
|||||||
@SerialName("arc")
|
@SerialName("arc")
|
||||||
public data class CircleTrajectory2D(
|
public data class CircleTrajectory2D(
|
||||||
public val circle: Circle2D,
|
public val circle: Circle2D,
|
||||||
public val begin: DubinsPose2D,
|
public val arcStart: Angle,
|
||||||
public val end: DubinsPose2D,
|
public val arcAngle: Angle,
|
||||||
) : Trajectory2D {
|
) : Trajectory2D {
|
||||||
|
public val direction: Trajectory2D.Direction = if (arcAngle > Angle.zero) Trajectory2D.R else Trajectory2D.L
|
||||||
|
|
||||||
override val beginPose: DubinsPose2D get() = begin
|
public val arcEnd: Angle = arcStart + arcAngle
|
||||||
override val endPose: DubinsPose2D get() = end
|
override val beginPose: Pose2D get() = circle.tangent(arcStart, direction)
|
||||||
|
override val endPose: Pose2D get() = circle.tangent(arcEnd, direction)
|
||||||
/**
|
|
||||||
* Arc length in radians
|
|
||||||
*/
|
|
||||||
val arcAngle: Angle
|
|
||||||
get() = if (direction == Trajectory2D.L) {
|
|
||||||
begin.bearing - end.bearing
|
|
||||||
} else {
|
|
||||||
end.bearing - begin.bearing
|
|
||||||
}.normalized()
|
|
||||||
|
|
||||||
|
|
||||||
override val length: Double by lazy {
|
override val length: Double by lazy {
|
||||||
circle.radius * arcAngle.radians
|
circle.radius * kotlin.math.abs(arcAngle.radians)
|
||||||
}
|
}
|
||||||
|
|
||||||
public val direction: Trajectory2D.Direction by lazy {
|
|
||||||
when {
|
override fun reversed(): CircleTrajectory2D = CircleTrajectory2D(circle, arcEnd, -arcAngle)
|
||||||
begin.y < circle.center.y -> if (begin.bearing > Angle.pi) Trajectory2D.R else Trajectory2D.L
|
|
||||||
begin.y > circle.center.y -> if (begin.bearing < Angle.pi) Trajectory2D.R else Trajectory2D.L
|
public companion object
|
||||||
else -> if (begin.bearing == Angle.zero) {
|
|
||||||
if (begin.x < circle.center.x) Trajectory2D.R else Trajectory2D.L
|
|
||||||
} else {
|
|
||||||
if (begin.x > circle.center.x) Trajectory2D.R else Trajectory2D.L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reversed(): CircleTrajectory2D = CircleTrajectory2D(circle, end.reversed(), begin.reversed())
|
public fun CircleTrajectory2D(
|
||||||
|
|
||||||
public companion object {
|
|
||||||
public fun of(
|
|
||||||
center: DoubleVector2D,
|
center: DoubleVector2D,
|
||||||
start: DoubleVector2D,
|
start: DoubleVector2D,
|
||||||
end: DoubleVector2D,
|
end: DoubleVector2D,
|
||||||
direction: Trajectory2D.Direction,
|
direction: Trajectory2D.Direction,
|
||||||
): CircleTrajectory2D {
|
): CircleTrajectory2D = with(Euclidean2DSpace) {
|
||||||
fun calculatePose(
|
// fun calculatePose(
|
||||||
vector: DoubleVector2D,
|
// vector: DoubleVector2D,
|
||||||
theta: Angle,
|
// theta: Angle,
|
||||||
direction: Trajectory2D.Direction,
|
// direction: Trajectory2D.Direction,
|
||||||
): DubinsPose2D = DubinsPose2D(
|
// ): DubinsPose2D = DubinsPose2D(
|
||||||
vector,
|
// vector,
|
||||||
|
// when (direction) {
|
||||||
|
// Trajectory2D.L -> (theta - Angle.piDiv2).normalized()
|
||||||
|
// Trajectory2D.R -> (theta + Angle.piDiv2).normalized()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// val s1 = StraightTrajectory2D(center, start)
|
||||||
|
// val s2 = StraightTrajectory2D(center, end)
|
||||||
|
// val pose1 = calculatePose(start, s1.bearing, direction)
|
||||||
|
// val pose2 = calculatePose(end, s2.bearing, direction)
|
||||||
|
// val trajectory = CircleTrajectory2D(Circle2D(center, s1.length), pose1, pose2)
|
||||||
|
// if (trajectory.direction != direction) error("Trajectory direction mismatch")
|
||||||
|
// return trajectory
|
||||||
|
val startVector = start - center
|
||||||
|
val endVector = end - center
|
||||||
|
val startRadius = norm(startVector)
|
||||||
|
val endRadius = norm(endVector)
|
||||||
|
require((startRadius - endRadius) / startRadius < 1e-6) { "Start and end points have different radii" }
|
||||||
|
val radius = (startRadius + endRadius) / 2
|
||||||
|
val startBearing = startVector.bearing
|
||||||
|
val endBearing = endVector.bearing
|
||||||
|
CircleTrajectory2D(
|
||||||
|
Circle2D(center, radius),
|
||||||
|
startBearing,
|
||||||
when (direction) {
|
when (direction) {
|
||||||
Trajectory2D.L -> (theta - Angle.piDiv2).normalized()
|
Trajectory2D.L -> if (endBearing >= startBearing) {
|
||||||
Trajectory2D.R -> (theta + Angle.piDiv2).normalized()
|
endBearing - startBearing - Angle.piTimes2
|
||||||
|
} else {
|
||||||
|
endBearing - startBearing
|
||||||
|
}
|
||||||
|
|
||||||
|
Trajectory2D.R -> if (endBearing >= startBearing) {
|
||||||
|
endBearing - startBearing
|
||||||
|
} else {
|
||||||
|
endBearing + Angle.piTimes2 - startBearing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val s1 = StraightTrajectory2D(center, start)
|
public fun CircleTrajectory2D(
|
||||||
val s2 = StraightTrajectory2D(center, end)
|
circle: Circle2D,
|
||||||
val pose1 = calculatePose(start, s1.bearing, direction)
|
beginPose: Pose2D,
|
||||||
val pose2 = calculatePose(end, s2.bearing, direction)
|
endPose: Pose2D,
|
||||||
val trajectory = CircleTrajectory2D(Circle2D(center, s1.length), pose1, pose2)
|
): CircleTrajectory2D = with(Euclidean2DSpace) {
|
||||||
if (trajectory.direction != direction) error("Trajectory direction mismatch")
|
val vectorToBegin = beginPose - circle.center
|
||||||
return trajectory
|
val vectorToEnd = endPose - circle.center
|
||||||
}
|
//TODO check pose bearing
|
||||||
}
|
return CircleTrajectory2D(circle, vectorToBegin.bearing, vectorToEnd.bearing - vectorToBegin.bearing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -147,8 +163,8 @@ public data class CircleTrajectory2D (
|
|||||||
public class CompositeTrajectory2D(public val segments: List<Trajectory2D>) : Trajectory2D {
|
public class CompositeTrajectory2D(public val segments: List<Trajectory2D>) : Trajectory2D {
|
||||||
override val length: Double get() = segments.sumOf { it.length }
|
override val length: Double get() = segments.sumOf { it.length }
|
||||||
|
|
||||||
override val beginPose: DubinsPose2D get() = segments.first().beginPose
|
override val beginPose: Pose2D get() = segments.first().beginPose
|
||||||
override val endPose: DubinsPose2D get() = segments.last().endPose
|
override val endPose: Pose2D get() = segments.last().endPose
|
||||||
|
|
||||||
override fun reversed(): CompositeTrajectory2D = CompositeTrajectory2D(segments.map { it.reversed() }.reversed())
|
override fun reversed(): CompositeTrajectory2D = CompositeTrajectory2D(segments.map { it.reversed() }.reversed())
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
package space.kscience.kmath.geometry
|
package space.kscience.kmath.geometry
|
||||||
|
|
||||||
import space.kscience.trajectory.CircleTrajectory2D
|
import space.kscience.trajectory.CircleTrajectory2D
|
||||||
import space.kscience.trajectory.DubinsPose2D
|
import space.kscience.trajectory.Pose2D
|
||||||
import space.kscience.trajectory.Trajectory2D
|
import space.kscience.trajectory.Trajectory2D
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@ -17,15 +17,15 @@ class ArcTests {
|
|||||||
@Test
|
@Test
|
||||||
fun arc() = with(Euclidean2DSpace) {
|
fun arc() = with(Euclidean2DSpace) {
|
||||||
val circle = Circle2D(vector(0.0, 0.0), 2.0)
|
val circle = Circle2D(vector(0.0, 0.0), 2.0)
|
||||||
val arc = CircleTrajectory2D.of(
|
val arc = CircleTrajectory2D(
|
||||||
circle.center,
|
circle.center,
|
||||||
vector(-2.0, 0.0),
|
vector(-2.0, 0.0),
|
||||||
vector(0.0, 2.0),
|
vector(0.0, 2.0),
|
||||||
Trajectory2D.R
|
Trajectory2D.R
|
||||||
)
|
)
|
||||||
assertEquals(circle.circumference / 4, arc.length, 1.0)
|
assertEquals(circle.circumference / 4, arc.length, 1.0)
|
||||||
assertEquals(0.0, arc.begin.bearing.degrees)
|
assertEquals(0.0, arc.beginPose.bearing.degrees)
|
||||||
assertEquals(90.0, arc.end.bearing.degrees)
|
assertEquals(90.0, arc.endPose.bearing.degrees)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -33,8 +33,8 @@ class ArcTests {
|
|||||||
val circle = circle(1, 0, 1)
|
val circle = circle(1, 0, 1)
|
||||||
val arc = CircleTrajectory2D(
|
val arc = CircleTrajectory2D(
|
||||||
circle,
|
circle,
|
||||||
DubinsPose2D(x = 2.0, y = 1.2246467991473532E-16, bearing = PI.radians),
|
Pose2D(x = 2.0, y = 1.2246467991473532E-16, bearing = PI.radians),
|
||||||
DubinsPose2D(x = 1.0, y = -1.0, bearing = (PI*3/2).radians)
|
Pose2D(x = 1.0, y = -1.0, bearing = (PI*3/2).radians)
|
||||||
)
|
)
|
||||||
assertEquals(Trajectory2D.R, arc.direction)
|
assertEquals(Trajectory2D.R, arc.direction)
|
||||||
assertEquals(PI / 2, arc.length, 1e-4)
|
assertEquals(PI / 2, arc.length, 1e-4)
|
||||||
|
@ -6,10 +6,9 @@
|
|||||||
package space.kscience.trajectory
|
package space.kscience.trajectory
|
||||||
|
|
||||||
import space.kscience.kmath.geometry.Euclidean2DSpace
|
import space.kscience.kmath.geometry.Euclidean2DSpace
|
||||||
import space.kscience.kmath.geometry.equalsFloat
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
|
|
||||||
class DubinsTests {
|
class DubinsTests {
|
||||||
@ -19,10 +18,10 @@ class DubinsTests {
|
|||||||
val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0))
|
val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0))
|
||||||
val lineP1 = straight.shift(1, 10.0).inverse()
|
val lineP1 = straight.shift(1, 10.0).inverse()
|
||||||
|
|
||||||
val start = DubinsPose2D(straight.end, straight.bearing)
|
val start = Pose2D(straight.end, straight.bearing)
|
||||||
val end = DubinsPose2D(lineP1.begin, lineP1.bearing)
|
val end = Pose2D(lineP1.begin, lineP1.bearing)
|
||||||
val radius = 2.0
|
val radius = 2.0
|
||||||
val dubins = DubinsPath.all(start, end, radius)
|
val dubins: List<CompositeTrajectory2D> = DubinsPath.all(start, end, radius)
|
||||||
|
|
||||||
val absoluteDistance = start.distanceTo(end)
|
val absoluteDistance = start.distanceTo(end)
|
||||||
println("Absolute distance: $absoluteDistance")
|
println("Absolute distance: $absoluteDistance")
|
||||||
@ -39,22 +38,22 @@ class DubinsTests {
|
|||||||
val path = dubins.find { p -> DubinsPath.trajectoryTypeOf(p) == it.key }
|
val path = dubins.find { p -> DubinsPath.trajectoryTypeOf(p) == it.key }
|
||||||
assertNotNull(path, "Path ${it.key} not found")
|
assertNotNull(path, "Path ${it.key} not found")
|
||||||
println("${it.key}: ${path.length}")
|
println("${it.key}: ${path.length}")
|
||||||
assertTrue(it.value.equalsFloat(path.length))
|
assertEquals(it.value, path.length, 1e-4)
|
||||||
|
|
||||||
val a = path.segments[0] as CircleTrajectory2D
|
val a = path.segments[0] as CircleTrajectory2D
|
||||||
val b = path.segments[1]
|
val b = path.segments[1]
|
||||||
val c = path.segments[2] as CircleTrajectory2D
|
val c = path.segments[2] as CircleTrajectory2D
|
||||||
|
|
||||||
assertEquals(start, a.begin)
|
assertEquals(start, a.beginPose, 1e-4)
|
||||||
assertEquals(end, c.end)
|
assertEquals(end, c.endPose, 1e-4)
|
||||||
|
|
||||||
// Not working, theta double precision inaccuracy
|
// Not working, theta double precision inaccuracy
|
||||||
if (b is CircleTrajectory2D) {
|
if (b is CircleTrajectory2D) {
|
||||||
assertEquals(a.end, b.begin)
|
assertEquals(a.endPose, b.beginPose, 1e-4)
|
||||||
assertEquals(c.begin, b.end)
|
assertEquals(c.beginPose, b.endPose, 1e-4)
|
||||||
} else if (b is StraightTrajectory2D) {
|
} else if (b is StraightTrajectory2D) {
|
||||||
assertEquals(a.end, DubinsPose2D(b.begin, b.bearing))
|
assertEquals(a.endPose, Pose2D(b.begin, b.bearing), 1e-4)
|
||||||
assertEquals(c.begin, DubinsPose2D(b.end, b.bearing))
|
assertEquals(c.beginPose, Pose2D(b.end, b.bearing),1e-4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,8 @@ class ObstacleTest {
|
|||||||
val finalDirection = vector(1.0, -1.0)
|
val finalDirection = vector(1.0, -1.0)
|
||||||
|
|
||||||
val outputTangents = Obstacles.avoidObstacles(
|
val outputTangents = Obstacles.avoidObstacles(
|
||||||
DubinsPose2D(startPoint, startDirection),
|
Pose2D(startPoint, startDirection),
|
||||||
DubinsPose2D(finalPoint, finalDirection),
|
Pose2D(finalPoint, finalDirection),
|
||||||
startRadius,
|
startRadius,
|
||||||
Obstacle(Circle2D(vector(7.0, 1.0), 5.0))
|
Obstacle(Circle2D(vector(7.0, 1.0), 5.0))
|
||||||
)
|
)
|
||||||
@ -53,8 +53,8 @@ class ObstacleTest {
|
|||||||
val finalDirection = vector(1.0, -1.0)
|
val finalDirection = vector(1.0, -1.0)
|
||||||
|
|
||||||
val paths = Obstacles.avoidObstacles(
|
val paths = Obstacles.avoidObstacles(
|
||||||
DubinsPose2D(startPoint, startDirection),
|
Pose2D(startPoint, startDirection),
|
||||||
DubinsPose2D(finalPoint, finalDirection),
|
Pose2D(finalPoint, finalDirection),
|
||||||
radius,
|
radius,
|
||||||
Obstacle(
|
Obstacle(
|
||||||
Circle2D(vector(1.0, 6.5), 0.5),
|
Circle2D(vector(1.0, 6.5), 0.5),
|
||||||
@ -98,8 +98,8 @@ class ObstacleTest {
|
|||||||
val finalDirection = vector(1.0, 0)
|
val finalDirection = vector(1.0, 0)
|
||||||
|
|
||||||
val paths = Obstacles.avoidObstacles(
|
val paths = Obstacles.avoidObstacles(
|
||||||
DubinsPose2D(startPoint, startDirection),
|
Pose2D(startPoint, startDirection),
|
||||||
DubinsPose2D(finalPoint, finalDirection),
|
Pose2D(finalPoint, finalDirection),
|
||||||
startRadius,
|
startRadius,
|
||||||
Obstacle(
|
Obstacle(
|
||||||
Circle2D(vector(0.0, 0.0), 1.0),
|
Circle2D(vector(0.0, 0.0), 1.0),
|
||||||
@ -117,8 +117,8 @@ class ObstacleTest {
|
|||||||
@Test
|
@Test
|
||||||
fun largeCoordinates() {
|
fun largeCoordinates() {
|
||||||
val paths = Obstacles.avoidObstacles(
|
val paths = Obstacles.avoidObstacles(
|
||||||
DubinsPose2D(x = 484149.535516561, y = 2995086.2534208703, bearing = 3.401475378237137.degrees),
|
Pose2D(x = 484149.535516561, y = 2995086.2534208703, bearing = 3.401475378237137.degrees),
|
||||||
DubinsPose2D(x = 456663.8489126448, y = 2830054.1087567504, bearing = 325.32183928982727.degrees),
|
Pose2D(x = 456663.8489126448, y = 2830054.1087567504, bearing = 325.32183928982727.degrees),
|
||||||
5000.0,
|
5000.0,
|
||||||
Obstacle(
|
Obstacle(
|
||||||
Circle2D(vector(x = 446088.2236175772, y = 2895264.0759535935), radius = 5000.0),
|
Circle2D(vector(x = 446088.2236175772, y = 2895264.0759535935), radius = 5000.0),
|
||||||
|
@ -10,7 +10,7 @@ import space.kscience.kmath.geometry.radians
|
|||||||
import space.kscience.kmath.geometry.sin
|
import space.kscience.kmath.geometry.sin
|
||||||
|
|
||||||
|
|
||||||
fun assertEquals(expected: DubinsPose2D, actual: DubinsPose2D, precision: Double = 1e-6){
|
fun assertEquals(expected: Pose2D, actual: Pose2D, precision: Double = 1e-6){
|
||||||
kotlin.test.assertEquals(expected.x, actual.x, precision)
|
kotlin.test.assertEquals(expected.x, actual.x, precision)
|
||||||
kotlin.test.assertEquals(expected.y, actual.y, precision)
|
kotlin.test.assertEquals(expected.y, actual.y, precision)
|
||||||
kotlin.test.assertEquals(expected.bearing.radians, actual.bearing.radians, precision)
|
kotlin.test.assertEquals(expected.bearing.radians, actual.bearing.radians, precision)
|
||||||
|
Loading…
Reference in New Issue
Block a user