commonMain.jetbrains.datalore.base.spatial.GeoBoundingBoxCalculator.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lets-plot-common Show documentation
Show all versions of lets-plot-common Show documentation
Lets-Plot JVM package without rendering part
/*
* Copyright (c) 2020. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/
package jetbrains.datalore.base.spatial
import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.spatial.LongitudeSegment.Companion.splitSegment
import jetbrains.datalore.base.typedGeometry.*
import kotlin.math.max
import kotlin.math.min
// Segment have direction, i.e. `start` can be less than `end` for the case
// of the antimeridian intersection.
// That's why we can't use ClosedRange class with lower <= upper invariant
typealias Segment = Pair
val Segment.start get() = first
val Segment.end get() = second
class GeoBoundingBoxCalculator(
private val myMapRect: Rect,
private val myLoopX: Boolean,
private val myLoopY: Boolean
) {
fun calculateBoundingBox(xSegments: Sequence, ySegments: Sequence): Rect {
val xRange = calculateBoundingRange(xSegments, myMapRect.xRange(), myLoopX)
val yRange = calculateBoundingRange(ySegments, myMapRect.yRange(), myLoopY)
return Rect(xRange.lowerEnd, yRange.lowerEnd, xRange.length(), yRange.length())
}
private fun calculateBoundingRange(
segments: Sequence,
mapRange: ClosedRange,
loop: Boolean
): ClosedRange {
return if (loop) {
calculateLoopLimitRange(segments, mapRange)
} else {
ClosedRange(
segments.map(Segment::start).minOrNull()!!,
segments.map(Segment::end).maxOrNull()!!
)
}
}
companion object {
internal fun calculateLoopLimitRange(segments: Sequence, mapRange: ClosedRange): ClosedRange {
return segments
.map { splitSegment(it.start, it.end, mapRange.lowerEnd, mapRange.upperEnd) }
.flatten()
.run { findMaxGapBetweenRanges(this, mapRange.length()) }
.run { invertRange(this, mapRange.length()) }
.run { normalizeCenter(this, mapRange) }
}
private fun normalizeCenter(range: ClosedRange, mapRange: ClosedRange): ClosedRange {
return if (mapRange.contains((range.upperEnd + range.lowerEnd) / 2)) {
range
} else {
ClosedRange(
range.lowerEnd - mapRange.length(),
range.upperEnd - mapRange.length()
)
}
}
private fun findMaxGapBetweenRanges(ranges: Sequence>, width: Double): ClosedRange {
val sortedRanges = ranges.sortedBy(ClosedRange::lowerEnd)
var prevUpper = sortedRanges.maxByOrNull(ClosedRange::upperEnd)!!.upperEnd
var nextLower = sortedRanges.first().lowerEnd
val gapRight = max(width + nextLower, prevUpper)
var maxGapRange = ClosedRange(prevUpper, gapRight)
val it = sortedRanges.iterator()
prevUpper = it.next().upperEnd
while (it.hasNext()) {
val range = it.next()
nextLower = range.lowerEnd
if (nextLower > prevUpper && nextLower - prevUpper > maxGapRange.length()) {
maxGapRange = ClosedRange(prevUpper, nextLower)
}
prevUpper = max(prevUpper, range.upperEnd)
}
return maxGapRange
}
private fun invertRange(range: ClosedRange, width: Double): ClosedRange {
// Fix for rounding error for invertRange introduced by math with width.
fun safeRange(first: Double, second: Double) = ClosedRange(min(first, second), max(first, second))
return when {
range.length() > width -> ClosedRange(range.lowerEnd, range.lowerEnd)
range.upperEnd > width -> safeRange(range.upperEnd - width, range.lowerEnd)
else -> safeRange(range.upperEnd, width + range.lowerEnd)
}
}
private fun ClosedRange.length(): Double = upperEnd - lowerEnd
}
}
fun makeSegments(start: (Int) -> Double, end: (Int) -> Double, size: Int): Sequence {
return (0 until size).asSequence().map { Segment(start(it), end(it)) }
}
fun GeoBoundingBoxCalculator.geoRectsBBox(rectangles: List): Rect {
return calculateBoundingBox(
makeSegments(
{ rectangles[it].startLongitude() },
{ rectangles[it].endLongitude() },
rectangles.size
),
makeSegments(
{ rectangles[it].minLatitude() },
{ rectangles[it].maxLatitude() },
rectangles.size
)
)
}
fun GeoBoundingBoxCalculator.pointsBBox(xyCoords: List): Rect {
require(xyCoords.size % 2 == 0) { "Longitude-Latitude list is not even-numbered." }
val x: (Int) -> Double = { index -> xyCoords[2 * index] }
val y: (Int) -> Double = { index -> xyCoords[2 * index + 1] }
val i = xyCoords.size / 2
return calculateBoundingBox(
makeSegments(x, x, i),
makeSegments(y, y, i)
)
}
fun GeoBoundingBoxCalculator.union(rectangles: List>): Rect {
return calculateBoundingBox(
makeSegments(
{ rectangles[it].left },
{ rectangles[it].right },
rectangles.size
),
makeSegments(
{ rectangles[it].top },
{ rectangles[it].bottom },
rectangles.size
)
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy