io.rtron.math.processing.Polyhedron3DFactory.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rtron-math Show documentation
Show all versions of rtron-math Show documentation
Component 'rtron-math' of rtron.
The newest version!
/*
* Copyright 2019-2022 Chair of Geoinformatics, Technical University of Munich
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.rtron.math.processing
import arrow.core.Option
import arrow.core.Some
import arrow.core.getOrElse
import arrow.core.none
import com.github.kittinunf.result.Result
import io.rtron.math.geometry.euclidean.threed.point.Vector3D
import io.rtron.math.geometry.euclidean.threed.solid.Polyhedron3D
import io.rtron.math.geometry.euclidean.threed.surface.LinearRing3D
import io.rtron.math.linear.dimensionOfSpan
import io.rtron.math.processing.triangulation.ExperimentalTriangulator
import io.rtron.math.processing.triangulation.Triangulator
import io.rtron.math.range.Tolerable
import io.rtron.std.ContextMessage
import io.rtron.std.filterWindowedEnclosing
import io.rtron.std.filterWithNextEnclosing
import io.rtron.std.getResult
import io.rtron.std.handleFailure
import io.rtron.std.unwrapMessages
import io.rtron.std.zipWithConsecutivesEnclosing
import io.rtron.std.zipWithNextEnclosing
/**
* Factory for building [Polyhedron3D] for which multiple preparation steps are required to overcome
* heterogeneous input.
*/
object Polyhedron3DFactory {
/**
* Builds a [Polyhedron3D] from a list of [VerticalOutlineElement], which define the boundary of the [Polyhedron3D].
*
* @param outlineElements vertical line segments or points bounding the polyhedron
*/
@OptIn(ExperimentalTriangulator::class)
fun buildFromVerticalOutlineElements(outlineElements: List, tolerance: Double):
Result, Exception> {
// prepare vertical outline elements
val preparedOutlineElementsWithContext = prepareOutlineElements(outlineElements, tolerance)
.handleFailure { return it }
val messages = preparedOutlineElementsWithContext.messages
val preparedOutlineElements = preparedOutlineElementsWithContext.value
// construct faces
val baseFace = LinearRing3D(preparedOutlineElements.reversed().map { it.basePoint }, tolerance)
val topFace = LinearRing3D(preparedOutlineElements.flatMap { it.getHighestPointAdjacentToTheTop() }, tolerance)
val sideFaces: List = preparedOutlineElements
.zipWithNextEnclosing()
.flatMap { buildSideFace(it.first, it.second, tolerance).toList() }
// triangulate faces
val triangulatedFaces = (sideFaces + baseFace + topFace)
.map { Triangulator.triangulate(it, tolerance) }
.handleFailure { return it }
.flatten()
val polyhedron = ContextMessage(Polyhedron3D(triangulatedFaces, tolerance), messages)
return Result.success(polyhedron)
}
/**
* A vertical outline element is represented by a [basePoint] and an optional [leftHeadPoint] and [rightHeadPoint].
* The [basePoint] defines the bound of the base surface
*
* @param basePoint base point of the outline element
* @param leftHeadPoint left head point representing the bound point of the side surface to the left
* @param rightHeadPoint right head point representing the bound point of the side surface to the right
*/
data class VerticalOutlineElement(
val basePoint: Vector3D,
val leftHeadPoint: Option,
val rightHeadPoint: Option = none(),
override val tolerance: Double
) : Tolerable {
// Properties and Initializers
init {
if (rightHeadPoint.isDefined())
require(leftHeadPoint.isDefined()) { "Left head point must be present, if right head point is present." }
if (leftHeadPoint.isDefined()) {
val leftHeadPointValue = leftHeadPoint.getResult().handleFailure { throw it.error }
require(basePoint.fuzzyUnequals(leftHeadPointValue, tolerance)) { "Left head point must be fuzzily unequal to base point." }
if (rightHeadPoint.isDefined()) {
val rightHeadPointValue = rightHeadPoint.getResult().handleFailure { throw it.error }
require(basePoint.fuzzyUnequals(rightHeadPointValue, tolerance)) { "Right head point must be fuzzily unequal to base point." }
require(leftHeadPointValue.fuzzyUnequals(rightHeadPointValue, tolerance)) { "Left head point must be fuzzily unequal to the right point." }
}
}
}
private val leftLength: Double get() = leftHeadPoint.map { basePoint.distance(it) }.getOrElse { 0.0 }
private val rightLength: Double get() = rightHeadPoint.map { basePoint.distance(it) }.getOrElse { 0.0 }
// Methods
fun containsHeadPoint() = leftHeadPoint.isDefined() || rightHeadPoint.isDefined()
fun containsOneHeadPoint() = leftHeadPoint.isDefined() && rightHeadPoint.isEmpty()
fun getVerticesAsLeftBoundary(): List {
val midPoint = if (containsOneHeadPoint() || leftLength < rightLength) leftHeadPoint.toList()
else emptyList()
return listOf(basePoint) + midPoint + rightHeadPoint.toList()
}
fun getVerticesAsRightBoundary(): List {
val midPoint = if (leftLength > rightLength) rightHeadPoint.toList() else emptyList()
return listOf(basePoint) + midPoint + leftHeadPoint.toList()
}
fun getHeadPointAdjacentToTheRight() = if (rightHeadPoint.isDefined()) rightHeadPoint else leftHeadPoint
fun getHighestPointAdjacentToTheTop(): List =
if (containsHeadPoint()) leftHeadPoint.toList() + rightHeadPoint.toList()
else listOf(basePoint)
companion object {
fun of(
basePoint: Vector3D,
leftHeadPoint: Option,
rightHeadPoint: Option,
tolerance: Double
): ContextMessage {
val infos = mutableListOf()
val headPoints = leftHeadPoint.toList() + rightHeadPoint.toList()
// remove head points that are fuzzily equal to base point
val prepHeadPoints = headPoints.filter { it.fuzzyUnequals(basePoint, tolerance) }
if (prepHeadPoints.size < headPoints.size)
infos += "Height of outline element must be above tolerance."
if (prepHeadPoints.size <= 1)
return ContextMessage(of(basePoint, prepHeadPoints, tolerance), infos)
// if head points are fuzzily equal, take only one
return if (prepHeadPoints.first().fuzzyEquals(prepHeadPoints.last(), tolerance))
ContextMessage(of(basePoint, prepHeadPoints.take(1), tolerance), infos)
else ContextMessage(of(basePoint, prepHeadPoints, tolerance), infos)
}
/**
* Returns a [VerticalOutlineElement] based on a provided list of [headPoints].
*
* @param basePoint base point of the outline element
* @param headPoints a maximum number of two head points must be provided
* @param tolerance allowed tolerance
*/
fun of(basePoint: Vector3D, headPoints: List, tolerance: Double): VerticalOutlineElement {
require(headPoints.size <= 2) { "Must contain not more than two head points." }
val leftHeadPoint = if (headPoints.isNotEmpty()) Some(headPoints.first()) else none()
val rightHeadPont = if (headPoints.size == 2) Some(headPoints.last()) else none()
return VerticalOutlineElement(basePoint, leftHeadPoint, rightHeadPont, tolerance)
}
/**
* Returns a [VerticalOutlineElement] by merging a list of [elements].
*
* @param elements list of [VerticalOutlineElement] which must all contain the same base point
* @param tolerance allowed tolerance
*/
fun of(elements: List, tolerance: Double): ContextMessage {
require(elements.isNotEmpty()) { "List of elements must not be empty." }
require(elements.drop(1).all { it.basePoint == elements.first().basePoint }) { "All elements must have the same base point." }
if (elements.size == 1)
return ContextMessage(elements.first())
val message = if (elements.size > 2) "Contains more than two consecutively following outline " +
"element duplicates." else ""
val basePoint = elements.first().basePoint
val leftHeadPoint = elements.first().leftHeadPoint
val rightHeadPoint = elements.last().getHeadPointAdjacentToTheRight()
return of(basePoint, leftHeadPoint, rightHeadPoint, tolerance).appendMessages(message)
}
}
}
/**
* Builds a side face, whereas the [leftElement] and [rightElement] represent the boundaries.
* Furthermore, the height of the previous or succeeding side face is taken into account, since it has an effect
* on the vertices of the side face in focus.
*
* @param leftElement left boundary of side surface to be constructed
* @param rightElement right boundary of side surface to be constructed
* @param tolerance allowed tolerance
*/
private fun buildSideFace(
leftElement: VerticalOutlineElement,
rightElement: VerticalOutlineElement,
tolerance: Double
): Option {
if (!leftElement.containsHeadPoint() && !rightElement.containsHeadPoint())
return none()
val vertices = rightElement.getVerticesAsRightBoundary() + leftElement.getVerticesAsLeftBoundary().reversed()
val linearRing = LinearRing3D(vertices, tolerance)
return Some(linearRing)
}
/**
* Preparation and cleanup of [verticalOutlineElements] including the removal of duplicates and error messaging.
*/
private fun prepareOutlineElements(verticalOutlineElements: List, tolerance: Double):
Result>, Exception> {
val infos = mutableListOf()
// remove consecutively following line segment duplicates
val elementsWithoutDuplicates = verticalOutlineElements.filterWithNextEnclosing { a, b -> a.basePoint.fuzzyUnequals(b.basePoint, tolerance) }
if (elementsWithoutDuplicates.size < verticalOutlineElements.size)
infos += "Removing at least one consecutively following line segment duplicate."
// if there are not enough points to construct a polyhedron
if (elementsWithoutDuplicates.size < 3)
return Result.error(IllegalStateException("A polyhedron requires at least three valid outline elements."))
// remove consecutively following side duplicates of the form (…, A, B, A, …)
val cleanedElements = elementsWithoutDuplicates
.filterWindowedEnclosing(listOf(false, true, true)) { it[0].basePoint == it[2].basePoint }
if (cleanedElements.size < elementsWithoutDuplicates.size)
infos += "Removing consecutively following side duplicates of the form (…, A, B, A, …)."
// if the base points of the outline element are located on a line (or point)
val innerBaseEdges = cleanedElements.map { it.basePoint }.filterIndexed { index, _ -> index != 0 }.map { it - cleanedElements.first().basePoint }
val dimensionOfSpan = innerBaseEdges.map { it.toRealVector() }.dimensionOfSpan()
if (dimensionOfSpan < 2)
return Result.error(IllegalStateException("A polyhedron requires at least three valid outline elements, which are not colinear (located on a line)."))
val elements: ContextMessage> = cleanedElements
.zipWithConsecutivesEnclosing { it.basePoint }
.map { VerticalOutlineElement.of(it, tolerance) }
.unwrapMessages()
return Result.success(elements)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy