Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-linear coordinate systems #899

Merged
merged 9 commits into from
Oct 12, 2023
Prev Previous commit
Next Next commit
coord_polar: resampler fix (infinite loop in some cases), refactoring…
…, tests.
  • Loading branch information
IKupriyanov-HORIS committed Oct 5, 2023
commit fe114a2e442782e9d978ecd2a112cb02e1f05695
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,38 @@ fun Int.ipow(e: Int): Double {
return this.toDouble().pow(e)
}

fun areEqual(a: Double, b: Double, epsilon: Double = 0.00001) = abs(a - b) < epsilon
fun areEqual(a: Double, b: Double, epsilon: Double = 0.00001) = abs(a - b) < epsilon

fun pointToLineSqrDistance(x: Double, y: Double, l1x: Double, l1y: Double, l2x: Double, l2y: Double): Double {
return if (l1x == l2x && l1y == l2y) {
pointSqrDistance(x, y, l1x, l1y)
} else {
val ortX = l2x - l1x
val ortY = -(l2y - l1y)

val dot = (x - l1x) * ortY + (y - l1y) * ortX
val len = ortY * ortY + ortX * ortX

dot * dot / len
}
}

fun pointSqrDistance(x1: Double, y1: Double, x2: Double, y2: Double): Double {
val dx = x1 - x2
val dy = y1 - y2
return dx * dx + dy * dy
}

fun yOnLine(p1x: Double, p1y: Double, p2x: Double, p2y: Double, x: Double): Double {
// the Equation for the Line
// y = m * x + b
// Where
// m = (y2 - y1) / (x2 - x1)
// and b computed by substitution p1 or p2 to the equation of the line

val m = (p2y - p1y) / ((p2x) - p1x)
val b = p2y - m * (p2x)

// Result
return m * x + b
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

package org.jetbrains.letsPlot.commons.intern.spatial

import org.jetbrains.letsPlot.commons.intern.typedGeometry.*
import org.jetbrains.letsPlot.commons.intern.math.yOnLine
import org.jetbrains.letsPlot.commons.intern.typedGeometry.*
import kotlin.math.abs

Expand All @@ -30,20 +30,6 @@ fun <TypeT> wrapPath(path: List<Vec<TypeT>>, domain: Rect<TypeT>): List<List<Vec
var currentPath = ArrayList<Vec<TypeT>>()
currentPath.add(path[0])

fun yOnLine(p1: Vec<TypeT>, p2: Vec<TypeT>, x: Double): Double {
// the Equation for the Line
// y = m * x + b
// Where
// m = (y2 - y1) / (x2 - x1)
// and b computed by substitution p1 or p2 to the equation of the line

val m = (p2.y - p1.y) / ((p2.x) - p1.x)
val b = p2.y - m * (p2.x)

// Result
return m * x + b
}

for (i in 1 until path.size) {
val p1 = path[i - 1]
val p2 = path[i]
Expand All @@ -54,11 +40,11 @@ fun <TypeT> wrapPath(path: List<Vec<TypeT>>, domain: Rect<TypeT>): List<List<Vec
val y: Double

if (p1.x < p2.x) {
y = yOnLine(p1, p2.copy(x = p2.x - domain.width), x = domain.left)
y = yOnLine(p1.x, p1.y, p2.x - domain.width, p2.y, x = domain.left)
xa = domain.left
xb = domain.right
} else {
y = yOnLine(p1, p2.copy(x = p2.x + domain.width), x = domain.right)
y = yOnLine(p1.x, p1.y, p2.x + domain.width, p2.y, x = domain.right)
xa = domain.right
xb = domain.left
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

package org.jetbrains.letsPlot.commons.intern.typedGeometry

import org.jetbrains.letsPlot.commons.intern.typedGeometry.VecResampler.Companion.resample

object Transforms {
private const val RESAMPLING_PRECISION = 0.001
private const val RESAMPLING_PRECISION = 0.004

fun <InT, OutT> transform(
bbox: Rect<InT>,
Expand Down Expand Up @@ -44,6 +46,6 @@ object Transforms {
resamplingPrecision: Double?
): List<Vec<OutT>> = when(resamplingPrecision) {
null -> path.mapNotNull(transform)
else -> VecResampler(transform, resamplingPrecision).resample(path)
else -> resample(path, resamplingPrecision, transform)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,35 @@ package org.jetbrains.letsPlot.commons.intern.typedGeometry
import org.jetbrains.letsPlot.commons.intern.typedGeometry.algorithms.AdaptiveResampler


class VecResampler<InT, OutT>(
class VecResampler<InT, OutT> private constructor(
private val transform: (Vec<InT>) -> Vec<OutT>?,
precision: Double
) {
companion object {
private val VEC_ADAPTER = object : AdaptiveResampler.DataAdapter<Vec<*>> {
override fun x(p: Vec<*>): Double = p.x
override fun y(p: Vec<*>): Double = p.y
override fun create(x: Double, y: Double): Vec<*> = Vec<Untyped>(x, y)
}

fun <InT, OutT> resample(
points: List<Vec<InT>>,
precision: Double,
transform: (Vec<InT>) -> Vec<OutT>?
): List<Vec<OutT>> {
return VecResampler(transform, precision).resample(points)
}

fun <InT, OutT> resample(
p1: Vec<InT>,
p2: Vec<InT>,
precision: Double,
transform: (Vec<InT>) -> Vec<OutT>?
): List<Vec<OutT>> {
return VecResampler(transform, precision).resample(p1, p2)
}
}

private val resampler = AdaptiveResampler.generic(this::transformWrapper, precision, VEC_ADAPTER)

private fun transformWrapper(v: Vec<*>): Vec<*>? {
Expand All @@ -28,12 +53,4 @@ class VecResampler<InT, OutT>(
@Suppress("UNCHECKED_CAST")
return resampler.resample(p1, p2) as List<Vec<OutT>>
}

private companion object {
val VEC_ADAPTER = object : AdaptiveResampler.DataAdapter<Vec<*>> {
override fun x(p: Vec<*>): Double = p.x
override fun y(p: Vec<*>): Double = p.y
override fun create(x: Double, y: Double): Vec<*> = Vec<Untyped>(x, y)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,100 +6,113 @@
package org.jetbrains.letsPlot.commons.intern.typedGeometry.algorithms

import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.intern.math.pointSqrDistance
import org.jetbrains.letsPlot.commons.intern.math.pointToLineSqrDistance


class AdaptiveResampler<T> private constructor(
private val transform: (T) -> T?,
precision: Double,
private val dataAdapter: DataAdapter<T>
) {
private val precisionSqr: Double = precision * precision

companion object {
private const val MAX_DEPTH_LIMIT = 9 // 1_025 points maximum (2^(LIMIT + 1) + 1)

private val DOUBLE_VECTOR_ADAPTER = object : DataAdapter<DoubleVector> {
override fun x(p: DoubleVector) = p.x
override fun y(p: DoubleVector) = p.y
override fun create(x: Double, y: Double) = DoubleVector(x, y)
}

fun forDoubleVector(transform: (DoubleVector) -> DoubleVector?, precision: Double): AdaptiveResampler<DoubleVector> {
private fun forDoubleVector(
transform: (DoubleVector) -> DoubleVector?,
precision: Double
): AdaptiveResampler<DoubleVector> {
return AdaptiveResampler(transform, precision, DOUBLE_VECTOR_ADAPTER)
}

fun <T> generic(transform: (T) -> T?, precision: Double, adapter: DataAdapter<T>): AdaptiveResampler<T> {
return AdaptiveResampler(transform, precision, adapter)
}

fun resample(points: List<DoubleVector>, precision: Double, transform: (DoubleVector) -> DoubleVector?): List<DoubleVector> {
fun resample(
points: List<DoubleVector>,
precision: Double,
transform: (DoubleVector) -> DoubleVector?
): List<DoubleVector> {
return forDoubleVector(transform, precision).resample(points)
}

fun resample(p1: DoubleVector, p2: DoubleVector, precision: Double, transform: (DoubleVector) -> DoubleVector?): List<DoubleVector> {
fun resample(
p1: DoubleVector,
p2: DoubleVector,
precision: Double,
transform: (DoubleVector) -> DoubleVector?
): List<DoubleVector> {
return forDoubleVector(transform, precision).resample(p1, p2)
}
}
private val precisionSqr: Double = precision * precision

fun resample(points: List<T>): List<T> {
val result = ArrayList<T>(points.size)

for (i in 1.until(points.size)) {
result.removeLastOrNull()
var pPrev: T = points.firstOrNull() ?: return emptyList()
var tPrev: T = transform(pPrev) ?: return emptyList()

val pLast = points.lastOrNull() ?: return emptyList()
val tLast = transform(pLast) ?: return emptyList()

val output = ArrayList<T>(points.size)
points
.asSequence()
.drop(1)
.forEach { p ->
val t = transform(p) ?: return@forEach
output.add(tPrev)
resample(pPrev, tPrev, p, t, output, MAX_DEPTH_LIMIT)

pPrev = p
tPrev = t
}

resample(points[i - 1], points[i])
.forEach(result::add)
}
output.add(tLast)

return result
return output
}

fun resample(p1: T, p2: T): List<T> {
val result = ArrayList<T>()
val temp = ArrayList<T>()
result.add(p1)
temp.add(p2)

while (temp.isNotEmpty()) {
when (val missingPoint = computeMissingPoint(result.last(), temp.last())) {
null -> result.add(temp.removeLast())
else -> temp.add(missingPoint)
}
}
return result.mapNotNull { transform(it) }
val t1 = transform(p1) ?: return emptyList()
val t2 = transform(p2) ?: return emptyList()

val output = mutableListOf<T>()
output.add(t1)
resample(p1, t1, p2, t2, output, MAX_DEPTH_LIMIT)
output.add(t2)
return output
}

private fun computeMissingPoint(p1: T, p2: T): T? {
val pc = (p1 + p2) / 2.0
val q1 = transform(p1) ?: return null
val q2 = transform(p2) ?: return null
val qc = transform(pc) ?: return null

val distance = if (q1 == q2) {
length(q1, qc)
} else {
distance(qc, q1, q2)
private fun resample(p1: T, t1: T, p2: T, t2: T, output: MutableList<T>, depth: Int) {
if (p1 == p2) {
return
}

return if (distance < precisionSqr) null else pc
}


private fun length(p1: T, p2: T): Double {
val x = p2.x - p1.x
val y = p2.y - p1.y
return x * x + y * y
}

private fun distance(p: T, l1: T, l2: T): Double {
val ortX = l2.x - l1.x
val ortY = -(l2.y - l1.y)

val dot = (p.x - l1.x) * ortY + (p.y - l1.y) * ortX
val len = ortY * ortY + ortX * ortX
val pm = (p1 + p2) / 2.0
val tm = transform(pm) ?: return

return dot * dot / len
if (pointToLineSqrDistance(tm.x, tm.y, t1.x, t1.y, t2.x, t2.y) >= precisionSqr && depth > 0) {
resample(p1, t1, pm, tm, output, depth - 1)
output.add(tm)
resample(pm, tm, p2, t2, output, depth - 1)
} else {
if (pointSqrDistance(t1.x, t1.y, t2.x, t2.y) > precisionSqr) {
output.add(tm)
}
}
}

val T.x get() = dataAdapter.x(this)
val T.y get() = dataAdapter.y(this)
private operator fun T.minus(other: T): T = dataAdapter.create(x - other.x, y - other.y)
private operator fun T.plus(other: T): T = dataAdapter.create(x + other.x, y + other.y)
private operator fun T.div(other: T): T = dataAdapter.create(x / other.x, y / other.y)
private operator fun T.div(v: Double): T = dataAdapter.create(x / v, y / v)
Expand Down
Loading