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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ package org.jetbrains.letsPlot.commons.geometry
import kotlin.math.*

class DoubleVector(val x: Double, val y: Double) {
operator fun component1(): Double = x
operator fun component2(): Double = y

val isFinite: Boolean get() = x.isFinite() && y.isFinite()

fun add(v: DoubleVector): DoubleVector {
Expand Down
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,92 +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)
}
}
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()

resample(points[i - 1], points[i])
.forEach(result::add)
fun resample(
points: List<DoubleVector>,
precision: Double,
transform: (DoubleVector) -> DoubleVector?
): List<DoubleVector> {
return forDoubleVector(transform, precision).resample(points)
}

return result
}

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)
}
fun resample(
p1: DoubleVector,
p2: DoubleVector,
precision: Double,
transform: (DoubleVector) -> DoubleVector?
): List<DoubleVector> {
return forDoubleVector(transform, precision).resample(p1, p2)
}
return result.mapNotNull { transform(it) }
}

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
fun resample(points: List<T>): List<T> {
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
}

val distance = if (q1 == q2) {
length(q1, qc)
} else {
distance(qc, q1, q2)
}
output.add(tLast)

return if (distance < precisionSqr) null else pc
return output
}


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
fun resample(p1: T, p2: T): List<T> {
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 distance(p: T, l1: T, l2: T): Double {
val ortX = l2.x - l1.x
val ortY = -(l2.y - l1.y)
private fun resample(p1: T, t1: T, p2: T, t2: T, output: MutableList<T>, depth: Int) {
if (p1 == p2) {
return
}

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