/* 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 licence 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.udig.ui.editingtools.internal.geometryoperations.split; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import com.vividsolutions.jts.algorithm.CGAlgorithms; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateArrays; import com.vividsolutions.jts.geom.CoordinateList; import com.vividsolutions.jts.geom.CoordinateSequence; 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.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geomgraph.DirectedEdge; import com.vividsolutions.jts.geomgraph.Node; import es.axios.udig.ui.editingtools.internal.i18n.Messages; /** * Performs a split of a LineString, MultiLineString, Polygon or MultiPolygon using a provided * LineString as cutting edge. * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ public class SplitStrategy { private final LineString splittingLine; private static final Map /* <Class, Class<? extends SpecificSplitOp> */strategies; static { Map knownStrategies = new HashMap(); knownStrategies.put(LineString.class, LineStringSplitter.class); knownStrategies.put(MultiLineString.class, MultiLineStringSplitter.class); knownStrategies.put(Polygon.class, PolygonSplitter.class); knownStrategies.put(MultiPolygon.class, MultiPolygonSplitter.class); strategies = Collections.unmodifiableMap(knownStrategies); } public SplitStrategy( final LineString splittingLine ) { if (splittingLine == null) { throw new NullPointerException(); } this.splittingLine = splittingLine; } public static Geometry splitOp( Geometry geom, LineString splitLine ) { SplitStrategy op = new SplitStrategy(splitLine); Geometry splittedGeometries = op.split(geom); return splittedGeometries; } /** * @param splitee * @return a <code>Geometry</code> containing all the splitted parts as aggregates. Use * {@link Geometry#getGeometryN(int) getGeometryN(int)} to get each part. * @throws NullPointerException if geom is null * @throws IllegalArgumentException if geom is not of an acceptable geometry type to be splitted * (i.e. not a linestring, multilinestring, polygon or multipolygon) */ public Geometry split( final Geometry splitee ) { if (splitee == null) { throw new NullPointerException("Geometry for split operation was null"); } Class spliteeClass = splitee.getClass(); SpecificSplitOp splitOp = findSplitOp(spliteeClass); Geometry splitResult; splitOp.setSplitter(splittingLine); splitResult = splitOp.split(splitee); return splitResult; } private SpecificSplitOp findSplitOp( Class spliteeClass ) { if (!strategies.containsKey(spliteeClass)) { throw new IllegalArgumentException(Messages.SplitStrategy_illegal_geometry + spliteeClass); } final Class splitOpClass = (Class) strategies.get(spliteeClass); SpecificSplitOp splitOp; try { splitOp = (SpecificSplitOp) splitOpClass.newInstance(); } catch (InstantiationException e) { throw new IllegalStateException("Cannot instantiate " + splitOpClass.getName(), e); } catch (IllegalAccessException e) { throw new IllegalStateException("Illegal access exception for " + splitOpClass.getName(), e ); } return splitOp; } /** * Strategy object for splitting geometry, subclasses are targeted towards specific kinds * of geometry. * <p> * The GeometryCollectionSplitter will hold onto a LineString provided by the user, and use * it to break provided geometry one by one. * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static interface SpecificSplitOp { /** * LineString used for splitting; as provided by the user. * * @param splitter LineString used by the split method to break up geometry one by one. */ public void setSplitter( LineString splitter ); /** * Split the provided Geometry using the LineString provided by the user. * * @param splitee Geometry to split using the user supplied lineString * @return Original geometry, or a GeometryCollection if a split occurred */ public Geometry split( Geometry splitee ); } /** * Hold onto a LineString for use by subclasses. * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static abstract class AbstractSplitter implements SpecificSplitOp { protected LineString splitter; public void setSplitter( LineString splitter ) { this.splitter = splitter; } } /** * User the lineString provided by the user to break up lineStrings one by one. * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static class LineStringSplitter extends AbstractSplitter { /** * No-op default constructor required to reflectively instantiate the class */ public LineStringSplitter() { } /** * @param splitee the {@link LineString} to be splitted */ public Geometry split( Geometry splitee ) { LineString lineString = (LineString) splitee; Geometry splitted = lineString.difference(splitter); return splitted; } } /** * Strategy object for splitting (a geometry collection). * <p> * The GeometryCollectionSplitter will hold onto the SpecificcSplitOp in order to hold * onto the LineString provided by the user for spliting. * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static abstract class AbstractGeometryCollectionSplitter implements SpecificSplitOp { /** Used to split a single geometry */ private SpecificSplitOp singlePartSplitter; private AbstractGeometryCollectionSplitter( SpecificSplitOp singlePartSplitter ) { this.singlePartSplitter = singlePartSplitter; } /** * Update the singlePartSplitter with the provided LineString. * @param splitter LineString used by split method to split provided geometry one by one */ public final void setSplitter( LineString splitter ) { singlePartSplitter.setSplitter(splitter); } /** * Actually forms the split, using the LineString held internally. * */ public final Geometry split( final Geometry splitee ) { final GeometryCollection coll = (GeometryCollection) splitee; final int numParts = coll.getNumGeometries(); List splittedParts = new ArrayList(); for( int partN = 0; partN < numParts; partN++ ) { Geometry simplePartN = coll.getGeometryN(partN); Geometry splittedPart = singlePartSplitter.split(simplePartN); if( splittedPart == null ) { continue; // part was not split ... move on to the next } final int splittedPartsCount = splittedPart.getNumGeometries(); for( int splittedPartN = 0; splittedPartN < splittedPartsCount; splittedPartN++ ) { Geometry simpleSplittedPart = splittedPart.getGeometryN(splittedPartN); splittedParts.add(simpleSplittedPart); } } GeometryFactory gf = splitee.getFactory(); GeometryCollection splittedCollection = buildFromParts(gf, splittedParts); return splittedCollection; } protected abstract GeometryCollection buildFromParts( GeometryFactory gf, List parts ); } /** * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static class MultiLineStringSplitter extends AbstractGeometryCollectionSplitter { public MultiLineStringSplitter() { super(new LineStringSplitter()); } @Override protected GeometryCollection buildFromParts( GeometryFactory gf, List parts ) { LineString[] lines = (LineString[]) parts.toArray(new LineString[parts.size()]); MultiLineString result = gf.createMultiLineString(lines); return result; } } /** * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static class MultiPolygonSplitter extends AbstractGeometryCollectionSplitter { public MultiPolygonSplitter() { super(new PolygonSplitter()); } @Override protected GeometryCollection buildFromParts( GeometryFactory gf, List parts ) { Polygon[] polygons = (Polygon[]) parts.toArray(new Polygon[parts.size()]); MultiPolygon result = gf.createMultiPolygon(polygons); return result; } } /** * Responsible for splitting a single polygon; polygon may be split into several parts (or a * hole may be formed). * * Polygon Strategy: * <ul> * <li>Build a graph with all the edges and nodes from the intersection between the polygon and * the line * <li>Put weights on the nodes depending on the amount of incident edges. Nodes are only the * intersection points between the polygon boundary and the linestring, and the start point of * the polygon boundary. * <li>Classify the edges between shared (all the linestring ones) and non shared (the polygon * boundary ones). Store the coordinate list of an edge on the edge object itself. * <li>Start traveling the graph at any node, starting by its first edge. * <li>Alway travel to the next node, selecting the edge whose first segment has the lower * angle to the left (CCW) with the last segment in the linestring from the current edge. * <li>Remove the non shared edges used from the graph. * <li>Decrement in 1 the weight of the used nodes. * <li>Mark the remaining edges that have a node with weight < 3 as non-shared * <li>Remove the nodes with weight < 1 from the graph * </ul> * * @author Gabriel Roldán (www.axios.es) * @author Mauricio Pazos (www.axios.es) * @since 1.1.0 */ private static class PolygonSplitter extends AbstractSplitter { /** * No-op default constructor required to reflectively instantiate the class */ public PolygonSplitter() { // no-op } /** * Split the provided geometry, will be null if no split was needed. * @return split geometry or null */ public Geometry split( Geometry splitee ) { assert splitee instanceof Polygon; // why? Class cast exception about to happen? final Polygon polygon = (Polygon) splitee; final Geometry splitted = splitPolygon(polygon); return splitted; } /** * Acutally split the provided polygon. * <p> * Depending on the topology we have three options: * <ul> * <li>single polygon: * <li>multipolgon: provided geom was split into several parts * <li>null: nothing see here, please move on * </ul> * * @return Ruturn a single polygon, multipolygon or null depending on how the split went. */ private Geometry splitPolygon( final Polygon geom ) { SplitGraph graph = new SplitGraph(geom, splitter); if( !graph.isSplit() ){ if( geom.contains( splitter )){ // possibility of a hole LinearRing ring = null; GeometryFactory factory = splitter.getFactory(); CoordinateList list = new CoordinateList( splitter.getCoordinates() ); list.closeRing(); ring = factory.createLinearRing( list.toCoordinateArray() ); Polygon hole = factory.createPolygon( ring, null ); return holePolygon( geom, hole ); } return null; } final GeometryFactory gf = geom.getFactory(); // store unsplitted holes for later addition List<LinearRing> unsplittedHoles = findUnsplittedHoles(graph, gf); List<List<SplitEdge>> allRings = findRings(graph); List<Polygon> resultingPolygons = buildSimplePolygons(allRings, unsplittedHoles, gf); List<Polygon> cleanedPolygons = new ArrayList<Polygon>(); for( Polygon poly : resultingPolygons ){ if( poly.isValid() ){ cleanedPolygons.add( poly ); } else { Geometry geometry = poly.buffer(0.0); // fix up splinters? often makes the geometry valid for( int i=0; i< geometry.getNumGeometries(); i++){ Geometry part = geometry.getGeometryN(i); if( part instanceof Polygon ){ cleanedPolygons.add( (Polygon) part ); } else { throw new IllegalStateException("Unexpected "+part.getGeometryType()+" during split, ensure polygon is valid prior to splitting"); } } } } Geometry result; if (cleanedPolygons.size() == 1) { result = cleanedPolygons.get(0); } else { Polygon[] array = cleanedPolygons.toArray(new Polygon[cleanedPolygons.size()]); result = gf.createMultiPolygon(array); } return result; } /** * Drill a hole in the provided polygon. * * @param geom * @param hole * @return GeometryCollection of the (usually two) resulting polygons. */ private GeometryCollection holePolygon( Polygon geom, Polygon hole ) { GeometryFactory factory = geom.getFactory(); Geometry difference = geom.difference( hole ); Geometry[] geometries = new Geometry[ difference.getNumGeometries()+1 ]; for( int i =0; i<difference.getNumGeometries(); i++){ geometries[i] = difference.getGeometryN(i); } geometries[ geometries.length-1] = hole; return factory.createGeometryCollection(geometries); } /** * Finds out and removes from the graph the edges that were originally holes in the polygon * and were not splitted by the splitting line. * * @param graph * @param gf * @return */ @SuppressWarnings("unchecked") private List<LinearRing> findUnsplittedHoles( SplitGraph graph, GeometryFactory gf ) { final List<LinearRing> unsplittedHoles = new ArrayList<LinearRing>(2); final List<SplitEdge> edges = new ArrayList<SplitEdge>(); for( Iterator it = graph.getEdgeIterator(); it.hasNext(); ) { SplitEdge edge = (SplitEdge) it.next(); edges.add(edge); } for( Iterator it = edges.iterator(); it.hasNext(); ) { SplitEdge edge = (SplitEdge) it.next(); if (edge.isHoleEdge()) { Coordinate[] coordinates = edge.getCoordinates(); Coordinate start = coordinates[0]; Coordinate end = coordinates[coordinates.length - 1]; boolean isLinearRing = start.equals2D(end); if (isLinearRing) { graph.remove(edge); LinearRing ring = gf.createLinearRing(coordinates); unsplittedHoles.add(ring); } } } return unsplittedHoles; } private List<Polygon> buildSimplePolygons( List<List<SplitEdge>> allRings, List<LinearRing> unsplittedHoles, GeometryFactory gf ) { List<Polygon> polygons = new ArrayList<Polygon>(allRings.size()); for( List<SplitEdge> edgeList : allRings ) { Polygon poly = buildPolygon(edgeList, gf); List<LinearRing> thisPolyHoles = new ArrayList<LinearRing>(unsplittedHoles.size()); for( LinearRing holeRing : unsplittedHoles ) { if (poly.covers(holeRing)) { thisPolyHoles.add(holeRing); } } unsplittedHoles.removeAll(thisPolyHoles); int numHoles = thisPolyHoles.size(); if (numHoles > 0) { LinearRing[] holes = thisPolyHoles.toArray(new LinearRing[numHoles]); LinearRing shell = gf.createLinearRing(poly.getExteriorRing().getCoordinates()); poly = gf.createPolygon(shell, holes); } polygons.add(poly); } return polygons; } private Polygon buildPolygon( List<SplitEdge> edgeList, GeometryFactory gf ) { List<Coordinate> coords = new ArrayList<Coordinate>(); Coordinate[] lastCoordinates = null; for( SplitEdge edge : edgeList ) { Coordinate[] coordinates = edge.getCoordinates(); if (lastCoordinates != null) { Coordinate endPoint = lastCoordinates[lastCoordinates.length - 1]; Coordinate startPoint = coordinates[0]; if (!endPoint.equals2D(startPoint)) { coordinates = CoordinateArrays.copyDeep(coordinates); CoordinateArrays.reverse(coordinates); } } lastCoordinates = coordinates; for( int i = 0; i < coordinates.length; i++ ) { Coordinate coord = coordinates[i]; coords.add(coord); } } Coordinate[] shellCoords = new Coordinate[coords.size()]; coords.toArray(shellCoords); shellCoords = CoordinateArrays.removeRepeatedPoints(shellCoords); LinearRing shell = gf.createLinearRing(shellCoords); Polygon poly = gf.createPolygon(shell, (LinearRing[]) null); return poly; } /** * Builds a list of rings from the graph's edges * * @param graph * @return */ @SuppressWarnings("unchecked") private List<List<SplitEdge>> findRings( SplitGraph graph ) { final List<List<SplitEdge>> rings = new ArrayList<List<SplitEdge>>(); DirectedEdge startEdge; // build each ring starting with the first edge belonging to the // shell found while( (startEdge = findShellEdge(graph)) != null ) { List<SplitEdge> ring = buildRing(graph, startEdge); rings.add(ring); } return rings; } private List<SplitEdge> buildRing( final SplitGraph graph, final DirectedEdge startEdge ) { // System.out.println("building ring edge list..."); final List<SplitEdge> ring = new ArrayList<SplitEdge>(); // follow this tessellation direction while possible, // switch to the opposite when not, and continue with // the same direction while possible. // Start travelling clockwise, as we start with a shell edge, // which is in clockwise order final int direction = CGAlgorithms.COUNTERCLOCKWISE; DirectedEdge currentDirectedEdge = startEdge; DirectedEdge nextDirectedEdge = null; while( nextDirectedEdge != startEdge ) { SplitEdge edge = (SplitEdge) currentDirectedEdge.getEdge(); // System.out.println("adding " + edge); if (ring.contains(edge)) { throw new IllegalStateException("trying to add edge twice: " + edge); } ring.add(edge); DirectedEdge sym = currentDirectedEdge.getSym(); SplitGraphNode endNode = (SplitGraphNode) sym.getNode(); SplitEdgeStar nodeEdges = (SplitEdgeStar) endNode.getEdges(); nextDirectedEdge = nodeEdges.findClosestEdgeInDirection(sym, direction); assert nextDirectedEdge != null; currentDirectedEdge = nextDirectedEdge; } removeUnneededEdges(graph, ring); return ring; } /** * Removes from <code>graph</code> the edges in <code>ring</code> that are no more * needed * * @param graph * @param ring */ private void removeUnneededEdges( final SplitGraph graph, final List<SplitEdge> ring ) { for( SplitEdge edge : ring ) { if (!edge.isInteriorEdge()) { graph.remove(edge); } } for( SplitEdge edge : ring ) { if (edge.isInteriorEdge()) { Node node = graph.find(edge.getCoordinate()); int degree = node.getEdges().getDegree(); if (degree < 2) { graph.remove(edge); } } } } /** * Returns the first edge found that belongs to the shell (not an interior edge, not one of * a hole boundary) * <p> * This method relies on shell edges being labeled {@link Location#EXTERIOR exterior} to the * left and {@link Location#INTERIOR interior} to the right. * </p> * * @param graph * @return the first shell edge found, or <code>null</code> if there are no more shell * edges in <code>graph</code> */ private DirectedEdge findShellEdge( SplitGraph graph ) { Iterator it = graph.getEdgeEnds().iterator(); DirectedEdge firstShellEdge = null; while( it.hasNext() ) { DirectedEdge de = (DirectedEdge) it.next(); SplitEdge edge = (SplitEdge) de.getEdge(); if (edge.isShellEdge()) { firstShellEdge = de; break; } } return firstShellEdge; } } }