/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see .
*/
package de.hft.stuttgart.citydoctor2.checks.geometry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import Jama.EigenvalueDecomposition;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.DegeneratedPolygonError;
import de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonDistancePlaneError;
import de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonNormalsDeviation;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.BoundingBox;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.math.CovarianceMatrix;
import de.hft.stuttgart.citydoctor2.math.Matrix3x3d;
import de.hft.stuttgart.citydoctor2.math.OrthogonalRegressionPlane;
import de.hft.stuttgart.citydoctor2.math.Plane;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.tesselation.JoglTesselator;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;
/**
* Check class to check for planarity issues as well as degenerated polygons.
* Checks for regression plane and normal issues.
*
* @author Matthias Betz
*
*/
public class PlanarCheck extends Check {
private static final String DISTANCE = "distance";
private static final String DISTANCE_TOLERANCE = "distanceTolerance";
private static final String ANGLE_TOLERANCE = "angleTolerance";
private static final String TYPE = "type";
private static final String DEGENERATED_POLYGON_TOLERANCE = "degeneratedPolygonTolerance";
private static final List dependencies;
static {
ArrayList deps = new ArrayList<>(4);
deps.add(CheckId.C_GE_R_TOO_FEW_POINTS);
deps.add(CheckId.C_GE_R_NOT_CLOSED);
deps.add(CheckId.C_GE_R_DUPLICATE_POINT);
deps.add(CheckId.C_GE_R_SELF_INTERSECTION);
dependencies = Collections.unmodifiableList(deps);
}
private String planarCheckType = DISTANCE;
private double rad = Math.toRadians(1);
private double delta = 0.01;
private double degeneratedPolygonTolerance = 0.00000;
@Override
public void init(Map parameters, ParserConfiguration config) {
if (parameters.containsKey(TYPE)) {
planarCheckType = parameters.get(TYPE).toLowerCase();
} else {
throw new IllegalStateException("Parameter " + TYPE + " is missing from parameters");
}
if (parameters.containsKey(ANGLE_TOLERANCE)) {
rad = Math.toRadians(Double.parseDouble(parameters.get(ANGLE_TOLERANCE)));
}
if (parameters.containsKey(DISTANCE_TOLERANCE)) {
delta = Double.parseDouble(parameters.get(DISTANCE_TOLERANCE));
}
if (parameters.containsKey(DEGENERATED_POLYGON_TOLERANCE)) {
degeneratedPolygonTolerance = Double.parseDouble(parameters.get(DEGENERATED_POLYGON_TOLERANCE));
}
}
@Override
public void check(Polygon p) {
if (DISTANCE.equals(planarCheckType)) {
planarDistance(p);
} else if ("angle".equals(planarCheckType)) {
// check for tiny edge as well
// store all used points in temporary list
ArrayList vertices = collectVertices(p);
Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
if (checkEigenvalues(p, vertices, ed)) {
// found tiny edge error, abort further checking
return;
}
planarNormalDeviation(p);
} else if ("both".equals(planarCheckType)) {
planarDistance(p);
planarNormalDeviation(p);
} else {
throw new IllegalStateException("Illegal planar check type was given: " + planarCheckType
+ "\nChoose one of: distance, angle, both");
}
}
private void planarNormalDeviation(Polygon p) {
TesselatedPolygon tp = JoglTesselator.tesselatePolygon(p);
ArrayList normals = new ArrayList<>();
for (Triangle3d t : tp.getTriangles()) {
Vector3d normal = t.getNormal();
normals.add(normal);
}
Vector3d averageNormal = calculateAverageNormal(normals);
averageNormal.normalize();
for (Vector3d normal : normals) {
normal.normalize();
double deviation = normal.dot(averageNormal);
double radiant = Math.acos(deviation);
if (radiant > rad) {
CheckError err = new NonPlanarPolygonNormalsDeviation(p, radiant);
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
p.addCheckResult(cr);
return;
}
}
CheckResult cr = p.getCheckResult(this.getCheckId());
// only change check result if missing, otherwise might override error from
// distance check
if (cr == null) {
p.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
}
}
private Vector3d calculateAverageNormal(ArrayList normals) {
double x = 0D;
double y = 0D;
double z = 0D;
for (Vector3d normal : normals) {
x += normal.getX();
y += normal.getY();
z += normal.getZ();
}
x = x / normals.size();
y = y / normals.size();
z = z / normals.size();
return new Vector3d(x, y, z);
}
private void planarDistance(Polygon p) {
// store all used points in temporary list
ArrayList vertices = collectVertices(p);
Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
if (checkEigenvalues(p, vertices, ed)) {
// found tiny edge error, abort further checking
return;
}
Vector3d eigenvalues = OrthogonalRegressionPlane.getEigenvalues(ed);
Plane plane = OrthogonalRegressionPlane.calculatePlane(centroid, ed, eigenvalues);
for (Vertex v : vertices) {
double distance = plane.getDistance(v);
if (distance > delta) {
CheckError err = new NonPlanarPolygonDistancePlaneError(p, distance, v, plane);
p.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
return;
}
}
CheckResult cr = p.getCheckResult(this.getCheckId());
// only change check result if missing
if (cr == null) {
p.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
}
}
private ArrayList collectVertices(Polygon p) {
ArrayList vertices = new ArrayList<>();
// only go to n - 1 points, because last point = first point
for (int i = 0; i < p.getExteriorRing().getVertices().size() - 1; i++) {
Vertex v = p.getExteriorRing().getVertices().get(i);
vertices.add(v);
}
for (LinearRing lr : p.getInnerRings()) {
for (int i = 0; i < lr.getVertices().size() - 1; i++) {
Vertex v = lr.getVertices().get(i);
vertices.add(v);
}
}
return vertices;
}
private boolean checkEigenvalues(Polygon p, List points, EigenvalueDecomposition ed) {
Matrix3x3d mat = new Matrix3x3d(ed.getV().getArray());
List rotatedVertices = new ArrayList<>();
for (Vertex v : points) {
rotatedVertices.add(mat.mult(v));
}
BoundingBox bbox = BoundingBox.ofPoints(rotatedVertices);
int nrOfEigenvaluesBelowTolerance = 0;
if (bbox.getWidth() < degeneratedPolygonTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (bbox.getHeight() < degeneratedPolygonTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (bbox.getDepth() < degeneratedPolygonTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (nrOfEigenvaluesBelowTolerance >= 2) {
CheckError err = new DegeneratedPolygonError(p);
p.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
return true;
}
return false;
}
@Override
public List getDependencies() {
return dependencies;
}
@Override
public Set appliesToRequirements() {
return CollectionUtils.singletonSet(Requirement.R_GE_P_NON_PLANAR);
}
@Override
public RequirementType getType() {
return RequirementType.GEOMETRY;
}
@Override
public Check createNewInstance() {
return new PlanarCheck();
}
@Override
public CheckId getCheckId() {
return CheckId.C_GE_P_NON_PLANAR;
}
}