001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032:
033: package com.vividsolutions.jump.qa;
034:
035: import java.util.*;
036:
037: import com.vividsolutions.jts.algorithm.RobustCGAlgorithms;
038: import com.vividsolutions.jts.geom.*;
039: import com.vividsolutions.jts.operation.valid.*;
040: import com.vividsolutions.jump.I18N;
041: import com.vividsolutions.jump.feature.Feature;
042: import com.vividsolutions.jump.geom.Angle;
043: import com.vividsolutions.jump.geom.CoordUtil;
044: import com.vividsolutions.jump.task.TaskMonitor;
045: import com.vividsolutions.jump.util.CoordinateArrays;
046:
047: /**
048: * Performs basic JTS validation, and additional validation like checking polygon
049: * orientation.
050: */
051: public class Validator {
052: private int validatedFeatureCount;
053: private boolean checkingBasicTopology = true;
054: private boolean checkingPolygonOrientation = false;
055: private boolean checkingLineStringsSimple = false;
056: private boolean checkingMinSegmentLength = false;
057: private boolean checkingMinAngle = false;
058: private boolean checkingMinPolygonArea = false;
059: private boolean checkingNoRepeatedConsecutivePoints = false;
060: private boolean checkingNoHoles = false;
061: private double minSegmentLength = 0;
062: private double minAngle = 0;
063: private double minPolygonArea = 0;
064: private Collection disallowedGeometryClassNames = new ArrayList();
065: private RepeatedPointTester repeatedPointTester = new RepeatedPointTester();
066: private RobustCGAlgorithms cgAlgorithms = new RobustCGAlgorithms();
067:
068: //<<TODO:REFACTORING>> Move this class and associated classes to JTS [Jon Aquino]
069: public Validator() {
070: }
071:
072: /**
073: * Sets whether basic JTS validation should be performed
074: * @param checkingBasicTopology whether basic JTS validation should be performed
075: */
076: public void setCheckingBasicTopology(boolean checkingBasicTopology) {
077: this .checkingBasicTopology = checkingBasicTopology;
078: }
079:
080: /**
081: * Sets whether consecutive points are not allowed to be the same
082: * @param checkingNoRepeatedConsecutivePoints whether consecutive points are
083: * not allowed to be the same
084: */
085: public void setCheckingNoRepeatedConsecutivePoints(
086: boolean checkingNoRepeatedConsecutivePoints) {
087: this .checkingNoRepeatedConsecutivePoints = checkingNoRepeatedConsecutivePoints;
088: }
089:
090: /**
091: * Sets whether polygons are not allowed to have holes
092: * @param checkingNoHoles whether polygons are not allowed to have holes
093: */
094: public void setCheckingNoHoles(boolean checkingNoHoles) {
095: this .checkingNoHoles = checkingNoHoles;
096: }
097:
098: /**
099: * Sets whether polygon orientation should be checked
100: * @param checkingPolygonOrientation whether to enforce the constraint that
101: * polygon shells should be oriented clockwise and holes should be oriented
102: * counterclockwise
103: */
104: public void setCheckingPolygonOrientation(
105: boolean checkingPolygonOrientation) {
106: this .checkingPolygonOrientation = checkingPolygonOrientation;
107: }
108:
109: /**
110: * Sets the segment length below which the minimum-segment-length check
111: * will raise a validation error.
112: * @param minSegmentLength the threshold used by the minimum-segment-length
113: * check
114: * @see #setCheckingMinSegmentLength(boolean)
115: */
116: public void setMinSegmentLength(double minSegmentLength) {
117: this .minSegmentLength = minSegmentLength;
118: }
119:
120: /**
121: * Sets the angle below which the minimum-angle check
122: * will raise a validation error.
123: * @param minAngle the threshold used by the minimum-angle check, in degrees
124: * @see #setCheckingMinAngle(boolean)
125: */
126: public void setMinAngle(double minAngle) {
127: this .minAngle = minAngle;
128: }
129:
130: /**
131: * Sets the area below which the minimum-polygon-area check will raise a
132: * validation error.
133: * @param minPolygonArea the threshould used by the minimum-polygon-area check
134: * @see #setCheckingMinPolygonArea(boolean)
135: */
136: public void setMinPolygonArea(double minPolygonArea) {
137: this .minPolygonArea = minPolygonArea;
138: }
139:
140: /**
141: * Sets whether to enforce the constraint that LineStrings must be simple
142: * @param checkingLineStringsSimple whether to enforce the constraint that
143: * LineStrings must be simple
144: */
145: public void setCheckingLineStringsSimple(
146: boolean checkingLineStringsSimple) {
147: this .checkingLineStringsSimple = checkingLineStringsSimple;
148: }
149:
150: /**
151: * Sets whether minimum segment length should be checked.
152: * @param checkingMinSegmentLength whether to enforce the constraint that
153: * segment length should be no less than the minimum
154: * @see #setMinSegmentLength(double)
155: */
156: public void setCheckingMinSegmentLength(
157: boolean checkingMinSegmentLength) {
158: this .checkingMinSegmentLength = checkingMinSegmentLength;
159: }
160:
161: /**
162: * Sets whether minimum angle should be checked.
163: * @param checkingMinAngle whether to enforce the constraint that
164: * angle should be no less than the minimum
165: * @see #setMinAngle(double)
166: */
167: public void setCheckingMinAngle(boolean checkingMinAngle) {
168: this .checkingMinAngle = checkingMinAngle;
169: }
170:
171: /**
172: * Sets whether minimum polygon area should be checked.
173: * @param checkingMinPolygonArea whether to enforce the constraint that
174: * area should be no less than the minimum, for single polygons and polygon
175: * elements of GeometryCollections (including MultiPolygons)
176: * @see #setMinPolygonArea(double)
177: */
178: public void setCheckingMinPolygonArea(boolean checkingMinPolygonArea) {
179: this .checkingMinPolygonArea = checkingMinPolygonArea;
180: }
181:
182: /**
183: * Sets the Geometry classes that are not allowed in the dataset that will
184: * be validated.
185: * @param disallowedGeometryClasses Geometry classes (Polygon.class, for
186: * example) that are not allowed
187: */
188: public void setDisallowedGeometryClasses(
189: Collection disallowedGeometryClasses) {
190: disallowedGeometryClassNames.clear();
191:
192: for (Iterator i = disallowedGeometryClasses.iterator(); i
193: .hasNext();) {
194: Class c = (Class) i.next();
195: disallowedGeometryClassNames.add(c.getName());
196: }
197: }
198:
199: /**
200: * Checks a collection of features.
201: * @param features the Feature's to validate
202: * @return a List of ValidationErrors; if all features are valid, the list
203: * will be empty
204: */
205: public List validate(Collection features, TaskMonitor monitor) {
206: monitor.allowCancellationRequests();
207: validatedFeatureCount = 0;
208: monitor.report(I18N.get("qa.Validator.validating"));
209:
210: ArrayList validationErrors = new ArrayList();
211: int totalFeatures = features.size();
212:
213: for (Iterator i = features.iterator(); i.hasNext()
214: && !monitor.isCancelRequested();) {
215: Feature feature = (Feature) i.next();
216: validate(feature, validationErrors);
217: validatedFeatureCount++;
218: monitor.report(validatedFeatureCount, totalFeatures,
219: "features");
220: }
221:
222: return validationErrors;
223: }
224:
225: protected void addIfNotNull(Object item, Collection collection) {
226: if (item == null) {
227: return;
228: }
229:
230: collection.add(item);
231: }
232:
233: /**
234: * Checks a feature.
235: * @param feature the Feature to validate
236: * @param validationErrors a List of ValidationError's to add to if the feature
237: * is not valid
238: */
239: protected void validate(Feature feature, List validationErrors) {
240: addIfNotNull((validateGeometryClass(feature)), validationErrors);
241:
242: if (checkingBasicTopology) {
243: addIfNotNull(validateBasicTopology(feature),
244: validationErrors);
245: }
246:
247: if (checkingPolygonOrientation) {
248: addIfNotNull(validatePolygonOrientation(feature),
249: validationErrors);
250: }
251:
252: if (checkingLineStringsSimple) {
253: addIfNotNull(validateLineStringsSimple(feature),
254: validationErrors);
255: }
256:
257: if (checkingMinSegmentLength) {
258: addIfNotNull(validateMinSegmentLength(feature),
259: validationErrors);
260: }
261:
262: if (checkingMinAngle) {
263: addIfNotNull(validateMinAngle(feature), validationErrors);
264: }
265:
266: if (checkingMinPolygonArea) {
267: addIfNotNull(validateMinPolygonArea(feature),
268: validationErrors);
269: }
270:
271: if (checkingNoHoles) {
272: addIfNotNull(validateNoHoles(feature), validationErrors);
273: }
274:
275: if (checkingNoRepeatedConsecutivePoints) {
276: addIfNotNull(validateNoRepeatedConsecutivePoints(feature),
277: validationErrors);
278: }
279: }
280:
281: protected ValidationError validateGeometryClass(Feature feature) {
282: //Match by class name rather than instanceof, which is less strict
283: //(e.g. instanceof considers a MultiLineString to be a GeometryCollection)
284: //[Jon Aquino]
285: if (disallowedGeometryClassNames.contains(feature.getGeometry()
286: .getClass().getName())) {
287: return new ValidationError(
288: ValidationErrorType.GEOMETRY_CLASS_DISALLOWED,
289: feature);
290: }
291:
292: return null;
293: }
294:
295: protected ValidationError validateBasicTopology(Feature feature) {
296: TopologyValidationError error = (new IsValidOp(feature
297: .getGeometry())).getValidationError();
298:
299: if (error != null) {
300: return new BasicTopologyValidationError(error, feature);
301: }
302:
303: return null;
304: }
305:
306: protected ValidationError validateNoRepeatedConsecutivePoints(
307: Feature feature) {
308: if (repeatedPointTester.hasRepeatedPoint(feature.getGeometry())) {
309: return new ValidationError(
310: ValidationErrorType.REPEATED_CONSECUTIVE_POINTS,
311: feature, repeatedPointTester.getCoordinate());
312: }
313:
314: return null;
315: }
316:
317: protected ValidationError validateLineStringsSimple(Feature feature) {
318: return recursivelyValidate(feature.getGeometry(), feature,
319: new RecursiveValidation() {
320: public ValidationError validate(Geometry g,
321: Feature f) {
322: LineString lineString = (LineString) g;
323:
324: if (!lineString.isSimple()) {
325: return new ValidationError(
326: ValidationErrorType.NONSIMPLE_LINESTRING,
327: f, lineString);
328: }
329:
330: return null;
331: }
332:
333: public Class getTargetGeometryClass() {
334: return LineString.class;
335: }
336: });
337: }
338:
339: protected ValidationError validatePolygonOrientation(Feature feature) {
340: return recursivelyValidate(feature.getGeometry(), feature,
341: new RecursiveValidation() {
342: public ValidationError validate(Geometry g,
343: Feature f) {
344: Polygon polygon = (Polygon) g;
345:
346: if (cgAlgorithms.isCCW(polygon
347: .getExteriorRing().getCoordinates())) {
348: return new ValidationError(
349: ValidationErrorType.EXTERIOR_RING_CCW,
350: f, polygon);
351: }
352:
353: for (int i = 0; i < polygon
354: .getNumInteriorRing(); i++) {
355: if (!cgAlgorithms.isCCW(polygon
356: .getInteriorRingN(i)
357: .getCoordinates())) {
358: return new ValidationError(
359: ValidationErrorType.INTERIOR_RING_CW,
360: f, polygon);
361: }
362: }
363:
364: return null;
365: }
366:
367: public Class getTargetGeometryClass() {
368: return Polygon.class;
369: }
370: });
371: }
372:
373: protected ValidationError validateNoHoles(Feature feature) {
374: return recursivelyValidate(feature.getGeometry(), feature,
375: new RecursiveValidation() {
376: public ValidationError validate(Geometry g,
377: Feature f) {
378: Polygon polygon = (Polygon) g;
379:
380: if (polygon.getNumInteriorRing() > 0) {
381: return new ValidationError(
382: ValidationErrorType.POLYGON_HAS_HOLES,
383: f, polygon.getInteriorRingN(0)
384: .getCoordinate());
385: }
386:
387: return null;
388: }
389:
390: public Class getTargetGeometryClass() {
391: return Polygon.class;
392: }
393: });
394: }
395:
396: private ValidationError recursivelyValidate(Geometry geometry,
397: Feature feature, RecursiveValidation validation) {
398: if (geometry.isEmpty()) {
399: return null;
400: }
401:
402: if (geometry instanceof GeometryCollection) {
403: return recursivelyValidateGeometryCollection(
404: (GeometryCollection) geometry, feature, validation);
405: }
406:
407: if (!(validation.getTargetGeometryClass().isInstance(geometry))) {
408: return null;
409: }
410:
411: return validation.validate(geometry, feature);
412: }
413:
414: private ValidationError recursivelyValidateGeometryCollection(
415: GeometryCollection gc, Feature feature,
416: RecursiveValidation validation) {
417: for (int i = 0; i < gc.getNumGeometries(); i++) {
418: ValidationError error = recursivelyValidate(gc
419: .getGeometryN(i), feature, validation);
420:
421: if (error != null) {
422: return error;
423: }
424: }
425:
426: return null;
427: }
428:
429: protected ValidationError validateMinSegmentLength(Feature feature) {
430: List arrays = CoordinateArrays.toCoordinateArrays(feature
431: .getGeometry(), false);
432:
433: for (Iterator i = arrays.iterator(); i.hasNext();) {
434: Coordinate[] coordinates = (Coordinate[]) i.next();
435: ValidationError error = validateMinSegmentLength(
436: coordinates, feature);
437:
438: if (error != null) {
439: return error;
440: }
441: }
442:
443: return null;
444: }
445:
446: protected ValidationError validateMinAngle(Feature feature) {
447: List arrays = CoordinateArrays.toCoordinateArrays(feature
448: .getGeometry(), false);
449:
450: for (Iterator i = arrays.iterator(); i.hasNext();) {
451: Coordinate[] coordinates = (Coordinate[]) i.next();
452: ValidationError error = validateMinAngle(coordinates,
453: feature);
454:
455: if (error != null) {
456: return error;
457: }
458: }
459:
460: return null;
461: }
462:
463: protected ValidationError validateMinPolygonArea(Feature feature) {
464: return recursivelyValidate(feature.getGeometry(), feature,
465: new RecursiveValidation() {
466: public ValidationError validate(Geometry g,
467: Feature f) {
468: Polygon polygon = (Polygon) g;
469:
470: if (polygon.getArea() < minPolygonArea) {
471: return new ValidationError(
472: ValidationErrorType.SMALL_AREA, f,
473: polygon);
474: }
475:
476: return null;
477: }
478:
479: public Class getTargetGeometryClass() {
480: return Polygon.class;
481: }
482: });
483: }
484:
485: private ValidationError validateMinSegmentLength(
486: Coordinate[] coordinates, Feature feature) {
487: if (coordinates.length < 2) {
488: return null;
489: }
490:
491: for (int i = 1; i < coordinates.length; i++) { //Start at 1 [Jon Aquino]
492:
493: ValidationError error = validateMinSegmentLength(
494: coordinates[i - 1], coordinates[i], feature);
495:
496: if (error != null) {
497: return error;
498: }
499: }
500:
501: return null;
502: }
503:
504: private ValidationError validateMinAngle(Coordinate[] coordinates,
505: Feature feature) {
506: if (coordinates.length < 3) {
507: return null;
508: }
509:
510: boolean closed = coordinates[0]
511: .equals(coordinates[coordinates.length - 1]);
512:
513: for (int i = (closed ? 1 : 2); i < coordinates.length; i++) {
514: ValidationError error = validateMinAngle(
515: (i == 1) ? coordinates[coordinates.length - 2]
516: : coordinates[i - 2], coordinates[i - 1],
517: coordinates[i], feature);
518:
519: if (error != null) {
520: return error;
521: }
522: }
523:
524: return null;
525: }
526:
527: private ValidationError validateMinSegmentLength(Coordinate c1,
528: Coordinate c2, Feature feature) {
529: if (c1.distance(c2) < minSegmentLength) {
530: return new ValidationError(
531: ValidationErrorType.SMALL_SEGMENT, feature,
532: CoordUtil.average(c1, c2));
533: }
534:
535: return null;
536: }
537:
538: private ValidationError validateMinAngle(Coordinate c1,
539: Coordinate c2, Coordinate c3, Feature feature) {
540: if (Angle.angleBetween(c2, c1, c3) < Angle.toRadians(minAngle)) {
541: return new ValidationError(ValidationErrorType.SMALL_ANGLE,
542: feature, c2);
543: }
544:
545: return null;
546: }
547:
548: /**
549: * Used to recurse through GeometryCollections (including GeometryCollection subclasses)
550: */
551: private interface RecursiveValidation {
552: /**
553: * @param g the Geometry to validate
554: * @param f used when constructing a ValidationError
555: * @return a ValidationError if the validation fails; otherwise, null
556: */
557: public ValidationError validate(Geometry g, Feature f);
558:
559: /**
560: * @return the Geometry class that this RecursiveValidation can validate.
561: */
562: public Class getTargetGeometryClass();
563: }
564: }
|