// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Rectangle; import java.awt.geom.Area; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.RecursiveTask; import java.util.function.Supplier; import java.util.stream.Collectors; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.Geometry; import org.openstreetmap.josm.tools.Geometry.PolygonIntersection; import org.openstreetmap.josm.tools.MultiMap; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.Utils; /** * Helper class to build multipolygons from multiple ways. * @author viesturs * @since 7392 (rename) * @since 3704 */ public class MultipolygonBuilder { private static final ForkJoinPool THREAD_POOL = Utils.newForkJoinPool("multipolygon_creation.numberOfThreads", "multipolygon-builder-%d", Thread.NORM_PRIORITY); /** * Helper class to avoid unneeded costly intersection calculations. * If the intersection between polygons a and b was calculated we also know * the result of intersection between b and a. The lookup in the hash tables is * much faster than the intersection calculation. */ private static class IntersectionMatrix { private final Map<Pair<JoinedPolygon, JoinedPolygon>, PolygonIntersection> results; IntersectionMatrix(Collection<JoinedPolygon> polygons) { results = new HashMap<>(Utils.hashMapInitialCapacity(polygons.size() * polygons.size())); } /** * Compute the reverse result of the intersection test done by {@code Geometry.polygonIntersection(Area a1, Area a2)} * * @param intersection the intersection result for polygons a1 and a2 (in that order) * @return the intersection result for a2 and a1 */ private PolygonIntersection getReverseIntersectionResult(PolygonIntersection intersection) { switch (intersection) { case FIRST_INSIDE_SECOND: return PolygonIntersection.SECOND_INSIDE_FIRST; case SECOND_INSIDE_FIRST: return PolygonIntersection.FIRST_INSIDE_SECOND; default: return intersection; } } /** * Returns the precomputed intersection between two polygons if known. Otherwise perform {@code computation}. * * @param a1 first polygon * @param a2 second polygon * @param computation the computation to perform when intersection is unknown * @return the intersection between two polygons * @see Map#computeIfAbsent */ PolygonIntersection computeIfAbsent(JoinedPolygon a1, JoinedPolygon a2, Supplier<PolygonIntersection> computation) { PolygonIntersection intersection = results.get(Pair.create(a1, a2)); if (intersection == null) { intersection = computation.get(); synchronized (results) { results.put(Pair.create(a1, a2), intersection); results.put(Pair.create(a2, a1), getReverseIntersectionResult(intersection)); } } return intersection; } } /** * Represents one polygon that consists of multiple ways. */ public static class JoinedPolygon { public final List<Way> ways; public final List<Boolean> reversed; public final List<Node> nodes; public final Area area; public final Rectangle bounds; /** * Constructs a new {@code JoinedPolygon} from given list of ways. * @param ways The ways used to build joined polygon * @param reversed list of reversed states */ public JoinedPolygon(List<Way> ways, List<Boolean> reversed) { this.ways = ways; this.reversed = reversed; this.nodes = this.getNodes(); this.area = Geometry.getArea(nodes); this.bounds = area.getBounds(); } /** * Creates a polygon from single way. * @param way the way to form the polygon */ public JoinedPolygon(Way way) { this(Collections.singletonList(way), Collections.singletonList(Boolean.FALSE)); } /** * Builds a list of nodes for this polygon. First node is not duplicated as last node. * @return list of nodes */ public List<Node> getNodes() { List<Node> nodes = new ArrayList<>(); for (int waypos = 0; waypos < this.ways.size(); waypos++) { Way way = this.ways.get(waypos); boolean reversed = this.reversed.get(waypos).booleanValue(); if (!reversed) { for (int pos = 0; pos < way.getNodesCount() - 1; pos++) { nodes.add(way.getNode(pos)); } } else { for (int pos = way.getNodesCount() - 1; pos > 0; pos--) { nodes.add(way.getNode(pos)); } } } return nodes; } } /** * Helper storage class for finding findOuterWays */ static class PolygonLevel { public final int level; // nesting level, even for outer, odd for inner polygons. public final JoinedPolygon outerWay; public List<JoinedPolygon> innerWays; PolygonLevel(JoinedPolygon pol, int level) { this.outerWay = pol; this.level = level; this.innerWays = new ArrayList<>(); } } /** List of outer ways **/ public final List<JoinedPolygon> outerWays; /** List of inner ways **/ public final List<JoinedPolygon> innerWays; /** * Constructs a new {@code MultipolygonBuilder} initialized with given ways. * @param outerWays The outer ways * @param innerWays The inner ways */ public MultipolygonBuilder(List<JoinedPolygon> outerWays, List<JoinedPolygon> innerWays) { this.outerWays = outerWays; this.innerWays = innerWays; } /** * Constructs a new empty {@code MultipolygonBuilder}. */ public MultipolygonBuilder() { this.outerWays = new ArrayList<>(0); this.innerWays = new ArrayList<>(0); } /** * Splits ways into inner and outer JoinedWays. Sets {@link #innerWays} and {@link #outerWays} to the result. * TODO: Currently cannot process touching polygons. See code in JoinAreasAction. * @param ways ways to analyze * @return error description if the ways cannot be split, {@code null} if all fine. */ public String makeFromWays(Collection<Way> ways) { try { List<JoinedPolygon> joinedWays = joinWays(ways); //analyze witch way is inside witch outside. return makeFromPolygons(joinedWays); } catch (JoinedPolygonCreationException ex) { Main.debug(ex); return ex.getMessage(); } } /** * An exception indicating an error while joining ways to multipolygon rings. */ public static class JoinedPolygonCreationException extends RuntimeException { /** * Constructs a new {@code JoinedPolygonCreationException}. * @param message the detail message. The detail message is saved for * later retrieval by the {@link #getMessage()} method */ public JoinedPolygonCreationException(String message) { super(message); } } /** * Joins the given {@code multipolygon} to a pair of outer and inner multipolygon rings. * * @param multipolygon the multipolygon to join. * @return a pair of outer and inner multipolygon rings. * @throws JoinedPolygonCreationException if the creation fails. */ public static Pair<List<JoinedPolygon>, List<JoinedPolygon>> joinWays(Relation multipolygon) { CheckParameterUtil.ensureThat(multipolygon.isMultipolygon(), "multipolygon.isMultipolygon"); final Map<String, Set<Way>> members = multipolygon.getMembers().stream() .filter(RelationMember::isWay) .collect(Collectors.groupingBy(RelationMember::getRole, Collectors.mapping(RelationMember::getWay, Collectors.toSet()))); final List<JoinedPolygon> outerRings = joinWays(members.getOrDefault("outer", Collections.emptySet())); final List<JoinedPolygon> innerRings = joinWays(members.getOrDefault("inner", Collections.emptySet())); return Pair.create(outerRings, innerRings); } /** * Joins the given {@code ways} to multipolygon rings. * @param ways the ways to join. * @return a list of multipolygon rings. * @throws JoinedPolygonCreationException if the creation fails. */ public static List<JoinedPolygon> joinWays(Collection<Way> ways) { List<JoinedPolygon> joinedWays = new ArrayList<>(); //collect ways connecting to each node. MultiMap<Node, Way> nodesWithConnectedWays = new MultiMap<>(); Set<Way> usedWays = new HashSet<>(); for (Way w: ways) { if (w.getNodesCount() < 2) { throw new JoinedPolygonCreationException(tr("Cannot add a way with only {0} nodes.", w.getNodesCount())); } if (w.isClosed()) { //closed way, add as is. JoinedPolygon jw = new JoinedPolygon(w); joinedWays.add(jw); usedWays.add(w); } else { nodesWithConnectedWays.put(w.lastNode(), w); nodesWithConnectedWays.put(w.firstNode(), w); } } //process unclosed ways for (Way startWay: ways) { if (usedWays.contains(startWay)) { continue; } Node startNode = startWay.firstNode(); List<Way> collectedWays = new ArrayList<>(); List<Boolean> collectedWaysReverse = new ArrayList<>(); Way curWay = startWay; Node prevNode = startNode; //find polygon ways while (true) { boolean curWayReverse = prevNode == curWay.lastNode(); Node nextNode = curWayReverse ? curWay.firstNode() : curWay.lastNode(); //add cur way to the list collectedWays.add(curWay); collectedWaysReverse.add(Boolean.valueOf(curWayReverse)); if (nextNode == startNode) { //way finished break; } //find next way Collection<Way> adjacentWays = nodesWithConnectedWays.get(nextNode); if (adjacentWays.size() != 2) { throw new JoinedPolygonCreationException(tr("Each node must connect exactly 2 ways")); } Way nextWay = null; for (Way way: adjacentWays) { if (way != curWay) { nextWay = way; } } //move to the next way curWay = nextWay; prevNode = nextNode; } usedWays.addAll(collectedWays); joinedWays.add(new JoinedPolygon(collectedWays, collectedWaysReverse)); } return joinedWays; } /** * This method analyzes which ways are inner and which outer. Sets {@link #innerWays} and {@link #outerWays} to the result. * @param polygons polygons to analyze * @return error description if the ways cannot be split, {@code null} if all fine. */ private String makeFromPolygons(List<JoinedPolygon> polygons) { List<PolygonLevel> list = findOuterWaysMultiThread(polygons); if (list == null) { return tr("There is an intersection between ways."); } this.outerWays.clear(); this.innerWays.clear(); //take every other level for (PolygonLevel pol : list) { if (pol.level % 2 == 0) { this.outerWays.add(pol.outerWay); } else { this.innerWays.add(pol.outerWay); } } return null; } private static Pair<Boolean, List<JoinedPolygon>> findInnerWaysCandidates(IntersectionMatrix cache, JoinedPolygon outerWay, Collection<JoinedPolygon> boundaryWays) { boolean outerGood = true; List<JoinedPolygon> innerCandidates = new ArrayList<>(); for (JoinedPolygon innerWay : boundaryWays) { if (innerWay == outerWay) { continue; } // Preliminary computation on bounds. If bounds do not intersect, no need to do a costly area intersection if (outerWay.bounds.intersects(innerWay.bounds)) { // Bounds intersection, let's see in detail final PolygonIntersection intersection = cache.computeIfAbsent(outerWay, innerWay, () -> Geometry.polygonIntersection(outerWay.area, innerWay.area)); if (intersection == PolygonIntersection.FIRST_INSIDE_SECOND) { outerGood = false; // outer is inside another polygon break; } else if (intersection == PolygonIntersection.SECOND_INSIDE_FIRST) { innerCandidates.add(innerWay); } else if (intersection == PolygonIntersection.CROSSING) { // ways intersect return null; } } } return new Pair<>(outerGood, innerCandidates); } /** * Collects outer way and corresponding inner ways from all boundaries. * @param boundaryWays boundary ways * @return the outermostWay, or {@code null} if intersection found. */ private static List<PolygonLevel> findOuterWaysMultiThread(List<JoinedPolygon> boundaryWays) { final IntersectionMatrix cache = new IntersectionMatrix(boundaryWays); return THREAD_POOL.invoke(new Worker(cache, boundaryWays, 0, boundaryWays.size(), new ArrayList<PolygonLevel>(), Math.max(32, boundaryWays.size() / THREAD_POOL.getParallelism() / 3))); } private static class Worker extends RecursiveTask<List<PolygonLevel>> { // Needed for Findbugs / Coverity because parent class is serializable private static final long serialVersionUID = 1L; private final transient List<JoinedPolygon> input; private final int from; private final int to; private final transient List<PolygonLevel> output; private final int directExecutionTaskSize; private final IntersectionMatrix cache; Worker(IntersectionMatrix cache, List<JoinedPolygon> input, int from, int to, List<PolygonLevel> output, int directExecutionTaskSize) { this.cache = cache; this.input = input; this.from = from; this.to = to; this.output = output; this.directExecutionTaskSize = directExecutionTaskSize; } /** * Collects outer way and corresponding inner ways from all boundaries. * @param level nesting level * @param cache cache that tracks previously calculated results * @param boundaryWays boundary ways * @return the outermostWay, or {@code null} if intersection found. */ private static List<PolygonLevel> findOuterWaysRecursive(int level, IntersectionMatrix cache, List<JoinedPolygon> boundaryWays) { final List<PolygonLevel> result = new ArrayList<>(); for (JoinedPolygon outerWay : boundaryWays) { if (processOuterWay(level, cache, boundaryWays, result, outerWay) == null) { return null; } } return result; } private static List<PolygonLevel> processOuterWay(int level, IntersectionMatrix cache, List<JoinedPolygon> boundaryWays, final List<PolygonLevel> result, JoinedPolygon outerWay) { Pair<Boolean, List<JoinedPolygon>> p = findInnerWaysCandidates(cache, outerWay, boundaryWays); if (p == null) { // ways intersect return null; } if (p.a) { //add new outer polygon PolygonLevel pol = new PolygonLevel(outerWay, level); //process inner ways if (!p.b.isEmpty()) { List<PolygonLevel> innerList = findOuterWaysRecursive(level + 1, cache, p.b); if (innerList == null) { return null; //intersection found } result.addAll(innerList); for (PolygonLevel pl : innerList) { if (pl.level == level + 1) { pol.innerWays.add(pl.outerWay); } } } result.add(pol); } return result; } @Override protected List<PolygonLevel> compute() { if (to - from <= directExecutionTaskSize) { return computeDirectly(); } else { final Collection<ForkJoinTask<List<PolygonLevel>>> tasks = new ArrayList<>(); for (int fromIndex = from; fromIndex < to; fromIndex += directExecutionTaskSize) { tasks.add(new Worker(cache, input, fromIndex, Math.min(fromIndex + directExecutionTaskSize, to), new ArrayList<PolygonLevel>(), directExecutionTaskSize)); } for (ForkJoinTask<List<PolygonLevel>> task : ForkJoinTask.invokeAll(tasks)) { List<PolygonLevel> res = task.join(); if (res == null) { return null; } output.addAll(res); } return output; } } List<PolygonLevel> computeDirectly() { for (int i = from; i < to; i++) { if (processOuterWay(0, cache, input, output, input.get(i)) == null) { return null; } } return output; } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { // Needed for Findbugs / Coverity because parent class is serializable ois.defaultReadObject(); } private void writeObject(ObjectOutputStream oos) throws IOException { // Needed for Findbugs / Coverity because parent class is serializable oos.defaultWriteObject(); } } }