/* * This file is part of the Trickl Open Source Libraries. * * Trickl Open Source Libraries - http://open.trickl.com/ * * Copyright (C) 2011 Tim Gee. * * Trickl Open Source Libraries are 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 3 of the License, or * (at your option) any later version. * * Trickl Open Source Libraries are 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 project. If not, see <http://www.gnu.org/licenses/>. */ package com.trickl.graph.planar; import com.trickl.graph.CopyEdgeFactory; import com.trickl.graph.CopyVertexFactory; import com.trickl.graph.EdgeVisitor; import com.trickl.graph.edges.DirectedEdge; import com.vividsolutions.jts.algorithm.Angle; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.LineSegment; import com.vividsolutions.jts.geom.LinearRing; import java.util.*; import java.util.stream.Collectors; import org.jgrapht.Graphs; import org.jgrapht.VertexFactory; public final class PlanarGraphs { private PlanarGraphs() { } static public <V, E> List<V> getBoundaryVertices(PlanarGraph<V, E> graph) { DirectedEdge<V> boundary = graph.getBoundary(); if (boundary == null) return new ArrayList<>(); return PlanarGraphs.getVerticesOnFace(graph, boundary.getSource(), boundary.getTarget()); } static public <V, E> void boundaryHops(PlanarGraph<V, E> graph, Map<V, Integer> hops) { Set<E> boundary = getBoundaryEdges(graph); boundary.stream().map((edge) -> { hops.put(graph.getEdgeSource(edge), 0); return edge; }).forEach((edge) -> { hops.put(graph.getEdgeTarget(edge), 0); }); Queue<V> vertexQueue = new LinkedList<>(); vertexQueue.addAll(hops.keySet()); while (!vertexQueue.isEmpty()) { V vertex = vertexQueue.poll(); getConnectedVertices(graph, vertex).stream().filter((neighbour) -> (!hops.containsKey(neighbour))).map((neighbour) -> { hops.put(neighbour, hops.get(vertex) + 1); return neighbour; }).forEach((neighbour) -> { vertexQueue.add(neighbour); }); } } static public <V1, E1, V2, E2> Map<V2, Set<DirectedEdge<V1>>> delaunayToVoronoi(PlanarGraph<V1, E1> delaunay, PlanarLayout<V1> delaunayLocations, PlanarGraph<V2, E2> voronoi, PlanarLayoutStore<V2> voronoiLayout, LinearRing boundary, VertexFactory<V2> vertexFactory) { DelaunayVoronoiVisitor<V1, E1, V2, E2> delaunayVoronoiVisitor = new DelaunayVoronoiVisitor( delaunay, delaunayLocations, voronoi, voronoiLayout, boundary, vertexFactory); PlanarFaceTraversal<V1, E1> planarFaceTraversal = new CanonicalPlanarFaceTraversal<>(delaunay); planarFaceTraversal.traverse(delaunayVoronoiVisitor); return delaunayVoronoiVisitor.getVertexToFaceMap(); } static public <V1, E1, V2, E2> Map<V2, Set<DirectedEdge<V1>>> dualGraph(PlanarGraph<V1, E1> graph, PlanarGraph<V2, E2> dualGraph, VertexFactory<V2> vertexFactory) { DualGraphVisitor<V1, E1, V2, E2> dualGraphVisitor = new DualGraphVisitor(graph, dualGraph, vertexFactory); PlanarFaceTraversal<V1, E1> planarFaceTraversal = new CanonicalPlanarFaceTraversal<>(graph); planarFaceTraversal.traverse(dualGraphVisitor); return dualGraphVisitor.getVertexToFaceMap(); } static public <V, E> void split(PlanarGraph<V, E> graph, E edge, V vertex) { V target = graph.getEdgeTarget(edge); V source = graph.getEdgeSource(edge); V before = graph.getPrevVertex(source, target); V after = graph.getNextVertex(source, target); graph.removeEdge(edge); graph.addEdge(source, vertex, before, null); graph.addEdge(vertex, target, source, after); } static public <V1, E1, V2, E2> Map<V1, V2> aggregate(PlanarGraph<V1, E1> source, PlanarGraph<V2, E2> target, Map<V1, Integer> aggregateGroups, CopyVertexFactory<V2, V1> vertexFactory, CopyEdgeFactory<V2, E2, E1> edgeFactory) { PlanarCopyGraphVisitor<V1, E1, V2, E2> copyGraphVisitor = new PlanarCopyGraphVisitor( source, target, vertexFactory, edgeFactory); copyGraphVisitor.setAggregationGroups(aggregateGroups); BreadthFirstPlanarFaceTraversal<V1, E1> planarFaceTraversal = new BreadthFirstPlanarFaceTraversal<>(source); planarFaceTraversal.traverse(copyGraphVisitor); return copyGraphVisitor.getVertexMap(); } static public <V1, E1, V2, E2> Map<V1, V2> aggregate(PlanarGraph<V1, E1> source, PlanarGraph<V2, E2> target, Map<V1, Integer> aggregateGroups) { return aggregate(source, target, aggregateGroups, null, null); } static public <V1, E1, V2, E2> Map<V1, V2> copy(PlanarGraph<V1, E1> source, PlanarGraph<V2, E2> target, CopyVertexFactory<V2, V1> vertexFactory, CopyEdgeFactory<V2, E2, E1> edgeFactory) { return aggregate(source, target, null, vertexFactory, edgeFactory); } static public <V1, E1, V2, E2> Map<V1, V2> subgraph(PlanarGraph<V1, E1> graph, PlanarGraph<V2, E2> subgraph, Set<V1> vertices, V1 boundarySource, V1 boundaryTarget, CopyVertexFactory<V2, V1> vertexFactory, CopyEdgeFactory<V2, E2, E1> edgeFactory) { PlanarGraph<V1, E1> subgraphProxy = new PlanarSubGraph<>(graph, vertices, boundarySource, boundaryTarget); return copy(subgraphProxy, subgraph, vertexFactory, edgeFactory); } static public <V1, E1, V2, E2> Map<V1, V2> subgraph(PlanarGraph<V1, E1> graph, PlanarGraph<V2, E2> subgraph, Set<V1> vertices, V1 boundarySource, V1 boundaryTarget) { return subgraph(graph, subgraph, vertices, boundarySource, boundaryTarget, null, null); } static public <V, E> Set<E> getEdgesInsideBoundary(PlanarGraph<V, E> graph, List<V> boundary) { // Search for edges within the boundary Stack<DirectedEdge<V>> edgeStack = new Stack<>(); List<E> boundaryEdges = new LinkedList<>(); for (int i = 0; i < boundary.size(); ++i) { V boundaryCurrent = boundary.get(i); V boundaryNext = boundary.get((i + 1) % boundary.size()); if (!graph.containsEdge(boundaryCurrent, boundaryNext)) { throw new NoSuchElementException("Boundary does not define an edge cycle in the graph."); } boundaryEdges.add(graph.getEdge(boundaryCurrent, boundaryNext)); edgeStack.add(new DirectedEdge(boundaryCurrent, boundaryNext)); } Set<E> edges = new HashSet<>(boundaryEdges); while (!edgeStack.isEmpty()) { DirectedEdge<V> edge = edgeStack.pop(); V nextVertex = graph.getNextVertex(edge.getSource(), edge.getTarget()); E nextEdge = graph.getEdge(nextVertex, edge.getTarget()); if (!edges.contains(nextEdge)) { edgeStack.add(new DirectedEdge<>(nextVertex, edge.getTarget())); edges.add(nextEdge); } } // Don't include boundary edges edges.removeAll(boundaryEdges); return edges; } /** * Boundary should have the same chirality as inner faces (so if an internal face * is described counter-clockwise, the boundary is also counter-clockwise). * @param <V> * @param <E> * @param graph * @param boundary * @param removeEdgeVisitor */ static public <V, E> void removeEdgesInsideBoundary(PlanarGraph<V, E> graph, List<V> boundary, EdgeVisitor<E> removeEdgeVisitor) { getEdgesInsideBoundary(graph, boundary).stream().map((edge) -> { if (removeEdgeVisitor != null) { removeEdgeVisitor.onEdge(edge); } return edge; }).forEach((edge) -> { graph.removeEdge(edge); }); } static public <V, E> void triangulateFace(PlanarGraph<V, E> graph, V source, V target, EdgeVisitor<E> addEdgeVisitor) { V sourceNext = target; V current = graph.getNextVertex(source, sourceNext); V next = graph.getNextVertex(sourceNext, current); while (!next.equals(source)) { if (graph.containsEdge(source, current)) { E edge = graph.addEdge(next, sourceNext, current, null); if (addEdgeVisitor != null) { addEdgeVisitor.onEdge(edge); } } else { E edge = graph.addEdge(current, source, sourceNext, null); if (addEdgeVisitor != null) { addEdgeVisitor.onEdge(edge); } sourceNext = current; } current = next; next = graph.getNextVertex(sourceNext, current); } } static public <V, E> void fanTransform(PlanarGraph<V, E> graph, List<V> boundary, EdgeVisitor<E> removeEdgeVisitor, EdgeVisitor<E> addEdgeVisitor) { PlanarGraphs.removeEdgesInsideBoundary(graph, boundary, removeEdgeVisitor); PlanarGraphs.triangulateFace(graph, boundary.get(0), boundary.get(1), addEdgeVisitor); } static public <V, E> List<V> getConnectedVertices(PlanarGraph<V, E> graph, V vertex) { return getConnectedVertices(graph, vertex, null); } static public <V, E> List<V> getConnectedVertices(PlanarGraph<V, E> graph, V vertex, V startVertex) { return getConnectedVertices(graph, vertex, startVertex, startVertex); } /** * @param <V> * @param <E> * @param graph * @param vertex * @param startVertex The first vertex, included in output. * @param endVertex The last vertex , included in output. * @return A list of all the vertices connected to the supplied vertex. Given in inward face order. * TODO: Handle self-loops, which are not returned at the moment. They probably should be? (self-connected) * , perhaps should be returned first regardless of start and end?... */ static public <V, E> List<V> getConnectedVertices(PlanarGraph<V, E> graph, V vertex, V startVertex, V endVertex) { if (graph == null || vertex == null) { throw new NullPointerException(); } Set<E> edges = graph.edgesOf(vertex); LinkedList<V> frontVertices = new LinkedList<>(); LinkedList<V> backVertices = new LinkedList<>(); // Note edge discovery order will be in outward face order, so // we need to work backwards boolean afterEnd = (endVertex == null); boolean beforeStart = true; for (E edge : edges) { V target = Graphs.getOppositeVertex(graph, edge, vertex); LinkedList<V> vertices = beforeStart ? frontVertices : backVertices; if (beforeStart || afterEnd) { vertices.addFirst(target); } if (beforeStart && (startVertex == null || target.equals(startVertex))) { beforeStart = false; afterEnd = false; } if (!afterEnd && (endVertex == null || target.equals(endVertex))) { afterEnd = true; if (beforeStart) { // Still waiting to encounter the start vertex frontVertices.clear(); } } } frontVertices.addAll(backVertices); return frontVertices; } static public <V, E> List<V> getVerticesOnFace(PlanarGraph<V, E> graph, V startVertex, V targetVertex) { return getVerticesOnFace(graph, startVertex, targetVertex, null); } static public <V, E> List<V> getVerticesOnFace(PlanarGraph<V, E> graph, V startVertex, V targetVertex, V endVertex) { if (graph == null) { throw new NullPointerException(); } List<V> vertices = new LinkedList<>(); if (startVertex != null) { V boundaryNext = targetVertex; V boundaryCurrent = startVertex; if (endVertex == null) endVertex = startVertex; do { vertices.add(boundaryCurrent); V boundaryNextNext = graph.getNextVertex(boundaryCurrent, boundaryNext); boundaryCurrent = boundaryNext; boundaryNext = boundaryNextNext; } while (!boundaryCurrent.equals(endVertex)); } return vertices; } static public <V, E> boolean isEdgeBoundary(PlanarGraph<V, E> graph, E edge) { V source = graph.getEdgeSource(edge); V target = graph.getEdgeTarget(edge); return graph.isBoundary(source, target) || (!com.trickl.graph.Graphs.isEdgeDirected(graph, edge) && graph.isBoundary(target, source)); } static public <V, E> boolean isVertexBoundary(PlanarGraph<V, E> graph, V vertex) { if (graph.edgesOf(vertex).stream().anyMatch((edge) -> (isEdgeBoundary(graph, edge)))) { return true; } return false; } static public <V, E> Set<E> getBoundaryEdges(PlanarGraph<V, E> graph) { Set<E> edges = new LinkedHashSet<>(); V priorVertex = null; V firstVertex = null; for (V vertex : getBoundaryVertices(graph)) { if (priorVertex != null) { edges.add(graph.getEdge(priorVertex, vertex)); } else { firstVertex = vertex; } priorVertex = vertex; } if (priorVertex != null) { edges.add(graph.getEdge(priorVertex, firstVertex)); } return edges; } static public <V, E> Set<E> getBoundaryEdges(PlanarGraph<V, E> graph, V vertex) { Set<E> edges = new HashSet<>(); graph.edgesOf(vertex).stream().filter((edge) -> (isEdgeBoundary(graph, edge))).forEach((edge) -> { edges.add(edge); }); return edges; } static public <V, E> V getNextVertexOnBoundary(PlanarGraph<V, E> graph, V vertex) { if (graph.edgeSet().size() < 2 && graph.containsVertex(vertex)) { // Special case where the graph has less than two edges return vertex; } for (E edge : graph.edgesOf(vertex)) { V target = Graphs.getOppositeVertex(graph, edge, vertex); if (graph.isBoundary(vertex, target)) { return target; } } throw new NoSuchElementException("Vertex not on boundary."); } static public <V, E> V getPrevVertexOnBoundary(PlanarGraph<V, E> graph, V vertex) { if (graph.edgesOf(vertex).isEmpty() && graph.containsVertex(vertex)) { // Special case where the vertex is isolated return vertex; } for (E edge : graph.edgesOf(vertex)) { V target = Graphs.getOppositeVertex(graph, edge, vertex); if (graph.isBoundary(target, vertex)) { return target; } } throw new NoSuchElementException("Vertex not on boundary."); } static public <V, E> List<V> getInnerBoundary(PlanarGraph<V, E> graph, List<V> outerBoundary) { List<V> innerBoundary = new LinkedList<>(); Collections.reverse(outerBoundary); for (int i = 0; i < outerBoundary.size(); ++i) { V prevOuterVertex = outerBoundary.get(i); V outerVertex = outerBoundary.get((i + 1) % outerBoundary.size()); V nextOuterVertex = outerBoundary.get((i + 2) % outerBoundary.size()); V nextInnerVertex = innerBoundary.isEmpty() ? prevOuterVertex : innerBoundary.get(innerBoundary.size() - 1); List<V> perimeter = PlanarGraphs.getConnectedVertices(graph, outerVertex, nextInnerVertex, nextOuterVertex); if (!perimeter.isEmpty()) { innerBoundary.addAll(perimeter.subList(1, perimeter.size())); } } return innerBoundary.isEmpty() ? innerBoundary : innerBoundary.subList(1, innerBoundary.size()); } static public <V, E, F> F getFace(PlanarFaceGraph<V, E, F> graph, DirectedEdge<V> edge) { return graph.getFace(edge.getSource(), edge.getTarget()); } static public <V, E> boolean isBoundaryConvex(PlanarGraph<V, E> graph, PlanarLayout<V> layout) { boolean isConvex = true; List<V> boundaryVertices = PlanarGraphs.getBoundaryVertices(graph); Coordinate[] boundaryCoordinates = new Coordinate[boundaryVertices.size() + 1]; for (int i = 0; i < boundaryVertices.size(); ++i) { boundaryCoordinates[i] = layout.getCoordinate(boundaryVertices.get(i)); } int priorOrientation = Angle.NONE; for (int i = 0; i < boundaryVertices.size(); ++i) { Coordinate currentCoord = boundaryCoordinates[i]; Coordinate nextCoord = boundaryCoordinates[(i + 1) % boundaryVertices.size()]; Coordinate prevCoord = boundaryCoordinates[(i + boundaryVertices.size() - 1) % boundaryVertices.size()]; int orientation = Angle.getTurn(Angle.angle(prevCoord, currentCoord), Angle.angle(currentCoord, nextCoord)); if (priorOrientation != Angle.NONE) { if (orientation != priorOrientation) { // The boundary should be defined in a consistent orientation return false; } priorOrientation = orientation; } } return isConvex; } static public <V, E> V getNextVertex(PlanarGraph<V, E> graph, V source, V target) { Set<E> edges = graph.edgesOf(target); boolean foundEdge = false; Iterator<E> itr = edges.iterator(); while (itr.hasNext()) { E edge = itr.next(); V opposite = Graphs.getOppositeVertex(graph, edge, target); if (opposite.equals(source)) { foundEdge = true; break; } } if (!foundEdge) { throw new NoSuchElementException("Graph does not contain supplied edge."); } if (!itr.hasNext()) { itr = edges.iterator(); } E nextEdge = itr.next(); return Graphs.getOppositeVertex(graph, nextEdge, target); } static public <V, E> V getPrevVertex(PlanarGraph<V, E> graph, V source, V target) { Set<E> edges = graph.edgesOf(target); boolean foundEdge = false; E priorEdge = null; Iterator<E> itr = edges.iterator(); while (itr.hasNext()) { E edge = itr.next(); V opposite = Graphs.getOppositeVertex(graph, edge, source); if (opposite.equals(target)) { foundEdge = true; break; } priorEdge = edge; } if (!foundEdge) { throw new NoSuchElementException("Graph does not contain supplied edge."); } if (priorEdge == null) { priorEdge = edges.iterator().next(); } return Graphs.getOppositeVertex(graph, priorEdge, source); } static public <V1, V2, E1, E2> boolean isIsomorphic(PlanarGraph<V1, E1> dataGraph, PlanarGraph<V2, E2> modelGraph) { boolean isIsomorphic = true; // First some simple checks if (dataGraph.vertexSet().size() != modelGraph.vertexSet().size() || dataGraph.edgeSet().size() != modelGraph.edgeSet().size()) { isIsomorphic = false; } // Use KuklukHolderCook codes to test for isomorphism KuklukHolderCookCodeGenerator codeGenerator = new KuklukHolderCookCodeGenerator(); if (!codeGenerator.getCode(dataGraph).equals(codeGenerator.getCode(modelGraph))) { isIsomorphic = false; } return isIsomorphic; } static public <V> int getNearestInterceptingLineSegment(LineSegment halfLine, List<V> boundaryVertices, PlanarLayout<V> layout) { // Check each segment in the boundary for intersection with the // bisector // TODO: O(boundary size), can we do this more efficiently? double minDistance = Double.POSITIVE_INFINITY; int segmentIndex = -1; for (int itr = 0; itr < boundaryVertices.size(); ++itr) { LineSegment boundarySegment = getLineSegment(itr, boundaryVertices, layout); Coordinate intersection = boundarySegment.lineIntersection(halfLine); if (intersection != null) { double boundaryProjectionFactor = boundarySegment.projectionFactor(intersection); if (boundaryProjectionFactor >= 0 && boundaryProjectionFactor < 1) { // Find the nearest boundary intersection (allows a concave boundary) double distance = halfLine.p0.distance(intersection); if (distance < minDistance) { minDistance = distance; segmentIndex = itr; } } } } return segmentIndex; } static public <V> LineSegment getLineSegment(int segmentIndex, List<V> vertices, PlanarLayout<V> layout) { int nextItr = (segmentIndex + 1) % vertices.size(); V boundarySource = vertices.get(segmentIndex); V boundaryTarget = vertices.get(nextItr); return new LineSegment( layout.getCoordinate(boundarySource), layout.getCoordinate(boundaryTarget)); } static public <V, E> List<V> getInnermostVertices(PlanarGraph<V, E> graph) { Map<V, Integer> boundaryHops = new HashMap<>(); PlanarGraphs.boundaryHops(graph, boundaryHops); int maxHops = Collections.max(boundaryHops.values()); List<V> innermostVertices = boundaryHops.entrySet().stream() .filter(entry -> entry.getValue() == maxHops) .map(entry -> entry.getKey()) .collect(Collectors.toList()); return innermostVertices; } }