/*-
* 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 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.CheckType;
import de.hft.stuttgart.citydoctor2.check.Checkable;
import de.hft.stuttgart.citydoctor2.check.DefaultParameter;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.Unit;
import de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonDistancePlaneError;
import de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonNormalsDeviation;
import de.hft.stuttgart.citydoctor2.check.error.TinyEdgeError;
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.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
*
* @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 TINY_EDGE_TOLERANCE = "tinyEdgeTolerance";
private static final List dependencies;
private static final List> applicableToClasses;
private static final List defaultParameters;
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);
ArrayList> classes = new ArrayList<>(1);
classes.add(Polygon.class);
applicableToClasses = Collections.unmodifiableList(classes);
ArrayList defParameters = new ArrayList<>(3);
defParameters.add(new DefaultParameter(TYPE, DISTANCE, Unit.NONE));
defParameters.add(new DefaultParameter(DISTANCE_TOLERANCE, "0.01", Unit.METER));
defParameters.add(new DefaultParameter(ANGLE_TOLERANCE, "1", Unit.DEGREE));
defParameters.add(new DefaultParameter(TINY_EDGE_TOLERANCE, "0.00000", Unit.METER));
defaultParameters = Collections.unmodifiableList(defParameters);
}
private String planarCheckType = DISTANCE;
private double rad = Math.toRadians(1);
private double delta = 0.01;
private double tinyEdgeTolerance = 0.00000;
public PlanarCheck() {
super(CheckId.C_GE_P_NON_PLANAR);
}
@Override
public void init(Map parameters, ParserConfiguration config) {
if (parameters.containsKey(TYPE)) {
planarCheckType = parameters.get(TYPE).toLowerCase();
} else {
planarCheckType = DISTANCE;
}
if (parameters.containsKey(ANGLE_TOLERANCE)) {
rad = Math.toRadians(Double.parseDouble(parameters.get(ANGLE_TOLERANCE)));
} else {
rad = Math.toRadians(1);
}
if (parameters.containsKey(DISTANCE_TOLERANCE)) {
delta = Double.parseDouble(parameters.get(DISTANCE_TOLERANCE));
} else {
delta = 0.01;
}
if (parameters.containsKey(TINY_EDGE_TOLERANCE)) {
tinyEdgeTolerance = Double.parseDouble(parameters.get(TINY_EDGE_TOLERANCE));
} else {
tinyEdgeTolerance = 0.00002;
}
}
@Override
public List getDefaultParameter() {
return defaultParameters;
}
@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);
Vector3d eigenvalues = OrthogonalRegressionPlane.getEigenvalues(ed);
if (checkEigenvalues(p, eigenvalues)) {
// 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);
Vector3d eigenvalues = OrthogonalRegressionPlane.getEigenvalues(ed);
if (checkEigenvalues(p, eigenvalues)) {
// found tiny edge error, abort further checking
return;
}
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, Vector3d eigenvalues) {
int nrOfEigenvaluesBelowTolerance = 0;
for (double d : eigenvalues.getCoordinates()) {
if (d <= tinyEdgeTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
}
if (nrOfEigenvaluesBelowTolerance >= 2) {
CheckError err = new TinyEdgeError(p);
p.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
return true;
}
return false;
}
@Override
public List> getApplicableToClasses() {
return applicableToClasses;
}
@Override
public List getDependencies() {
return dependencies;
}
@Override
public CheckType getType() {
return CheckType.GEOMETRY;
}
@Override
public Check createNewInstance() {
return new PlanarCheck();
}
}