it.unibo.alchemist.model.geometry.AbstractConvexPolygon.kt Maven / Gradle / Ivy
/*
* Copyright (C) 2010-2023, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/
package it.unibo.alchemist.model.geometry
import it.unibo.alchemist.model.geometry.util.AwtShapes.vertices
import it.unibo.alchemist.model.positions.Euclidean2DPosition
import kotlin.math.min
import java.awt.Shape as AwtShape
/**
* An abstract [ConvexPolygon] providing a convexity test.
*/
abstract class AbstractConvexPolygon : ConvexPolygon {
private companion object {
/**
* @returns the sum of the distances between this segment's endpoints and [other].
*/
private fun > Segment2D.cumulativeDistanceTo(other: Segment2D): Double =
other.distanceTo(first) + other.distanceTo(second)
/**
* @returns the minimum cumulative distance between this segment and [other] (this segment's
* [cumulativeDistanceTo] [other] maybe different from [other]'s [cumulativeDistanceTo] this segment).
*/
private fun > Segment2D.minCumulativeDistanceTo(other: Segment2D): Double =
min(cumulativeDistanceTo(other), other.cumulativeDistanceTo(this))
}
override fun liesOnBoundary(vector: Euclidean2DPosition): Boolean = edges().any { it.contains(vector) }
override fun containsBoundaryIncluded(vector: Euclidean2DPosition): Boolean =
contains(vector) || liesOnBoundary(vector)
override fun containsBoundaryExcluded(vector: Euclidean2DPosition): Boolean =
contains(vector) && !liesOnBoundary(vector)
override fun contains(shape: AwtShape): Boolean = shape.vertices().all { containsBoundaryIncluded(it) }
/*
* It's important that intersects(Shape) does not consider adjacent shapes as intersecting.
*/
override fun isAdjacentTo(other: ConvexPolygon): Boolean =
!intersects(other.asAwtShape()) &&
(other.vertices().any { liesOnBoundary(it) } || vertices().any { other.liesOnBoundary(it) })
override fun closestEdgeTo(segment: Segment2D): Segment2D =
requireNotNull(
edges().minWithOrNull(compareBy({ it.distanceTo(segment) }, { it.minCumulativeDistanceTo(segment) })),
) { "no edge found" }
override fun intersects(segment: Segment2D): Boolean {
if (containsBoundaryExcluded(segment.first) || containsBoundaryExcluded(segment.second)) {
return true
}
val intersections =
edges()
.map { it.intersect(segment) } // Either InfinitePoints, SinglePoint, or None
.filterNot { it is Intersection2D.None }
.asSequence()
// Lazily evaluated
val intersectionPoints =
intersections
.filterIsInstance>()
.map { it.point }
.distinct()
return intersections.none { it is Intersection2D.InfinitePoints } && intersectionPoints.count() > 1
}
override fun toString(): String = javaClass.simpleName + vertices()
/**
* Finds the previous index with the respect to the given [index], restarting from the end if necessary.
*/
protected fun circularPrevious(index: Int): Int = vertices().size.let { (index - 1 + it) % it }
/**
* Finds the next index with respect to the given [index], restarting from the beginning if necessary.
*/
protected fun circularNext(index: Int): Int = (index + 1) % vertices().size
/**
* Checks if the polygon is convex (see [ConvexPolygon]).
* In order to be convex, a polygon must first be simple (not self-intersecting).
* Ascertained that the polygon is simple, a rather easy convexity test consists
* in checking that every edge turns in the same direction (either left or right)
* with respect to the previous one. If they all turn in the same direction, then
* the polygon is convex. That is the definition of convexity of a polygon's boundary
* in this context.
*/
protected fun isConvex() = !isSelfIntersecting() && isBoundaryConvex()
/**
* Checks if the polygon is convex, assuming that every edge apart from the specified
* ones does not cause self-intersection.
*/
protected fun isConvex(vararg modifiedEdges: Int) =
isBoundaryConvex() && modifiedEdges.none { causeSelfIntersection(it) }
/**
* Checks if the polygon's boundary is convex. See [isConvex].
*/
private fun isBoundaryConvex(): Boolean {
if (edges().count { !it.isDegenerate } < 3) {
return false
}
var e1 = getEdge(vertices().size - 1)
var sense: Boolean? = null
return edges().none { e2 ->
val z = Vector2D.zCross(e1.toVector, e2.toVector)
var lostConvexity = false
/*
* Cross product is 0 in the following cases:
* - one (or both) of the two edges is degenerate, so it's perfectly
* fine to skip it as it doesn't affect convexity.
* - the two edges are linearly dependent, i.e. either they have
* the same direction or opposite ones. In the former case it's
* fine to ignore the edge since it can't violate convexity,
* whereas the latter case means edges are overlapping (since they
* have opposite directions and are consecutive), which will be
* detected by a self-intersection test.
*/
if (z != 0.0) {
if (sense == null) {
sense = z > 0.0
} else if (sense != z > 0.0) {
lostConvexity = true
}
e1 = e2
}
lostConvexity
}
}
/**
* Checks whether the polygon is self-intersecting. In this context,
* a polygon is considered non self-intersecting if the following holds
* for every edge e:
* - e must share ONLY its endpoints with its neighboring edges,
* no other point shall be in common with those edges.
* - e should not have any point in common with any other edge.
* Degenerate edges are not considered as they cannot cause self-intersection.
*
* This method has a time complexity of O(n^2). Consider using a hash
* data structure with spatial-related buckets in the future.
*/
private fun isSelfIntersecting() = vertices().indices.any { causeSelfIntersection(it) }
/**
* Checks whether an edge of the polygon cause the latter to be self-intersecting.
* See [isSelfIntersecting].
*/
private fun causeSelfIntersection(index: Int): Boolean {
val curr = getEdge(index)
if (curr.isDegenerate) {
return false
}
/*
* First previous edge not degenerate
*/
var i = circularPrevious(index)
while (getEdge(i).isDegenerate) {
i = circularPrevious(i)
}
val prevIndex = i
val prev = getEdge(i)
/*
* First next edge not degenerate
*/
i = circularNext(index)
while (getEdge(i).isDegenerate) {
i = circularNext(i)
}
val next = getEdge(i)
return prev.intersect(curr) !is Intersection2D.SinglePoint ||
curr.intersect(next) !is Intersection2D.SinglePoint ||
/*
* We check every edge between the first prev not
* degenerate and the first next not degenerate.
*/
generateSequence(circularNext(i)) { circularNext(it) }
.takeWhile { it != prevIndex }
.map { getEdge(it) }
.filter { !it.isDegenerate }
.any { curr.intersect(it) !is Intersection2D.None }
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy