
commonMain.me.piruin.geok.Intersection.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2021 Piruin Panichphol
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package me.piruin.geok
import kotlin.math.abs
import kotlin.math.min
const val EPISILON = 0.00005
/**
* touches the other, they do intersect.
* @see How to check if two line segments intersect
* @return value of the cross product
*/
fun Pair.crossProduct(): Double {
return first.x * second.y - second.x * first.y
}
/**
* Check if bounding boxes do intersect. If one bounding box
* touches the other, they do intersect.
* @see How to check if two line segments intersect
* @return true
if they intersect,
* false
otherwise.
*/
infix fun Pair.bboxIntersectWith(other: Pair): Boolean {
return BBox.from(this).intersectWith(BBox.from(other))
}
/**
* Checks if a Point is on a given line
* @see How to check if two line segments intersect
* @return true
if point is on line,
* otherwise false
*/
infix fun LatLng.ifOn(line: Pair): Boolean {
// Move the image, so that line.first is on (0|0)
val tmpLine = LatLng(0 to 0) to LatLng(line.second.x - line.first.x to line.second.y - line.first.y)
val tmpPoint = LatLng(this.x - line.first.x, this.y - line.first.y)
val r = (tmpLine.second to tmpPoint).crossProduct()
return abs(r) < EPISILON
}
/**
* Checks if a point is right of a line. If the point is on the
* line, it is not right of the line.
* @see How to check if two line segments intersect
* @return true
if the point is right of the line,
* false
otherwise
*/
infix fun LatLng.isRightOf(line: Pair): Boolean {
// Move the image, so that line.first is on (0|0)
val tmpLine = LatLng(0 to 0) to LatLng(line.second.x - line.first.x to line.second.y - line.first.y)
val tmpPoint = LatLng(this.x - line.first.x, this.y - line.first.y)
return (tmpLine.second to tmpPoint).crossProduct() < 0.0
}
/**
* Check if line segment first touches or crosses the line that is
* defined by line segment second.
* @see How to check if two line segments intersect
* @return true
if line segment first touches or crosses line second,
* false
otherwise.
*/
infix fun Pair.segmentTouchesOrCrosses(other: Pair): Boolean {
val thisLine = this
return other.first ifOn thisLine ||
other.second ifOn thisLine ||
(other.first isRightOf thisLine xor other.second.isRightOf(this))
}
/**
* Check if line segments intersect
* @return true
if lines do intersect,
* false
otherwise
*/
infix fun Pair.isIntersectWith(other: Pair): Boolean {
return this bboxIntersectWith other &&
this segmentTouchesOrCrosses other &&
other segmentTouchesOrCrosses this
}
/**
* Get where that given 2 lines will be intersect with each other if both line have infinite length.
* Use segmentIntersectionWith
for get intersection of finite line.
*
* @see segmentIntersectionWith
* @return the intersection point of 2 line, it might be a line or a single point.
* If it is a line, then x1 = x2 and y1 = y2.
*/
infix fun Pair.intersectionWith(other: Pair): Pair {
var a = this
var b = other
val x1: Double
val y1: Double
val x2: Double
val y2: Double
if (a.first.x == a.second.x) {
// Case (A)
// As a is a perfect vertical line, it cannot be represented
// nicely in a mathematical way. But we directly know that
//
x1 = a.first.x
x2 = x1
if (b.first.x == b.second.x) {
// Case (AA): all x are the same!
// Normalize
if (a.first.y > a.second.y) {
a = a.swap()
}
if (b.first.y > b.second.y) {
b = b.swap()
}
if (a.first.y > b.first.y) {
a = b.also { b = a }
}
// Now we know that the y-value of a.first is the
// lowest of all 4 y values
// this means, we are either in case (AAA):
// a: x--------------x
// b: x---------------x
// or in case (AAB)
// a: x--------------x
// b: x-------x
// in both cases:
// get the relavant y intervall
y1 = b.first.y
y2 = min(a.second.y, b.second.y)
} else {
// Case (AB)
// we can mathematically represent line b as
// y = m*x + t <=> t = y - m*x
// m = (y1-y2)/(x1-x2)
val m = (b.first.y - b.second.y) /
(b.first.x - b.second.x)
val t = b.first.y - m * b.first.x
y1 = m * x1 + t
y2 = y1
}
} else if (b.first.x == b.second.x) {
// Case (B)
// essentially the same as Case (AB), but with
// a and b switched
x1 = b.first.x
x2 = x1
val tmp = a
a = b
b = tmp
val m = (b.first.y - b.second.y) /
(b.first.x - b.second.x)
val t = b.first.y - m * b.first.x
y1 = m * x1 + t
y2 = y1
} else {
// Case (C)
// Both lines can be represented mathematically
val ma = (a.first.y - a.second.y) /
(a.first.x - a.second.x)
val mb = (b.first.y - b.second.y) /
(b.first.x - b.second.x)
val ta = a.first.y - ma * a.first.x
val tb = b.first.y - mb * b.first.x
if (ma == mb) {
// Case (CA)
// both lines are in parallel. As we know that they
// intersect, the intersection could be a line
// when we rotated this, it would be the same situation
// as in case (AA)
// Normalize
if (a.first.x > a.second.x) {
a.swap()
}
if (b.first.x > b.second.x) {
b.swap()
}
if (a.first.x > b.first.x) {
a = b.also { b = a }
}
// get the relavant x intervall
x1 = b.first.x
x2 = min(a.second.x, b.second.x)
y1 = ma * x1 + ta
y2 = ma * x2 + ta
} else {
// Case (CB): only a point as intersection:
// y = ma*x+ta
// y = mb*x+tb
// ma*x + ta = mb*x + tb
// (ma-mb)*x = tb - ta
// x = (tb - ta)/(ma-mb)
x1 = (tb - ta) / (ma - mb)
y1 = ma * x1 + ta
x2 = x1
y2 = y1
}
}
return LatLng(x1 to y1) to LatLng(x2 to y2)
}
val Pair.isVerticalLine: Boolean get() = first.x == second.x
val Pair.isHorizontalLine: Boolean get() = first.y == second.y
/**
* @return the intersection point of 2 line, it might be a line or a single point.
* If it is a line, then x1 = x2 and y1 = y2.
*/
infix fun Pair.segmentIntersectionWith(other: Pair): Pair? {
if (this.isHorizontalLine || this.isVerticalLine ||
other.isHorizontalLine || other.isVerticalLine
) {
// case of line of perfect vertical & perfect horizontal
val result = this.intersectionWith(other)
if (result.bboxIntersectWith(this) && result.bboxIntersectWith(other))
return result
}
if (!this.isIntersectWith(other))
return null
val result = intersectionWith(other)
// Make sure result is on both lines, this require for some case!!
if (result.bboxIntersectWith(this) && result.bboxIntersectWith(other))
return result
return null
}
/**
* @see Intersection Convex Polygons Algorithm
* @return List of intersection points of this Line and Polygon
*/
infix fun Pair.intersectionsWith(polygon: Collection): List {
val result = mutableListOf()
polygon.forEachLine {
this.segmentIntersectionWith(it)?.let { point -> result.addUnique(point.toList()) }
}
return result
}
/**
* @see Intersection Convex Polygons Algorithm
* @return Intersection polygon of this and another polygon
*/
infix fun Collection.intersectionsWith(other: Collection): List {
val polygon1 = this.close()
val polygon2 = other.close()
val clippedPoint = mutableListOf()
polygon1.forEach { if (it insideOf polygon2) clippedPoint.addUnique(it) }
polygon2.forEach { if (it insideOf polygon1) clippedPoint.addUnique(it) }
polygon1.forEachLine { line -> clippedPoint.addUnique(line intersectionsWith polygon2) }
// Require for some edge case, please don't worry about performance (dear Myself).
polygon2.forEachLine { line -> clippedPoint.addUnique(line intersectionsWith polygon1) }
if (clippedPoint.isEmpty())
return clippedPoint
return clippedPoint.sortedClockwise()
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy