From 0a8669edae90327783bd14c698fc1046c561aec2 Mon Sep 17 00:00:00 2001 From: Sander Deryckere Date: Tue, 11 Sep 2012 21:19:38 +0200 Subject: [PATCH 1/2] Split concept of multipolygon in two classes: the concept of a ring and the concept of a multipolygon which has rings. --- .../src/net/osmand/data/Multipolygon.java | 430 +++++++-------- .../src/net/osmand/data/Ring.java | 491 ++++++++++++++++++ 2 files changed, 715 insertions(+), 206 deletions(-) create mode 100644 DataExtractionOSM/src/net/osmand/data/Ring.java diff --git a/DataExtractionOSM/src/net/osmand/data/Multipolygon.java b/DataExtractionOSM/src/net/osmand/data/Multipolygon.java index 98a9dabca5..c62986793c 100644 --- a/DataExtractionOSM/src/net/osmand/data/Multipolygon.java +++ b/DataExtractionOSM/src/net/osmand/data/Multipolygon.java @@ -2,10 +2,9 @@ package net.osmand.data; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.IdentityHashMap; import java.util.List; -import java.util.Stack; +import java.util.SortedSet; +import java.util.TreeSet; import net.osmand.osm.LatLon; import net.osmand.osm.MapUtils; @@ -23,180 +22,124 @@ import net.osmand.osm.Way; * @author Pavol Zibrita */ public class Multipolygon { + + /** + * cache with the ways grouped per Ring + */ + private SortedSet innerRings, outerRings; - protected List closedOuterWays; - protected List outerWays; - protected List closedInnerWays; - protected List innerWays; + /** + * ways added by the user + */ + private List outerWays, innerWays; - protected IdentityHashMap> outerInnerMapping; - - private void addNewPolygonPart(List polygons, List closedPolygons, Way newPoly) { - if (!newPoly.getNodes().isEmpty()) { - if (isClosed(newPoly)) { - closedPolygons.add(newPoly); //if closed, put directly to closed polygons - } else if (polygons.isEmpty()) { - polygons.add(newPoly); //if open, and first, put to polygons.. - } else { - // now we try to merge the ways to form bigger polygons - Stack wayStack = new Stack(); - wayStack.push(newPoly); - addAndMergePolygon(polygons, closedPolygons, wayStack); - } - //reset the mapping - outerInnerMapping = null; - } //else do nothing + /** + * create a multipolygon with these outer and inner rings + * the rings have to be well formed or data inconsistency will happen + * @param outerRings the collection of outer rings + * @param innerRings the collection of inner rings + */ + public Multipolygon(SortedSet outerRings, SortedSet innerRings) { + this(); + this.outerRings = outerRings; + this.innerRings = innerRings; + for (Ring r : outerRings) { + outerWays.addAll(r.getWays()); + } + + for (Ring r : innerRings) { + innerWays.addAll(r.getWays()); + } } - private boolean isClosed(Way newPoly) { - List ns = newPoly.getNodes(); - return !ns.isEmpty() && ns.get(0).getId() == ns.get(ns.size()-1).getId(); + /** + * Create a multipolygon with initialized outer and inner ways + * @param outers a list of outer ways + * @param inners a list of inner ways + */ + public Multipolygon(List outers, List inners) { + this(); + outerWays.addAll(outers); + innerWays.addAll(inners); + } + + /** + * create a new empty multipolygon + */ + public Multipolygon(){ + outerWays = new ArrayList (); + innerWays = new ArrayList (); + } + + /** + * check if this multipolygon contains a point + * @param point point to check + * @return true if this multipolygon is correct and contains the point + */ + public boolean containsPoint(LatLon point) { + + return containsPoint(point.getLatitude(), point.getLongitude()); + } - private void addAndMergePolygon(List polygons, List closedPolygons, Stack workStack) { - while (!workStack.isEmpty()) { - Way changedWay = workStack.pop(); - List nodes = changedWay.getNodes(); - if (nodes.isEmpty()) { - //don't bother with it! - continue; - } - if (isClosed(changedWay)) { - polygons.remove(changedWay); - closedPolygons.add(changedWay); - continue; + /** + * check if this multipolygon contains a point + * @param latitude lat to check + * @param longitude lon to check + * @return true if this multipolygon is correct and contains the point + */ + public boolean containsPoint(double latitude, double longitude){ + + + TreeSet outers = new TreeSet(); + TreeSet inners = new TreeSet(); + + for (Ring outer : getOuterRings()) { + if (outer.containsPoint(latitude, longitude)) { + outers.add(outer); + } } - Node first = nodes.get(0); - Node last = nodes.get(nodes.size()-1); - for (Way anotherWay : polygons) { - if (anotherWay == changedWay) { - continue; - } - //try to find way, that matches the one ... - if (anotherWay.getNodes().get(0).getId() == first.getId()) { - Collections.reverse(changedWay.getNodes()); - anotherWay.getNodes().addAll(0,changedWay.getNodes()); - workStack.push(anotherWay); - break; - } else if (anotherWay.getNodes().get(0).getId() == last.getId()) { - anotherWay.getNodes().addAll(0,changedWay.getNodes()); - workStack.push(anotherWay); - break; - } else if (anotherWay.getNodes().get(anotherWay.getNodes().size()-1).getId() == first.getId()) { - anotherWay.getNodes().addAll(changedWay.getNodes()); - workStack.push(anotherWay); - break; - } else if (anotherWay.getNodes().get(anotherWay.getNodes().size()-1).getId() == last.getId()) { - Collections.reverse(changedWay.getNodes()); - anotherWay.getNodes().addAll(changedWay.getNodes()); - workStack.push(anotherWay); - break; + for(Ring inner : getInnerRings()) { + if (inner.containsPoint(latitude, longitude)) { + inners.add(inner); } } - //if we could not merge the new polygon, and it is not already there, add it! - if (workStack.isEmpty() && !polygons.contains(changedWay)) { - polygons.add(changedWay); - } else if (!workStack.isEmpty()) { - polygons.remove(changedWay); - } - } + + if(outers.size() == 0) return false; + if(inners.size() == 0) return true; + + Ring smallestOuter = outers.first(); + Ring smallestInner = inners.first(); + + // if the smallest outer is in the smallest inner, the multiPolygon contains the point + + return smallestOuter.isIn(smallestInner); + } - public boolean containsPoint(LatLon point) { - return containsPoint(point.getLatitude(), point.getLongitude()); - } - - public boolean containsPoint(double latitude, double longitude) { - return containsPointInPolygons(closedOuterWays, latitude, longitude) || containsPointInPolygons(outerWays, latitude, longitude); - } - - private boolean containsPointInPolygons(List outerPolygons, double latitude, double longitude) { - if (outerPolygons != null) { - for (Way polygon : outerPolygons) { - List inners = getOuterInnerMapping().get(polygon); - if (polygonContainsPoint(latitude, longitude, polygon, inners)) { - return true; - } - } - } - return false; - } - - private boolean polygonContainsPoint(double latitude, double longitude, - Way polygon, List inners) { - int intersections = 0; - intersections = countIntersections(latitude, longitude, polygon, - intersections); - if (inners != null) { - for (Way w : inners) { - intersections = countIntersections(latitude, longitude, w, - intersections); - } - } - return intersections % 2 == 1; - } - - private int countIntersections(double latitude, double longitude, - Way polygon, int intersections) { - List polyNodes = polygon.getNodes(); - for (int i = 0; i < polyNodes.size() - 1; i++) { - if (MapAlgorithms.ray_intersect_lon(polyNodes.get(i), - polyNodes.get(i + 1), latitude, longitude) != -360d) { - intersections++; - } - } - // special handling, also count first and last, might not be closed, but - // we want this! - if (MapAlgorithms.ray_intersect_lon(polyNodes.get(0), - polyNodes.get(polyNodes.size() - 1), latitude, longitude) != -360d) { - intersections++; - } - return intersections; - } - - private IdentityHashMap> getOuterInnerMapping() { - if (outerInnerMapping == null) { - outerInnerMapping = new IdentityHashMap>(); - //compute the mapping - if ((innerWays != null || closedInnerWays != null) - && countOuterPolygons() != 0) { - fillOuterInnerMapping(closedOuterWays); - fillOuterInnerMapping(outerWays); - } - } - return outerInnerMapping; - } - - private void fillOuterInnerMapping(List outerPolygons) { - for (Way outer : outerPolygons) { - List inners = new ArrayList(); - inners.addAll(findInnersFor(outer, innerWays)); - inners.addAll(findInnersFor(outer, closedInnerWays)); - outerInnerMapping.put(outer, inners); - } - } - - private Collection findInnersFor(Way outer, List inners) { - if(inners == null) { - return Collections.emptyList(); - } - List result = new ArrayList(inners.size()); - for (Way in : inners) { - boolean inIsIn = true; - for (Node n : in.getNodes()) { - if (!polygonContainsPoint(n.getLatitude(), n.getLongitude(), outer, null)) { - inIsIn = false; - break; - } - } - if (inIsIn) { - result.add(in); - } - } - return result; + /** + * get the Inner Rings + * @return the inner rings + */ + private SortedSet getInnerRings() { + groupInRings(); + return innerRings; + } + + /** + * get the outer rings + * @return outer rings + */ + private SortedSet getOuterRings() { + groupInRings(); + return outerRings; } + /** + * get the outer ways + * @return outerWays or empty list if null + */ private List getOuterWays() { if (outerWays == null) { outerWays = new ArrayList(1); @@ -204,50 +147,68 @@ public class Multipolygon { return outerWays; } - private List getClosedOuterWays() { - if (closedOuterWays == null) { - closedOuterWays = new ArrayList(1); - } - return closedOuterWays; - } - - + /** + * get the inner ways + * @return innerWays or empty list if null + */ private List getInnerWays() { if (innerWays == null) { innerWays = new ArrayList(1); } return innerWays; } - - private List getClosedInnerWays() { - if (closedInnerWays == null) { - closedInnerWays = new ArrayList(1); - } - return closedInnerWays; - } - public int countOuterPolygons() - { - return zeroSizeIfNull(outerWays) + zeroSizeIfNull(closedOuterWays); - } - - public boolean hasOpenedPolygons() - { - return zeroSizeIfNull(outerWays) != 0; + /** + * get the number of outer Rings + * @return + */ + public int countOuterPolygons() { + + groupInRings(); + return zeroSizeIfNull(getOuterRings()); + + } - private int zeroSizeIfNull(List list) { - return list != null ? list.size() : 0; + /** + * Check if this multiPolygon has outer ways + * @return true if this has outer ways + */ + public boolean hasOpenedPolygons() { + return zeroSizeIfNull(getOuterWays()) != 0; } - public void addInnerWay(Way es) { - addNewPolygonPart(getInnerWays(), getClosedInnerWays(), new Way(es)); + /** + * return 0 if the list is null + * @param l the list to check + * @return the size of the list, or 0 if the list is null + */ + private int zeroSizeIfNull(Collection l) { + return l != null ? l.size() : 0; + } + + /** + * Add an inner way to the multiPolygon + * @param w the way to add + */ + public void addInnerWay(Way w) { + getInnerWays().add(w); + innerRings = null; } - public void addOuterWay(Way es) { - addNewPolygonPart(getOuterWays(), getClosedOuterWays(), new Way(es)); + /** + * Add an outer way to the multiPolygon + * @param w the way to add + */ + public void addOuterWay(Way w) { + getOuterWays().add(w); + outerRings = null; } + /** + * Add everything from multipolygon to this + * @param multipolygon the MultiPolygon to copy + */ public void copyPolygonsFrom(Multipolygon multipolygon) { for (Way inner : multipolygon.getInnerWays()) { addInnerWay(inner); @@ -255,31 +216,88 @@ public class Multipolygon { for (Way outer : multipolygon.getOuterWays()) { addOuterWay(outer); } - getClosedInnerWays().addAll(multipolygon.getClosedInnerWays()); - getClosedOuterWays().addAll(multipolygon.getClosedOuterWays()); + // reset cache + outerRings = null; + innerRings = null; } - public void addOuterWays(List ring) { - for (Way outer : ring) { + /** + * Add outer ways to the outer Ring + * @param ways the ways to add + */ + public void addOuterWays(List ways) { + for (Way outer : ways) { addOuterWay(outer); } } + /** + * Get the weighted center of all nodes in this multiPolygon
+ * This only works when the ways have initialized nodes + * @return the weighted center + */ public LatLon getCenterPoint() { List points = new ArrayList(); - collectPoints(points, outerWays); - collectPoints(points, closedOuterWays); - collectPoints(points, innerWays); - collectPoints(points, closedInnerWays); + for (Way w : getOuterWays()) { + points.addAll(w.getNodes()); + } + + for (Way w : getInnerWays()) { + points.addAll(w.getNodes()); + } + return MapUtils.getWeightCenterForNodes(points); } - - private void collectPoints(List points, List polygons) { - if (polygons != null) { - for(Way w : polygons){ - points.addAll(w.getNodes()); - } + + /** + * check if a cache has been created + * @return true if the cache exists + */ + public boolean hasCache() { + return outerRings != null && innerRings != null; + } + + /** + * Create the cache
+ * The cache has to be null before it will be created + */ + private void groupInRings() { + if (outerRings == null) { + outerRings = Ring.combineToRings(getOuterWays()); + } + if (innerRings == null) { + innerRings = Ring.combineToRings(getInnerWays()); } } + + /** + * Split this multipolygon in several separate multipolygons with one outer ring each + * @return a list with multipolygons which have exactly one outer ring + */ + public List splitPerOuterRing() { + + SortedSet inners = new TreeSet(getInnerRings()); + + SortedSet outers = getOuterRings(); + ArrayList multipolygons = new ArrayList(); + + for (Ring outer : outers) { + SortedSet innersInsideOuter = new TreeSet(); + for (Ring inner : inners) { + if (inner.isIn(outer)) { + innersInsideOuter.add(inner); + } + } + + SortedSet thisOuter = new TreeSet(); + + Multipolygon m = new Multipolygon(thisOuter, innersInsideOuter); + + multipolygons.add(m); + } + + return multipolygons; + } + } diff --git a/DataExtractionOSM/src/net/osmand/data/Ring.java b/DataExtractionOSM/src/net/osmand/data/Ring.java new file mode 100644 index 0000000000..9976e857db --- /dev/null +++ b/DataExtractionOSM/src/net/osmand/data/Ring.java @@ -0,0 +1,491 @@ +package net.osmand.data; + +import gnu.trove.list.array.TLongArrayList; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import net.osmand.osm.Node; +import net.osmand.osm.Way; + +/** + * A ring is a list of ways that form a simple boundary or an area.

+ * + * + * + * @author sander + * + */ +public class Ring implements Comparable{ + /** + * This is a list of the ways added by the user + * The order can be changed with methods from this class + */ + private ArrayList ways; + /** + * This is the closure of the ways added by the user + * So simple two-node ways are added to close the ring + * This is a cache from what can calculated with the ways + */ + private ArrayList closedWays; + /** + * This is a single way, consisting of all the nodes + * from ways in the closedWays + * this is a cache from what can be calculated with the closedWays + */ + private Way closedBorder; + + /** + * Construct a Ring with a list of ways + * @param ways the ways that make up the Ring + */ + public Ring(List ways) { + this.ways = new ArrayList(); + this.ways.addAll(ways); + } + + /** + * Construct an empty Ring + */ + public Ring() { + this.ways = new ArrayList(); + } + + /** + * Get the ways added to the Ring. + * This is not closed + * The order is not fixed + * @return the ways added to the Ring + */ + public List getWays() { + return ways; + } + + /** + * Add a way to the Ring + * @param w the way to add + */ + public void addWay(Way w) { + // Reset the cache + closedWays = null; + closedBorder = null; + // Add the way + ways.add(w); + } + + /** + * Get the closed ways that make up the Ring + * This method will sort the ways, so it is CPU intensive + * @return the closed ways + */ + public List getClosedWays() { + // Add ways to close the ring + closeWays(); + return closedWays; + } + + /** + * get a single closed way that represents the border + * this method is CPU intensive + * @return a closed way that represents the border + */ + public Way getBorder() { + mergeWays(); + return closedBorder; + } + + /** + * Merge all ways from the closedways into a single way + * If the original ways are initialized with nodes, the new one will be so too + */ + private void mergeWays() { + if (closedBorder != null) return; + + closeWays(); + + closedBorder = new Way(0L); + + Long previousConnection = getMultiLineEndNodes(closedWays)[0]; + + for (Way w : closedWays) { + boolean firstNode = true; + TLongArrayList nodeIds = w.getNodeIds(); + List nodes = w.getNodes(); + + if (w.getFirstNodeId() == previousConnection) { + + for (int i = 0; i< nodeIds.size(); i++) { + // don't need to add the first node, that one was added by the previous way + if (!firstNode) { + if(nodes == null || i>=nodes.size()) { + closedBorder.addNode(nodeIds.get(i)); + } else { + closedBorder.addNode(nodes.get(i)); + } + + } + firstNode = false; + } + + previousConnection = w.getLastNodeId(); + } else { + + // add the nodes in reverse order + for (int i = nodeIds.size() - 1; i >= 0; i--) { + // don't need to add the first node, that one was added by the previous way + if (!firstNode) { + if(nodes == null || i>=nodes.size()) { + closedBorder.addNode(nodeIds.get(i)); + } else { + closedBorder.addNode(nodes.get(i)); + } + } + firstNode = false; + } + + previousConnection = w.getFirstNodeId(); + + } + } + + } + + /** + * Check if there exists a cache, if so, return it + * If there isn't a cache, sort the ways to form connected strings

+ * + * If a Ring contains a gap, one way (without initialized nodes and id=0) is added to the list + */ + private void closeWays(){ + // If the ways have been closed, return the cache + if (closedWays != null) return; + if (ways.size() == 0) { + closedWays = new ArrayList(); + return; + } + ArrayList> multiLines = createMultiLines(ways); + + // TODO try to close rings which consist out of multiple segments. + // This is a data fault, but it could be solved a bit by OsmAnd + if (multiLines.size() != 1) return; + + ArrayList multiLine = multiLines.get(0); + + closedWays = multiLine; + + long[] endNodes = getMultiLineEndNodes(multiLine); + + + if (endNodes[0] != endNodes[1]) { + Way w = new Way(0L); + w.addNode(endNodes[0]); + w.addNode(endNodes[1]); + closedWays.add(w); + } + + return; + + } + + /** + * Join the ways in connected strings for further processing + * @return A list with list of connected ways + */ + private static ArrayList> createMultiLines(List ways){ + // make a list of multiLines (connecter pieces of way) + // One ArrayList is one multiLine + ArrayList> multiLines = new ArrayList>(); + for (Way toAdd : ways) { + /* + * Check if the way has at least 2 nodes + * + * FIXME TO LOG OR NOT TO LOG? + * + * logging this creates a whole bunch of log lines for all ways + * part of a multipolygon but not in the map + */ + if (toAdd.getNodeIds().size() < 2) continue; + + long toAddBeginPt = toAdd.getFirstNodeId(); + long toAddEndPt = toAdd.getLastNodeId(); + + // the way has been added to this number of multiLines + int addedTo = 0; + + // save the first and second changed multiLine + ArrayList firstMultiLine = new ArrayList (); + ArrayList secondMultiLine = new ArrayList (); + + + // iterate over the multiLines, and add the way to the correct one + for( ArrayList multiLine : multiLines) { + + // to check if this multiLine has been changed at the end of the loop + int previousAddedTo = addedTo; + + // get the first and last way of a multiLine + Way firstWay = multiLine.get(0); + Way lastWay = multiLine.get(multiLine.size() - 1); + // add the way to the correct multiLines (maybe two) + if (toAddBeginPt == firstWay.getFirstNodeId() || + toAddBeginPt == firstWay.getLastNodeId() || + toAddEndPt == firstWay.getFirstNodeId() || + toAddEndPt == firstWay.getLastNodeId() ) { + // add the way to the begining to respect order + multiLine.add(0, toAdd); + addedTo++; + } else if (toAddBeginPt == lastWay.getFirstNodeId() || + toAddBeginPt == lastWay.getLastNodeId() || + toAddEndPt == lastWay.getFirstNodeId() || + toAddEndPt == lastWay.getLastNodeId()) { + // add the way to the end + multiLine.add(toAdd); + addedTo++; + } + + // save this multiLines if it has been changed + if (previousAddedTo != addedTo) { + + if (addedTo == 1) { + firstMultiLine = multiLine; + } + + if (addedTo == 2) { + secondMultiLine = multiLine; + } + + // a Ring may never contain a fork + // if there is a third multiline, don't process + // hope there is a fourth one, sot these two will be processed later on + } + + } + + // If the way is added to nothing, make a new multiLine + if (addedTo == 0 ) { + ArrayList multiLine = new ArrayList(); + multiLine.add(toAdd); + multiLines.add(multiLine); + continue; + } + + //everything OK + if (addedTo == 1) continue; + + + // only the case addedTo == 2 remains + // two multiLines have to be merged + + + + if (firstMultiLine.get(firstMultiLine.size() - 1) == secondMultiLine.get(0)) { + // add the second to the first + secondMultiLine.remove(0) ; + for (Way w : secondMultiLine) { + firstMultiLine.add(w); + } + multiLines.remove(secondMultiLine); + } else if (secondMultiLine.get(secondMultiLine.size() - 1) == firstMultiLine.get(0)) { + // just add the first to the second + firstMultiLine.remove(0) ; + for (Way w : firstMultiLine) { + secondMultiLine.add(w); + } + multiLines.remove(firstMultiLine); + } else if (firstMultiLine.get(0) == secondMultiLine.get(0)) { + // add the first in reversed to the beginning of the second + firstMultiLine.remove(toAdd); + for (Way w : firstMultiLine) { + secondMultiLine.add(0,w); + } + multiLines.remove(firstMultiLine); + } else { + // add the first in reversed to the end of the second + firstMultiLine.remove(toAdd); + int index = secondMultiLine.size(); + for (Way w : firstMultiLine) { + secondMultiLine.add(index ,w); + } + multiLines.remove(firstMultiLine); + } + + + } + return multiLines; + } + + /** + * Get the end nodes of a multiLine + * The ways in the multiLine don't have to be initialized for this. + * + * @param multiLine the multiLine to get the end nodes of + * @return an array of size two with the end nodes on both sides.
+ * * The first node is the end node of the first way in the multiLine.
+ * * The second node is the end node of the last way in the multiLine. + */ + private long[] getMultiLineEndNodes(ArrayList multiLine) { + + // special case, the multiLine contains only a single way, return the end nodes of the way + if (multiLine.size() == 1){ + return new long[] {multiLine.get(0).getFirstNodeId(), multiLine.get(0).getLastNodeId()}; + } + + long n1 = 0, n2 = 0; + + if (multiLine.get(0).getFirstNodeId() == multiLine.get(1).getFirstNodeId() || + multiLine.get(0).getFirstNodeId() == multiLine.get(1).getLastNodeId()) { + n1 = multiLine.get(0).getLastNodeId(); + } else if (multiLine.get(0).getLastNodeId() == multiLine.get(1).getFirstNodeId() || + multiLine.get(0).getLastNodeId() == multiLine.get(1).getLastNodeId()) { + n1 = multiLine.get(0).getFirstNodeId(); + } + + int lastIdx = multiLine.size()-1; + + if (multiLine.get(lastIdx).getFirstNodeId() == multiLine.get(1).getFirstNodeId() || + multiLine.get(lastIdx).getFirstNodeId() == multiLine.get(1).getLastNodeId()) { + n2 = multiLine.get(lastIdx).getLastNodeId(); + } else if (multiLine.get(lastIdx).getLastNodeId() == multiLine.get(lastIdx - 1).getFirstNodeId() || + multiLine.get(lastIdx).getLastNodeId() == multiLine.get(lastIdx - 1).getLastNodeId()) { + n2 = multiLine.get(lastIdx).getFirstNodeId(); + } + + + return new long[] {n1, n2}; + } + + /** + * Combine a list of ways to a list of rings + * + * The ways must not have initialized nodes for this + * + * @param ways the ways to group + * @return a list of Rings + */ + public static SortedSet combineToRings(List ways){ + ArrayList> multiLines = createMultiLines(ways); + + SortedSet result = new TreeSet (); + + for (ArrayList multiLine : multiLines) { + Ring r = new Ring(multiLine); + result.add(r); + } + + return result; + } + + /** + * check if this Ring contains the node + * @param n the Node to check + * @return yes if the node is inside the ring + */ + public boolean containsNode(Node n) { + return containsPoint(n.getLatitude(), n.getLongitude()); + } + + /** + * check if this Ring contains the point + * @param latitude lat of the point + * @param longitude lon of the point + * @return yes if the point is inside the ring + */ + public boolean containsPoint(double latitude, double longitude){ + return countIntersections(latitude, longitude) % 2 == 1; + } + + /** + * count the intersections when going from lat, lon to outside the ring + * @param latitude the lat to start + * @param longitude the lon to start + * @param intersections the number of intersections to start with + * @return the number of intersections + */ + private int countIntersections(double latitude, double longitude) { + int intersections = 0; + + mergeWays(); + List polyNodes = closedBorder.getNodes(); + for (int i = 0; i < polyNodes.size() - 1; i++) { + if (MapAlgorithms.ray_intersect_lon(polyNodes.get(i), + polyNodes.get(i + 1), latitude, longitude) != -360d) { + intersections++; + } + } + // special handling, also count first and last, might not be closed, but + // we want this! + if (MapAlgorithms.ray_intersect_lon(polyNodes.get(0), + polyNodes.get(polyNodes.size() - 1), latitude, longitude) != -360d) { + intersections++; + } + return intersections; + } + + /** + * collect the points of all ways added by the user
+ * automatically added ways because of closing the Ring won't be added
+ * Only ways with initialized points can be handled. + * @return a List with nodes + */ + public List collectPoints() { + + ArrayList collected = new ArrayList(); + + for (Way w : ways) { + collected.addAll(w.getNodes()); + } + + return collected; + + } + + /** + * Check if this is in Ring r + * @param r the ring to check + * @return true if this Ring is inside Ring r + */ + public boolean isIn(Ring r) { + /* + * bi-directional check is needed because some concave rings can intersect + * and would only fail on one of the checks + */ + List points = this.collectPoints(); + + // r should contain all nodes of this + for(Node n : points) { + if (!r.containsNode(n)) { + return false; + } + } + + points = r.collectPoints(); + + // this should not contain a node from r + for(Node n : points) { + if (this.containsNode(n)) { + return false; + } + } + + return true; + + } + + + @Override + /** + * @return -1 if this Ring is inside r
+ * 1 if r is inside this Ring
+ * 0 otherwise (Rings are next to each other, Rings intersect or Rings are malformed) + */ + public int compareTo(Ring r) { + if (this.isIn(r)) return -1; + if (r.isIn(this)) return 1; + return 0; + } + + + +} From 107c814dcacba43b61b5d45fa9f4303253eaecae Mon Sep 17 00:00:00 2001 From: Sander Deryckere Date: Wed, 12 Sep 2012 15:44:11 +0200 Subject: [PATCH 2/2] Fix indexing of multipolygon with multiple outer rings issue 1265 --- .../src/net/osmand/data/Multipolygon.java | 84 ++++- .../src/net/osmand/data/Ring.java | 67 +++- .../preparation/IndexVectorMapCreator.java | 305 +++++------------- 3 files changed, 225 insertions(+), 231 deletions(-) diff --git a/DataExtractionOSM/src/net/osmand/data/Multipolygon.java b/DataExtractionOSM/src/net/osmand/data/Multipolygon.java index c62986793c..10a9c26988 100644 --- a/DataExtractionOSM/src/net/osmand/data/Multipolygon.java +++ b/DataExtractionOSM/src/net/osmand/data/Multipolygon.java @@ -11,6 +11,8 @@ import net.osmand.osm.MapUtils; import net.osmand.osm.Node; import net.osmand.osm.Way; +import org.apache.commons.logging.Log; + /** * The idea of multipolygon: * - we treat each outer way as closed polygon @@ -33,6 +35,11 @@ public class Multipolygon { */ private List outerWays, innerWays; + /** + * an optional id of the multipolygon + */ + private long id; + /** * create a multipolygon with these outer and inner rings * the rings have to be well formed or data inconsistency will happen @@ -69,6 +76,32 @@ public class Multipolygon { public Multipolygon(){ outerWays = new ArrayList (); innerWays = new ArrayList (); + id = 0L; + } + + /** + * create a new empty multipolygon with specified id + * @param id the id to set + */ + public Multipolygon(long id){ + this(); + setId(id); + } + + /** + * set the id of the multipolygon + * @param newId id to set + */ + public void setId(long newId) { + id = newId; + } + + /** + * get the id of the multipolygon + * @return id + */ + public long getId() { + return id; } /** @@ -122,7 +155,7 @@ public class Multipolygon { * get the Inner Rings * @return the inner rings */ - private SortedSet getInnerRings() { + public SortedSet getInnerRings() { groupInRings(); return innerRings; } @@ -131,7 +164,7 @@ public class Multipolygon { * get the outer rings * @return outer rings */ - private SortedSet getOuterRings() { + public SortedSet getOuterRings() { groupInRings(); return outerRings; } @@ -178,6 +211,26 @@ public class Multipolygon { return zeroSizeIfNull(getOuterWays()) != 0; } + /** + * chekc if all rings are closed + * @return true if all rings are closed by nature, false otherwise + */ + public boolean areRingsComplete() { + SortedSet set = getOuterRings(); + for (Ring r : set) { + if (!r.isClosed()) { + return false; + } + } + set = getInnerRings(); + for (Ring r : set) { + if (!r.isClosed()) { + return false; + } + } + return true; + } + /** * return 0 if the list is null * @param l the list to check @@ -272,16 +325,23 @@ public class Multipolygon { /** * Split this multipolygon in several separate multipolygons with one outer ring each + * @param log the stream to log problems to, if log = null, nothing will be logged * @return a list with multipolygons which have exactly one outer ring */ - public List splitPerOuterRing() { + public List splitPerOuterRing(Log log) { + //make a clone of the inners set + // this set will be changed through execution of the method SortedSet inners = new TreeSet(getInnerRings()); + // get the set of outer rings in a variable. This set will not be changed SortedSet outers = getOuterRings(); ArrayList multipolygons = new ArrayList(); + // loop; start with the smallest outer ring for (Ring outer : outers) { + + // Search the inners inside this outer ring SortedSet innersInsideOuter = new TreeSet(); for (Ring inner : inners) { if (inner.isIn(outer)) { @@ -289,15 +349,31 @@ public class Multipolygon { } } - SortedSet thisOuter = new TreeSet(); + // the inners should belong to this outer, so remove them from the list to check + inners.removeAll(innersInsideOuter); + SortedSet thisOuter = new TreeSet(); + thisOuter.add(outer); + + // create a new multipolygon with this outer and a list of inners Multipolygon m = new Multipolygon(thisOuter, innersInsideOuter); multipolygons.add(m); } + if (inners.size() != 0 && log != null) + log.warn("Multipolygon "+getId() + " has a mismatch in outer and inner rings"); + return multipolygons; } + + /** + * This method only works when the multipolygon has exaclt one outer Ring + * @return the list of nodes in the outer ring + */ + public List getOuterNodes() { + return getOuterRings().first().getBorder().getNodes(); + } } diff --git a/DataExtractionOSM/src/net/osmand/data/Ring.java b/DataExtractionOSM/src/net/osmand/data/Ring.java index 9976e857db..651b41d50d 100644 --- a/DataExtractionOSM/src/net/osmand/data/Ring.java +++ b/DataExtractionOSM/src/net/osmand/data/Ring.java @@ -86,6 +86,20 @@ public class Ring implements Comparable{ return closedWays; } + /** + * check if this ring is closed by nature + * @return true if this ring is closed, false otherwise + */ + public boolean isClosed() { + closeWays(); + for (int i = closedWays.size()-1; i>=0; i--) { + if (!ways.contains(closedWays.get(i))){ + return false; + } + } + return true; + } + /** * get a single closed way that represents the border * this method is CPU intensive @@ -176,15 +190,38 @@ public class Ring implements Comparable{ closedWays = multiLine; long[] endNodes = getMultiLineEndNodes(multiLine); - - if (endNodes[0] != endNodes[1]) { - Way w = new Way(0L); - w.addNode(endNodes[0]); - w.addNode(endNodes[1]); - closedWays.add(w); + if(multiLine.get(0).getNodes() == null) { + Way w = new Way(0L); + w.addNode(endNodes[0]); + w.addNode(endNodes[1]); + closedWays.add(w); + } else { + Node n1 = null, n2 = null; + if (multiLine.get(0).getFirstNodeId() == endNodes[0]) { + n1 = multiLine.get(0).getNodes().get(0); + } else { + int index = multiLine.get(0).getNodes().size() - 1; + n1 = multiLine.get(0).getNodes().get(index); + } + + int lastML = multiLine.size() - 1; + if (multiLine.get(lastML).getFirstNodeId() == endNodes[0]) { + n2 = multiLine.get(lastML).getNodes().get(0); + } else { + int index = multiLine.get(lastML).getNodes().size() - 1; + n2 = multiLine.get(lastML).getNodes().get(index); + } + + Way w = new Way(0L); + w.addNode(n1); + w.addNode(n2); + closedWays.add(w); + } } + + return; } @@ -206,7 +243,9 @@ public class Ring implements Comparable{ * logging this creates a whole bunch of log lines for all ways * part of a multipolygon but not in the map */ - if (toAdd.getNodeIds().size() < 2) continue; + if (toAdd.getNodeIds().size() < 2) { + continue; + } long toAddBeginPt = toAdd.getFirstNodeId(); long toAddEndPt = toAdd.getLastNodeId(); @@ -332,6 +371,19 @@ public class Ring implements Comparable{ return new long[] {multiLine.get(0).getFirstNodeId(), multiLine.get(0).getLastNodeId()}; } + if (multiLine.size() == 2) { + // ring of two elements, arbitrary choice of the end nodes + if(multiLine.get(0).getFirstNodeId() == multiLine.get(1).getFirstNodeId() && + multiLine.get(0).getLastNodeId() == multiLine.get(1).getLastNodeId()) { + return new long[] {multiLine.get(0).getFirstNodeId(), multiLine.get(0).getFirstNodeId()}; + } else if(multiLine.get(0).getFirstNodeId() == multiLine.get(1).getLastNodeId() && + multiLine.get(0).getLastNodeId() == multiLine.get(1).getFirstNodeId()) { + return new long[] {multiLine.get(0).getFirstNodeId(), multiLine.get(0).getFirstNodeId()}; + } + } + + // For all other multiLine lenghts, or for non-closed multiLines with two elements, proceed + long n1 = 0, n2 = 0; if (multiLine.get(0).getFirstNodeId() == multiLine.get(1).getFirstNodeId() || @@ -352,7 +404,6 @@ public class Ring implements Comparable{ n2 = multiLine.get(lastIdx).getFirstNodeId(); } - return new long[] {n1, n2}; } diff --git a/DataExtractionOSM/src/net/osmand/data/preparation/IndexVectorMapCreator.java b/DataExtractionOSM/src/net/osmand/data/preparation/IndexVectorMapCreator.java index 7949a5755f..622bccc4ed 100644 --- a/DataExtractionOSM/src/net/osmand/data/preparation/IndexVectorMapCreator.java +++ b/DataExtractionOSM/src/net/osmand/data/preparation/IndexVectorMapCreator.java @@ -26,8 +26,9 @@ import net.osmand.Algoritms; import net.osmand.IProgress; import net.osmand.binary.OsmandOdb.MapData; import net.osmand.binary.OsmandOdb.MapDataBlock; -import net.osmand.data.Boundary; import net.osmand.data.MapAlgorithms; +import net.osmand.data.Multipolygon; +import net.osmand.data.Ring; import net.osmand.data.preparation.MapZooms.MapZoomPair; import net.osmand.osm.Entity; import net.osmand.osm.Entity.EntityId; @@ -113,234 +114,100 @@ public class IndexVectorMapCreator extends AbstractIndexPartCreator { } } + /** + * index a multipolygon into the database + * only multipolygons without admin_level and with type=multipolygon are indexed + * broken multipolygons are also indexed, inner ways are sometimes left out, broken rings are split and closed + * broken multipolygons will normally be logged + * @param e the entity to index + * @param ctx the database context + * @throws SQLException + */ private void indexMultiPolygon(Entity e, OsmDbAccessorContext ctx) throws SQLException { - if (e instanceof Relation && "multipolygon".equals(e.getTag(OSMTagKey.TYPE))) { //$NON-NLS-1$ - if(e.getTag(OSMTagKey.ADMIN_LEVEL) != null) { - // don't index boundaries as multipolygon (only areas ideally are multipolygon) - return; - } - ctx.loadEntityRelation((Relation) e); - Map entities = ((Relation) e).getMemberEntities(); + // Don't handle things that aren't multipolygon, and nothing administrative + if (! (e instanceof Relation) || + ! "multipolygon".equals(e.getTag(OSMTagKey.TYPE)) || + e.getTag(OSMTagKey.ADMIN_LEVEL) != null ) return; - boolean outerFound = false; - for (Entity es : entities.keySet()) { - if (es instanceof Way) { - boolean inner = "inner".equals(entities.get(es)); //$NON-NLS-1$ - if (!inner) { - outerFound = true; - // This is incorrect (it should be intersection of all boundaries) - // Currently it causes an issue with coastline (if one line is coastline) -// for (String t : es.getTagKeySet()) { -// e.putTag(t, es.getTag(t)); -// } - break; - } - } - } - if (!outerFound) { - logMapDataWarn.warn("Probably map bug: Multipoligon id=" + e.getId() + " contains only inner ways : "); //$NON-NLS-1$ //$NON-NLS-2$ - return; - } + ctx.loadEntityRelation((Relation) e); + Map entities = ((Relation) e).getMemberEntities(); - renderingTypes.encodeEntityWithType(e, mapZooms.getLevel(0).getMaxZoom(), typeUse, addtypeUse, namesUse, tempNameUse); - if (typeUse.size() > 0) { - List> completedRings = new ArrayList>(); - List> incompletedRings = new ArrayList>(); - for (Entity es : entities.keySet()) { - if (es instanceof Way) { - if (!((Way) es).getNodeIds().isEmpty()) { - combineMultiPolygons((Way) es, completedRings, incompletedRings); - } - } - } - // skip incompleted rings and do not add whole relation ? - if (!incompletedRings.isEmpty()) { - logMapDataWarn.warn("In multipolygon " + e.getId() + " there are incompleted ways : " + incompletedRings); - return; - // completedRings.addAll(incompletedRings); - } + // create a multipolygon object for this + Multipolygon original = new Multipolygon(e.getId()); - // skip completed rings that are not one type - for (List l : completedRings) { - boolean innerType = "inner".equals(entities.get(l.get(0))); //$NON-NLS-1$ - for (Way way : l) { - boolean inner = "inner".equals(entities.get(way)); //$NON-NLS-1$ - if (innerType != inner) { - logMapDataWarn - .warn("Probably map bug: Multipoligon contains outer and inner ways.\n" + //$NON-NLS-1$ - "Way:" - + way.getId() - + " is strange part of completed ring. InnerType:" + innerType + " way inner: " + inner + " way inner string:" + entities.get(way)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ - return; - } - } - } - - // That check is not strictly needed on preproccessing step because client can handle it - Node nodeOut = checkOuterWaysEncloseInnerWays(completedRings, entities); - if (nodeOut != null) { - logMapDataWarn.warn("Map bug: Multipoligon contains 'inner' way point outside of 'outer' border.\n" + //$NON-NLS-1$ - "Multipolygon id : " + e.getId() + ", inner node out id : " + nodeOut.getId()); //$NON-NLS-1$ - } - - List outerWaySrc = new ArrayList(); - List> innerWays = new ArrayList>(); - - TIntArrayList typeToSave = new TIntArrayList(typeUse); - long baseId = 0; - for (List l : completedRings) { - boolean innerType = "inner".equals(entities.get(l.get(0))); //$NON-NLS-1$ - if (!innerType && !outerWaySrc.isEmpty()) { - logMapDataWarn.warn("Map bug: Multipoligon contains many 'outer' borders.\n" + //$NON-NLS-1$ - "Multipolygon id : " + e.getId() + ", outer way id : " + l.get(0).getId()); //$NON-NLS-1$ - return; - } - List toCollect; - if (innerType) { - toCollect = new ArrayList(); - innerWays.add(toCollect); - } else { - toCollect = outerWaySrc; - } - - for (Way way : l) { - toCollect.addAll(way.getNodes()); - if (!innerType) { - TIntArrayList out = multiPolygonsWays.put(way.getId(), typeToSave); - if(out == null){ - baseId = -way.getId(); - } - } - } - } - if(baseId == 0){ - // use base id as well? - baseId = notUsedId --; - } - nextZoom: for (int level = 0; level < mapZooms.size(); level++) { - renderingTypes.encodeEntityWithType(e, mapZooms.getLevel(level).getMaxZoom(), typeUse, addtypeUse, namesUse, - tempNameUse); - if (typeUse.isEmpty()) { - continue; - } - long id = convertBaseIdToGeneratedId(baseId, level); - // simplify route - List outerWay = outerWaySrc; - int zoomToSimplify = mapZooms.getLevel(level).getMaxZoom() - 1; - if (zoomToSimplify < 15) { - outerWay = simplifyCycleWay(outerWay, zoomToSimplify, zoomWaySmothness); - if (outerWay == null) { - continue nextZoom; - } - List> newinnerWays = new ArrayList>(); - for (List ls : innerWays) { - ls = simplifyCycleWay(ls, zoomToSimplify, zoomWaySmothness); - if (ls != null) { - newinnerWays.add(ls); - } - } - innerWays = newinnerWays; - } - insertBinaryMapRenderObjectIndex(mapTree[level], outerWay, innerWays, namesUse, id, true, typeUse, addtypeUse, true); - } - } - } - } - - private Node checkOuterWaysEncloseInnerWays(List> completedRings, Map entities) { - List> innerWays = new ArrayList>(); - Boundary outerBoundary = new Boundary(); - Node toReturn = null; - for (List ring : completedRings) { - boolean innerType = "inner".equals(entities.get(ring.get(0))); //$NON-NLS-1$ - if (!innerType) { - outerBoundary.addOuterWays(ring); - } else { - innerWays.add(ring); - } - } - - for (List innerRing : innerWays) { - ring: for (Way innerWay : innerRing) { - for (Node node : innerWay.getNodes()) { - if (!outerBoundary.containsPoint(node.getLatitude(), node.getLongitude())) { - if (toReturn == null) { - toReturn = node; - } - completedRings.remove(innerRing); - break ring; - } - } - } - } - return toReturn; - } - - private List reverse(List l) { - Collections.reverse(l); - for(Way w : l){ - w.getNodeIds().reverse(); - Collections.reverse(w.getNodes()); - } - return l; - } - - private List appendLists(List w1, List w2){ - w1.addAll(w2); - return w1; - } - - //TODO Can the Multipolygon class be the one that replaces this? - private void combineMultiPolygons(Way w, List> completedRings, List> incompletedRings) { - long lId = w.getEntityIds().get(w.getEntityIds().size() - 1).getId().longValue(); - long fId = w.getEntityIds().get(0).getId().longValue(); - if (fId == lId) { - completedRings.add(Collections.singletonList(w)); - } else { - List l = new ArrayList(); - l.add(w); - boolean add = true; - for (int k = 0; k < incompletedRings.size();) { - boolean remove = false; - List i = incompletedRings.get(k); - Way last = i.get(i.size() - 1); - Way first = i.get(0); - long lastId = last.getEntityIds().get(last.getEntityIds().size() - 1).getId().longValue(); - long firstId = first.getEntityIds().get(0).getId().longValue(); - if (fId == lastId) { - remove = true; - l = appendLists(i, l); - fId = firstId; - } else if (lId == firstId) { - l = appendLists(l, i); - remove = true; - lId = lastId; - } else if (lId == lastId) { - l = appendLists(l, reverse(i)); - remove = true; - lId = firstId; - } else if (fId == firstId) { - l = appendLists(reverse(i), l); - remove = true; - fId = lastId; - } - if (remove) { - incompletedRings.remove(k); + // fill the multipolygon with all ways from the Relation + for (Entity es : entities.keySet()) { + if (es instanceof Way) { + boolean inner = "inner".equals(entities.get(es)); //$NON-NLS-1$ + if (inner) { + original.addInnerWay((Way) es); } else { - k++; - } - if (fId == lId) { - completedRings.add(l); - add = false; - break; + original.addOuterWay((Way) es); } } - if (add) { - incompletedRings.add(l); + } + + // Log if something is wrong + if (!original.hasOpenedPolygons()) { + logMapDataWarn.warn("Multipolygon has unclosed parts: Multipoligon id=" + e.getId()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + renderingTypes.encodeEntityWithType(e, mapZooms.getLevel(0).getMaxZoom(), typeUse, addtypeUse, namesUse, tempNameUse); + + //Don't add multipolygons with an unknown type + if (typeUse.size() == 0) return; + + // Log the fact that Rings aren't complete, but continue with the relation, try to close it as well as possible + if (!original.areRingsComplete()) { + logMapDataWarn.warn("In multipolygon " + e.getId() + " there are incompleted ways"); + } + // Rings with different types (inner or outer) in one ring will be logged in the previous case + // The Rings are only composed by type, so if one way gets in a different Ring, the rings will be incomplete + + List multipolygons = original.splitPerOuterRing(logMapDataWarn); + + + for (Multipolygon m : multipolygons) { + + // innerWays are new closed ways + List> innerWays = new ArrayList>(); + + for (Ring r : m.getInnerRings()) { + innerWays.add(r.getBorder().getNodes()); + } + + // don't use the relation ids. Create new ones + long baseId = notUsedId --; + nextZoom: for (int level = 0; level < mapZooms.size(); level++) { + renderingTypes.encodeEntityWithType(e, mapZooms.getLevel(level).getMaxZoom(), typeUse, addtypeUse, namesUse, + tempNameUse); + if (typeUse.isEmpty()) { + continue; + } + long id = convertBaseIdToGeneratedId(baseId, level); + // simplify route + List outerWay = m.getOuterNodes(); + int zoomToSimplify = mapZooms.getLevel(level).getMaxZoom() - 1; + if (zoomToSimplify < 15) { + outerWay = simplifyCycleWay(outerWay, zoomToSimplify, zoomWaySmothness); + if (outerWay == null) { + continue nextZoom; + } + List> newinnerWays = new ArrayList>(); + for (List ls : innerWays) { + ls = simplifyCycleWay(ls, zoomToSimplify, zoomWaySmothness); + if (ls != null) { + newinnerWays.add(ls); + } + } + innerWays = newinnerWays; + } + insertBinaryMapRenderObjectIndex(mapTree[level], outerWay, innerWays, namesUse, id, true, typeUse, addtypeUse, true); + } } } - + public static List simplifyCycleWay(List ns, int zoom, int zoomWaySmothness) throws SQLException { if (checkForSmallAreas(ns, zoom + Math.min(zoomWaySmothness / 2, 3), 2, 4)) { return null;