/* Spatial Operations & Editing Tools for uDig * * Axios Engineering under a funding contract with: * Diputación Foral de Gipuzkoa, Ordenación Territorial * * http://b5m.gipuzkoa.net * http://www.axios.es * * (C) 2006, Diputación Foral de Gipuzkoa, Ordenación Territorial (DFG-OT). * DFG-OT agrees to license under Lesser General Public License (LGPL). * * 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; version 2.1 of the License. * * This library 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. */ package es.axios.lib.geometry.split; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Logger; import com.vividsolutions.jts.algorithm.CGAlgorithms; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.Location; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geomgraph.PlanarGraph; import com.vividsolutions.jts.geomgraph.Position; import es.axios.lib.geometry.util.GeometryList; /** * A {@link PlanarGraph} that builds itself from a {@link Polygon} and a * {@link LineString split line}. * <p> * The resulting graph will have the following characteristics: * <ul> * <li>It will contain as many edges as lineStrings in the boundary of the * intersection geometry between the polygon and the splitting line string. * <li>All edges will be labeled {@link Location#BOUNDARY} at * {@link Position#ON}</li> * <li>The edges from the polygon's exterior ring will be labeled * {@link Location#EXTERIOR} at the {@link Position#LEFT}, * {@link Location#INTERIOR} at {@link Position#RIGHT}</li> * <li>The edges from the polygon's holes will be labeled * {@link Location#INTERIOR} at the {@link Position#LEFT}, * {@link Location#EXTERIOR} at {@link Position#RIGHT}</li> * </ul> * <p> * Note the provided polygon may result modified as the result of * {@link Polygon#normalize()}, which is called in order to ensure proper * orientation of the shell and holes. * </p> * </p> * * @author Mauricio Pazos (www.axios.es) * @author Aritz Davila (www.axios.es) * @since 1.1.0 */ class SplitGraphBuilder { private static final Logger LOGGER = Logger .getLogger(SplitGraphBuilder.class.getName()); private final Polygon polygon; final private UsefulSplitLineBuilder usefulSplitLineBuilder; private Graph graph = new Graph(); public String toString() { return this.usefulSplitLineBuilder.getClass().getName() + ":" + this.usefulSplitLineBuilder.toString(); //$NON-NLS-1$ } /** * Constructor for split graph. * * @param polygon * The polygon to analyze. * @param splitter * The split line. */ public SplitGraphBuilder(final Polygon polygon, final UsefulSplitLineBuilder splitLine) { this.polygon = polygon; LOGGER.fine("Input polygon: " + polygon.toText()); //$NON-NLS-1$ LOGGER.fine("Input split line: " + splitLine.getOriginalSplitLine().toText()); //$NON-NLS-1$ this.usefulSplitLineBuilder = splitLine; } /** * Build the graph using the given polygon and the split line. * * @return this builder */ public SplitGraphBuilder build() { // after normalize() we know the shell is oriented CW and the holes CCW this.polygon.normalize(); this.usefulSplitLineBuilder.build(this.polygon); Geometry utilSplitLine = this.usefulSplitLineBuilder .getResultSplitLine(); AdaptedPolygon adaptedPolygon = this.usefulSplitLineBuilder .getAdaptedPolygon(); LOGGER.fine("Adapted Polygon: " + adaptedPolygon.asPolygon().toText()); //$NON-NLS-1$ LOGGER.fine("Util split line: " + utilSplitLine.toText()); //$NON-NLS-1$ buildGraph(utilSplitLine, adaptedPolygon.asPolygon()); return this; } /** * <pre> * Build the graph using the given polygon and the split line. * <code> * * +----------o-----------+ * | | | * | | | * | +-----------+ | * | | | | | * | | | | | * | | | | | * | o__\__o_____| | * | / | | * /|\ /|\ | * o__________o___________| * * * </code> * </pre> * * @param utilSplitLine * the part of split line that can be used to split the polygon * @param polygon * the polygon to be split * */ private void buildGraph(final Geometry utilSplitLine, final Polygon polygon) { List<Geometry> shellList = makeShellGeometryList(polygon, utilSplitLine); addShellInCW(shellList); Set<LinearRing> nonSplitRings = this.usefulSplitLineBuilder .getNonSplitRings(); List<LineString> splitHoleList = makeValidSplitHoles(polygon, nonSplitRings); List<Geometry> holesList = makeHoleGeometryList(splitHoleList, utilSplitLine); addHolesInCCW(holesList); addSplitLineIntoGraph(utilSplitLine, polygon, splitHoleList); } /** * split intersection segments have interior location at both left and right * * @param utilSplitLine * @param polygon * @param holesList */ private void addSplitLineIntoGraph(final Geometry utilSplitLine, final Polygon polygon, List<LineString> holesList) { // split intersection segments have interior location at both left // and right Geometry intersectingLineStrings = utilSplitLine.intersection(polygon); if (intersectingLineStrings.getNumGeometries() > 1) { // If points exist, then remove them. intersectingLineStrings = filterLineString(intersectingLineStrings); } // use the same input used to create hole edges Geometry holeCollection = intersectingLineStrings.getFactory() .createMultiLineString( holesList.toArray(new LineString[holesList.size()])); Geometry holeGeometries = holeCollection.difference(utilSplitLine); insertEdge(intersectingLineStrings, holeGeometries, Location.BOUNDARY, Location.INTERIOR, Location.INTERIOR); } /** * Only return the lines contained on the given geometry, the non lines * geometry are rejected. * * @param geometry * Intersection geometry between split line and source geometry. * @return The valid geometries needed for the graph, those are lines and * multiLines. */ private Geometry filterLineString(Geometry geometry) { List<Geometry> filteredLines = new ArrayList<Geometry>(); for (int i = 0; i < geometry.getNumGeometries(); i++) { Geometry possibleLine = geometry.getGeometryN(i); // if there are point geometries, discard it. if (possibleLine instanceof LineString || possibleLine instanceof MultiLineString) { // also remove very very short liens. if (possibleLine.getLength() > UsefulSplitLineBuilder.DEPRECIATE_VALUE) { filteredLines.add(possibleLine); } } } GeometryFactory gf = geometry.getFactory(); return gf.buildGeometry(filteredLines); } /** * hole segments oriented CCW means interior at the left, exterior at the * right * * @param holesList */ private void addHolesInCCW(List<Geometry> holesList) { this.graph.addEdges(holesList, Location.BOUNDARY, Location.INTERIOR, Location.EXTERIOR); } /** * shell segments oriented CW means exterior at the left, interior at the * right; * * @param shellList */ private void addShellInCW(List<Geometry> shellList) { this.graph.addEdges(shellList, Location.BOUNDARY, Location.EXTERIOR, Location.INTERIOR); } /** * Makes a List using the polygon shell (hull) and the lineString that are * common to polygon shell and intersection edges. * * @param polygon * @param utilSplitLine * @return a list with the shell and the common edges */ private List<Geometry> makeShellGeometryList(final Polygon polygon, final Geometry utilSplitLine) { Geometry shellDiff = polygon.getExteriorRing() .difference(utilSplitLine); // Geometries that belong to shell List<Geometry> geometriesBelongShell = new ArrayList<Geometry>(); geometriesBelongShell.add(shellDiff); // add the lineString that is common with the polygon shell and // intersection // edges. Geometry intersectResult = polygon.getExteriorRing().intersection( utilSplitLine); geometriesBelongShell = addLinesInCommon(intersectResult, geometriesBelongShell); return geometriesBelongShell; } /** * Adds the geometries present in the intersection result if they are * {@link LineString} into the geometry list * * @param intersectResult * @param geometryList * * @return the geometry list updated with the geometries intersection */ private List<Geometry> addLinesInCommon(Geometry intersectResult, List<Geometry> geometryList) { if (intersectResult instanceof GeometryCollection) { // get the lineString or multiLineString instances for (int i = 0; i < intersectResult.getNumGeometries(); i++) { Geometry part = intersectResult.getGeometryN(i); if (part instanceof LineString || part instanceof MultiLineString) { geometryList.add(part); } } } else if (intersectResult instanceof LineString || intersectResult instanceof MultiLineString) { geometryList.add(intersectResult); } return geometryList; } /** * Makes a list of polygon holes that intersect with the split line. * * @param polygonHolesArray * polygon holes * @param utilSplitLine * Line or MultiLine * @return a List of holes */ private List<Geometry> makeHoleGeometryList( final List<LineString> polygonHolesArray, final Geometry utilSplitLine) { GeometryFactory factory = this.polygon.getFactory(); Geometry holeCollection = factory .createMultiLineString(polygonHolesArray .toArray(new LineString[polygonHolesArray.size()])); Geometry nodedHoles = holeCollection.difference(utilSplitLine); List<Geometry> geometriesBelongHole = new ArrayList<Geometry>(); geometriesBelongHole.add(nodedHoles); // add the lineString that are common to hole and intersection edges. Geometry intersectionResult = holeCollection .intersection(utilSplitLine); geometriesBelongHole = addLinesInCommon(intersectionResult, geometriesBelongHole); return geometriesBelongHole; } /** * Make a list of holes involved in the split operation * * @param polygon * @param nonSplitRings * @return the holes involved in the split operation */ private List<LineString> makeValidSplitHoles(final Polygon polygon, Set<LinearRing> nonSplitRings) { List<LineString> holesArray = new GeometryList<LineString>(); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { LineString hole = polygon.getInteriorRingN(i); // if the hole isn't one of the non-split ring, add them because // this hole will suffer split. if (!nonSplitRings.contains(hole)) { holesArray.add(hole); } } return holesArray; } /** * Each edge will be built with 2 coordinates. * * @param intersectingLineStrings * The geometry which edges will be based on. * @param onLoc * position for ON. * @param leftLoc * position for LEFT. * @param rightLoc * position for RIGHT. */ private void insertEdge(final Geometry intersectingLineStrings, final Geometry holeGeometries, final int onLoc, final int leftLoc, final int rightLoc) { for (int i = 0; i < intersectingLineStrings.getNumGeometries(); i++) { Geometry intersectingSegment = intersectingLineStrings .getGeometryN(i); if ((intersectingSegment.getNumPoints() == 2) && !holeGeometries.isEmpty()) { // special case, when the line has 2 coordinates and // its orientation can't be calculated because it hasn't. intersectingSegment = adjustSegmentToHoleDirection( intersectingSegment, holeGeometries); } Coordinate[] coords = intersectingSegment.getCoordinates(); for (int j = 0; j < coords.length - 1; j++) { final SplitEdge edge = SplitEdge.newInstance(coords[j], coords[j + 1], onLoc, leftLoc, rightLoc); // add the list that only contains one edge because it will // create 2 directedEdge. this.graph.addEdge(edge); } } } /** * Checks if the intersecting segment intersects with a hole in two points. * In that case, the segment might be adjusted following the orientation of * the intersected hole. * * @param intersectingSegment * this segment that could intersect with a hole * @param holeGeometries * the polygon hole list */ private LineString adjustSegmentToHoleDirection( final Geometry intersectingSegment, final Geometry holeGeometries) { LineString intersectedHole = intersectionHole(intersectingSegment, holeGeometries); if (intersectedHole == null) { return (LineString) intersectingSegment;// it does not require // adjust orientation } // Traverses the hole-segments until the second intersection with the // intersectingSegment is found // a ring will be created with those segments between first intersection // and second intersection. Coordinate secondIntersection = null; Coordinate firstIntersection = null; int j = -1; Coordinate[] holeCoords = intersectedHole.getCoordinates(); List<Coordinate> ring = new LinkedList<Coordinate>(); for (int i = 0; i < holeCoords.length - 1; i++) { Geometry intersection = intersectionWithSegment(holeCoords, i, intersectingSegment); if (intersection instanceof Point) { // store first and second coordinates if (firstIntersection == null) { firstIntersection = intersection.getCoordinate(); ring.add(firstIntersection); ring.add(holeCoords[i + 1]); j = i + 1; break; } // Adds the rest of segments in the ring until found a second // intersection } } assert firstIntersection != null && j != -1; while (true) { Geometry intersection = intersectionWithSegment(holeCoords, j, intersectingSegment); if (intersection instanceof Point && !intersection.getCoordinate() .equals2D(firstIntersection)) { secondIntersection = intersection.getCoordinate(); ring.add(secondIntersection); // close the ring ring.add(firstIntersection); break; } else { ring.add(holeCoords[j + 1]); } j++; } assert secondIntersection != null; // Creates the adjusted line following this rules: // - if the ring is CW then the result line must be this: first // intersection coordinate--> second intersection coordinate. // - if the ring is CCW the the result line must be this: second // intersection coordinate --> first intersection coordinate. GeometryFactory factory = intersectingSegment.getFactory(); LinearRing linearRing = factory.createLinearRing(ring .toArray(new Coordinate[ring.size()])); LineString adjustedSegment = null; if (isCW(linearRing)) { adjustedSegment = createAdjustedSegment(firstIntersection, secondIntersection, factory); } else { adjustedSegment = createAdjustedSegment(secondIntersection, firstIntersection, factory); } return adjustedSegment; } /** * Finds the hole that intersects in two points with the the segment. * * @param splitLineSegment * @param holeGeometries * * @return the hole that intersect with the segment, null in other case */ private LineString intersectionHole(final Geometry splitLineSegment, final Geometry holeGeometries) { List<LineString> holeList = convertHolesGeometriesToHoleList(holeGeometries); LineString intersectedHole = null; for (LineString hole : holeList) { // Seeks if the segment intersect with the line Geometry intersectionWithHole = splitLineSegment.intersection(hole); if (intersectionWithHole.getNumGeometries() == 2) { intersectedHole = hole; } } return intersectedHole; } /** * converts the collection of holes (LineString) to a List of String * * @param holeGeometries * @return List of holes as LineString */ private List<LineString> convertHolesGeometriesToHoleList( final Geometry holeGeometries) { assert holeGeometries != null; List<LineString> holeList = new ArrayList<LineString>( holeGeometries.getNumGeometries()); for (int i = 0; i < holeGeometries.getNumGeometries(); i++) { LineString hole = (LineString) holeGeometries.getGeometryN(i); holeList.add(hole); } return holeList; } private LineString createAdjustedSegment( final Coordinate firstIntersection, final Coordinate secondIntersection, final GeometryFactory factory) { Coordinate[] adjustedSegmentCoords = new Coordinate[] { firstIntersection, secondIntersection }; return factory.createLineString(adjustedSegmentCoords); } /** * Check if it's CW. * * @param linearRing * @return true if the ring has a clock wise orientation */ private boolean isCW(final LinearRing linearRing) { Coordinate[] ringCoord = linearRing.getCoordinates(); return !CGAlgorithms.isCCW(ringCoord); } private Geometry intersectionWithSegment(Coordinate[] holeCoords, int i, Geometry intersectingSegment) { Coordinate[] holeSegmentCoord = new Coordinate[] { holeCoords[i], holeCoords[i + 1] }; LineString holeSegment; GeometryFactory geomFact = intersectingSegment.getFactory(); holeSegment = geomFact.createLineString(holeSegmentCoord); Geometry intersection = holeSegment.intersection(intersectingSegment); return intersection; } /** * The set of rings that have not suffered split * * @return a list of rings */ public Set<LinearRing> getNonSplitRings() { return this.usefulSplitLineBuilder.getNonSplitRings(); } /** * The resultant of {@link #build()} method. * * @return The built graph */ public Graph getResultantGraph() { return this.graph; } }