/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.qa; import java.util.*; import com.vividsolutions.jts.algorithm.RobustCGAlgorithms; import com.vividsolutions.jts.geom.*; import com.vividsolutions.jts.operation.valid.*; import com.vividsolutions.jump.I18N; import com.vividsolutions.jump.feature.Feature; import com.vividsolutions.jump.geom.Angle; import com.vividsolutions.jump.geom.CoordUtil; import com.vividsolutions.jump.task.TaskMonitor; import com.vividsolutions.jump.util.CoordinateArrays; /** * Performs basic JTS validation, and additional validation like checking polygon * orientation. */ public class Validator { private int validatedFeatureCount; private boolean checkingBasicTopology = true; private boolean checkingPolygonOrientation = false; private boolean checkingLineStringsSimple = false; private boolean checkingMinSegmentLength = false; private boolean checkingMinAngle = false; private boolean checkingMinPolygonArea = false; private boolean checkingNoRepeatedConsecutivePoints = false; private boolean checkingNoHoles = false; private double minSegmentLength = 0; private double minAngle = 0; private double minPolygonArea = 0; private Collection disallowedGeometryClassNames = new ArrayList(); private RepeatedPointTester repeatedPointTester = new RepeatedPointTester(); private RobustCGAlgorithms cgAlgorithms = new RobustCGAlgorithms(); //<<TODO:REFACTORING>> Move this class and associated classes to JTS [Jon Aquino] public Validator() { } /** * Sets whether basic JTS validation should be performed * @param checkingBasicTopology whether basic JTS validation should be performed */ public void setCheckingBasicTopology(boolean checkingBasicTopology) { this.checkingBasicTopology = checkingBasicTopology; } /** * Sets whether consecutive points are not allowed to be the same * @param checkingNoRepeatedConsecutivePoints whether consecutive points are * not allowed to be the same */ public void setCheckingNoRepeatedConsecutivePoints( boolean checkingNoRepeatedConsecutivePoints) { this.checkingNoRepeatedConsecutivePoints = checkingNoRepeatedConsecutivePoints; } /** * Sets whether polygons are not allowed to have holes * @param checkingNoHoles whether polygons are not allowed to have holes */ public void setCheckingNoHoles(boolean checkingNoHoles) { this.checkingNoHoles = checkingNoHoles; } /** * Sets whether polygon orientation should be checked * @param checkingPolygonOrientation whether to enforce the constraint that * polygon shells should be oriented clockwise and holes should be oriented * counterclockwise */ public void setCheckingPolygonOrientation( boolean checkingPolygonOrientation) { this.checkingPolygonOrientation = checkingPolygonOrientation; } /** * Sets the segment length below which the minimum-segment-length check * will raise a validation error. * @param minSegmentLength the threshold used by the minimum-segment-length * check * @see #setCheckingMinSegmentLength(boolean) */ public void setMinSegmentLength(double minSegmentLength) { this.minSegmentLength = minSegmentLength; } /** * Sets the angle below which the minimum-angle check * will raise a validation error. * @param minAngle the threshold used by the minimum-angle check, in degrees * @see #setCheckingMinAngle(boolean) */ public void setMinAngle(double minAngle) { this.minAngle = minAngle; } /** * Sets the area below which the minimum-polygon-area check will raise a * validation error. * @param minPolygonArea the threshould used by the minimum-polygon-area check * @see #setCheckingMinPolygonArea(boolean) */ public void setMinPolygonArea(double minPolygonArea) { this.minPolygonArea = minPolygonArea; } /** * Sets whether to enforce the constraint that LineStrings must be simple * @param checkingLineStringsSimple whether to enforce the constraint that * LineStrings must be simple */ public void setCheckingLineStringsSimple(boolean checkingLineStringsSimple) { this.checkingLineStringsSimple = checkingLineStringsSimple; } /** * Sets whether minimum segment length should be checked. * @param checkingMinSegmentLength whether to enforce the constraint that * segment length should be no less than the minimum * @see #setMinSegmentLength(double) */ public void setCheckingMinSegmentLength(boolean checkingMinSegmentLength) { this.checkingMinSegmentLength = checkingMinSegmentLength; } /** * Sets whether minimum angle should be checked. * @param checkingMinAngle whether to enforce the constraint that * angle should be no less than the minimum * @see #setMinAngle(double) */ public void setCheckingMinAngle(boolean checkingMinAngle) { this.checkingMinAngle = checkingMinAngle; } /** * Sets whether minimum polygon area should be checked. * @param checkingMinPolygonArea whether to enforce the constraint that * area should be no less than the minimum, for single polygons and polygon * elements of GeometryCollections (including MultiPolygons) * @see #setMinPolygonArea(double) */ public void setCheckingMinPolygonArea(boolean checkingMinPolygonArea) { this.checkingMinPolygonArea = checkingMinPolygonArea; } /** * Sets the Geometry classes that are not allowed in the dataset that will * be validated. * @param disallowedGeometryClasses Geometry classes (Polygon.class, for * example) that are not allowed */ public void setDisallowedGeometryClasses( Collection disallowedGeometryClasses) { disallowedGeometryClassNames.clear(); for (Iterator i = disallowedGeometryClasses.iterator(); i.hasNext();) { Class c = (Class) i.next(); disallowedGeometryClassNames.add(c.getName()); } } /** * Checks a collection of features. * @param features the Feature's to validate * @return a List of ValidationErrors; if all features are valid, the list * will be empty */ public List validate(Collection features, TaskMonitor monitor) { monitor.allowCancellationRequests(); validatedFeatureCount = 0; monitor.report(I18N.get("qa.Validator.validating")); ArrayList validationErrors = new ArrayList(); int totalFeatures = features.size(); for (Iterator i = features.iterator(); i.hasNext() && !monitor.isCancelRequested();) { Feature feature = (Feature) i.next(); validate(feature, validationErrors); validatedFeatureCount++; monitor.report(validatedFeatureCount, totalFeatures, "features"); } return validationErrors; } protected void addIfNotNull(Object item, Collection collection) { if (item == null) { return; } collection.add(item); } /** * Checks a feature. * @param feature the Feature to validate * @param validationErrors a List of ValidationError's to add to if the feature * is not valid */ protected void validate(Feature feature, List validationErrors) { addIfNotNull((validateGeometryClass(feature)), validationErrors); if (checkingBasicTopology) { addIfNotNull(validateBasicTopology(feature), validationErrors); } if (checkingPolygonOrientation) { addIfNotNull(validatePolygonOrientation(feature), validationErrors); } if (checkingLineStringsSimple) { addIfNotNull(validateLineStringsSimple(feature), validationErrors); } if (checkingMinSegmentLength) { addIfNotNull(validateMinSegmentLength(feature), validationErrors); } if (checkingMinAngle) { addIfNotNull(validateMinAngle(feature), validationErrors); } if (checkingMinPolygonArea) { addIfNotNull(validateMinPolygonArea(feature), validationErrors); } if (checkingNoHoles) { addIfNotNull(validateNoHoles(feature), validationErrors); } if (checkingNoRepeatedConsecutivePoints) { addIfNotNull(validateNoRepeatedConsecutivePoints(feature), validationErrors); } } protected ValidationError validateGeometryClass(Feature feature) { //Match by class name rather than instanceof, which is less strict //(e.g. instanceof considers a MultiLineString to be a GeometryCollection) //[Jon Aquino] if (disallowedGeometryClassNames.contains(feature.getGeometry() .getClass() .getName())) { return new ValidationError(ValidationErrorType.GEOMETRY_CLASS_DISALLOWED, feature); } return null; } protected ValidationError validateBasicTopology(Feature feature) { TopologyValidationError error = (new IsValidOp(feature.getGeometry())).getValidationError(); if (error != null) { return new BasicTopologyValidationError(error, feature); } return null; } protected ValidationError validateNoRepeatedConsecutivePoints( Feature feature) { if (repeatedPointTester.hasRepeatedPoint(feature.getGeometry())) { return new ValidationError(ValidationErrorType.REPEATED_CONSECUTIVE_POINTS, feature, repeatedPointTester.getCoordinate()); } return null; } protected ValidationError validateLineStringsSimple(Feature feature) { return recursivelyValidate(feature.getGeometry(), feature, new RecursiveValidation() { public ValidationError validate(Geometry g, Feature f) { LineString lineString = (LineString) g; if (!lineString.isSimple()) { return new ValidationError(ValidationErrorType.NONSIMPLE_LINESTRING, f, lineString); } return null; } public Class getTargetGeometryClass() { return LineString.class; } }); } protected ValidationError validatePolygonOrientation(Feature feature) { return recursivelyValidate(feature.getGeometry(), feature, new RecursiveValidation() { public ValidationError validate(Geometry g, Feature f) { Polygon polygon = (Polygon) g; if (cgAlgorithms.isCCW(polygon.getExteriorRing() .getCoordinates())) { return new ValidationError(ValidationErrorType.EXTERIOR_RING_CCW, f, polygon); } for (int i = 0; i < polygon.getNumInteriorRing(); i++) { if (!cgAlgorithms.isCCW(polygon.getInteriorRingN(i) .getCoordinates())) { return new ValidationError(ValidationErrorType.INTERIOR_RING_CW, f, polygon); } } return null; } public Class getTargetGeometryClass() { return Polygon.class; } }); } protected ValidationError validateNoHoles(Feature feature) { return recursivelyValidate(feature.getGeometry(), feature, new RecursiveValidation() { public ValidationError validate(Geometry g, Feature f) { Polygon polygon = (Polygon) g; if (polygon.getNumInteriorRing() > 0) { return new ValidationError(ValidationErrorType.POLYGON_HAS_HOLES, f, polygon.getInteriorRingN(0).getCoordinate()); } return null; } public Class getTargetGeometryClass() { return Polygon.class; } }); } private ValidationError recursivelyValidate(Geometry geometry, Feature feature, RecursiveValidation validation) { if (geometry.isEmpty()) { return null; } if (geometry instanceof GeometryCollection) { return recursivelyValidateGeometryCollection((GeometryCollection) geometry, feature, validation); } if (!(validation.getTargetGeometryClass().isInstance(geometry))) { return null; } return validation.validate(geometry, feature); } private ValidationError recursivelyValidateGeometryCollection( GeometryCollection gc, Feature feature, RecursiveValidation validation) { for (int i = 0; i < gc.getNumGeometries(); i++) { ValidationError error = recursivelyValidate(gc.getGeometryN(i), feature, validation); if (error != null) { return error; } } return null; } protected ValidationError validateMinSegmentLength(Feature feature) { List arrays = CoordinateArrays.toCoordinateArrays(feature.getGeometry(), false); for (Iterator i = arrays.iterator(); i.hasNext();) { Coordinate[] coordinates = (Coordinate[]) i.next(); ValidationError error = validateMinSegmentLength(coordinates, feature); if (error != null) { return error; } } return null; } protected ValidationError validateMinAngle(Feature feature) { List arrays = CoordinateArrays.toCoordinateArrays(feature.getGeometry(), false); for (Iterator i = arrays.iterator(); i.hasNext();) { Coordinate[] coordinates = (Coordinate[]) i.next(); ValidationError error = validateMinAngle(coordinates, feature); if (error != null) { return error; } } return null; } protected ValidationError validateMinPolygonArea(Feature feature) { return recursivelyValidate(feature.getGeometry(), feature, new RecursiveValidation() { public ValidationError validate(Geometry g, Feature f) { Polygon polygon = (Polygon) g; if (polygon.getArea() < minPolygonArea) { return new ValidationError(ValidationErrorType.SMALL_AREA, f, polygon); } return null; } public Class getTargetGeometryClass() { return Polygon.class; } }); } private ValidationError validateMinSegmentLength(Coordinate[] coordinates, Feature feature) { if (coordinates.length < 2) { return null; } for (int i = 1; i < coordinates.length; i++) { //Start at 1 [Jon Aquino] ValidationError error = validateMinSegmentLength(coordinates[i - 1], coordinates[i], feature); if (error != null) { return error; } } return null; } private ValidationError validateMinAngle(Coordinate[] coordinates, Feature feature) { if (coordinates.length < 3) { return null; } boolean closed = coordinates[0].equals(coordinates[coordinates.length - 1]); for (int i = (closed ? 1 : 2); i < coordinates.length; i++) { ValidationError error = validateMinAngle((i == 1) ? coordinates[coordinates.length - 2] : coordinates[i - 2], coordinates[i - 1], coordinates[i], feature); if (error != null) { return error; } } return null; } private ValidationError validateMinSegmentLength(Coordinate c1, Coordinate c2, Feature feature) { if (c1.distance(c2) < minSegmentLength) { return new ValidationError(ValidationErrorType.SMALL_SEGMENT, feature, CoordUtil.average(c1, c2)); } return null; } private ValidationError validateMinAngle(Coordinate c1, Coordinate c2, Coordinate c3, Feature feature) { if (Angle.angleBetween(c2, c1, c3) < Angle.toRadians(minAngle)) { return new ValidationError(ValidationErrorType.SMALL_ANGLE, feature, c2); } return null; } /** * Used to recurse through GeometryCollections (including GeometryCollection subclasses) */ private interface RecursiveValidation { /** * @param g the Geometry to validate * @param f used when constructing a ValidationError * @return a ValidationError if the validation fails; otherwise, null */ public ValidationError validate(Geometry g, Feature f); /** * @return the Geometry class that this RecursiveValidation can validate. */ public Class getTargetGeometryClass(); } }