Merge pull request #309 from sanderd18/multipolygon

Fix issue 1265
This commit is contained in:
Pavol Zibrita 2012-09-12 11:08:58 -07:00
commit f37049863d
3 changed files with 926 additions and 423 deletions

View file

@ -2,16 +2,17 @@ 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;
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
@ -23,180 +24,155 @@ import net.osmand.osm.Way;
* @author Pavol Zibrita
*/
public class Multipolygon {
/**
* cache with the ways grouped per Ring
*/
private SortedSet<Ring> innerRings, outerRings;
protected List<Way> closedOuterWays;
protected List<Way> outerWays;
protected List<Way> closedInnerWays;
protected List<Way> innerWays;
/**
* ways added by the user
*/
private List<Way> outerWays, innerWays;
protected IdentityHashMap<Way,List<Way>> outerInnerMapping;
/**
* an optional id of the multipolygon
*/
private long id;
private void addNewPolygonPart(List<Way> polygons, List<Way> 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<Way> wayStack = new Stack<Way>();
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<Ring> outerRings, SortedSet<Ring> 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<Node> 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<Way> outers, List<Way> inners) {
this();
outerWays.addAll(outers);
innerWays.addAll(inners);
}
/**
* create a new empty multipolygon
*/
public Multipolygon(){
outerWays = new ArrayList<Way> ();
innerWays = new ArrayList<Way> ();
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;
}
/**
* 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<Way> polygons, List<Way> closedPolygons, Stack<Way> workStack) {
while (!workStack.isEmpty()) {
Way changedWay = workStack.pop();
List<Node> 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<Ring> outers = new TreeSet<Ring>();
TreeSet<Ring> inners = new TreeSet<Ring>();
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<Way> outerPolygons, double latitude, double longitude) {
if (outerPolygons != null) {
for (Way polygon : outerPolygons) {
List<Way> inners = getOuterInnerMapping().get(polygon);
if (polygonContainsPoint(latitude, longitude, polygon, inners)) {
return true;
}
}
}
return false;
}
private boolean polygonContainsPoint(double latitude, double longitude,
Way polygon, List<Way> 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<Node> 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<Way, List<Way>> getOuterInnerMapping() {
if (outerInnerMapping == null) {
outerInnerMapping = new IdentityHashMap<Way, List<Way>>();
//compute the mapping
if ((innerWays != null || closedInnerWays != null)
&& countOuterPolygons() != 0) {
fillOuterInnerMapping(closedOuterWays);
fillOuterInnerMapping(outerWays);
}
}
return outerInnerMapping;
}
private void fillOuterInnerMapping(List<Way> outerPolygons) {
for (Way outer : outerPolygons) {
List<Way> inners = new ArrayList<Way>();
inners.addAll(findInnersFor(outer, innerWays));
inners.addAll(findInnersFor(outer, closedInnerWays));
outerInnerMapping.put(outer, inners);
}
}
private Collection<Way> findInnersFor(Way outer, List<Way> inners) {
if(inners == null) {
return Collections.emptyList();
}
List<Way> result = new ArrayList<Way>(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
*/
public SortedSet<Ring> getInnerRings() {
groupInRings();
return innerRings;
}
/**
* get the outer rings
* @return outer rings
*/
public SortedSet<Ring> getOuterRings() {
groupInRings();
return outerRings;
}
/**
* get the outer ways
* @return outerWays or empty list if null
*/
private List<Way> getOuterWays() {
if (outerWays == null) {
outerWays = new ArrayList<Way>(1);
@ -204,50 +180,88 @@ public class Multipolygon {
return outerWays;
}
private List<Way> getClosedOuterWays() {
if (closedOuterWays == null) {
closedOuterWays = new ArrayList<Way>(1);
}
return closedOuterWays;
}
/**
* get the inner ways
* @return innerWays or empty list if null
*/
private List<Way> getInnerWays() {
if (innerWays == null) {
innerWays = new ArrayList<Way>(1);
}
return innerWays;
}
/**
* get the number of outer Rings
* @return
*/
public int countOuterPolygons() {
groupInRings();
return zeroSizeIfNull(getOuterRings());
}
private List<Way> getClosedInnerWays() {
if (closedInnerWays == null) {
closedInnerWays = new ArrayList<Way>(1);
/**
* Check if this multiPolygon has outer ways
* @return true if this has outer ways
*/
public boolean hasOpenedPolygons() {
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<Ring> set = getOuterRings();
for (Ring r : set) {
if (!r.isClosed()) {
return false;
}
}
return closedInnerWays;
}
public int countOuterPolygons()
{
return zeroSizeIfNull(outerWays) + zeroSizeIfNull(closedOuterWays);
}
public boolean hasOpenedPolygons()
{
return zeroSizeIfNull(outerWays) != 0;
set = getInnerRings();
for (Ring r : set) {
if (!r.isClosed()) {
return false;
}
}
return true;
}
private int zeroSizeIfNull(List<Way> list) {
return list != null ? list.size() : 0;
/**
* 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;
}
public void addInnerWay(Way es) {
addNewPolygonPart(getInnerWays(), getClosedInnerWays(), new Way(es));
/**
* 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 +269,111 @@ 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<Way> ring) {
for (Way outer : ring) {
/**
* Add outer ways to the outer Ring
* @param ways the ways to add
*/
public void addOuterWays(List<Way> ways) {
for (Way outer : ways) {
addOuterWay(outer);
}
}
/**
* Get the weighted center of all nodes in this multiPolygon <br />
* This only works when the ways have initialized nodes
* @return the weighted center
*/
public LatLon getCenterPoint() {
List<Node> points = new ArrayList<Node>();
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<Node> points, List<Way> 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 <br />
* 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
* @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<Multipolygon> splitPerOuterRing(Log log) {
//make a clone of the inners set
// this set will be changed through execution of the method
SortedSet<Ring> inners = new TreeSet<Ring>(getInnerRings());
// get the set of outer rings in a variable. This set will not be changed
SortedSet<Ring> outers = getOuterRings();
ArrayList<Multipolygon> multipolygons = new ArrayList<Multipolygon>();
// loop; start with the smallest outer ring
for (Ring outer : outers) {
// Search the inners inside this outer ring
SortedSet<Ring> innersInsideOuter = new TreeSet<Ring>();
for (Ring inner : inners) {
if (inner.isIn(outer)) {
innersInsideOuter.add(inner);
}
}
// the inners should belong to this outer, so remove them from the list to check
inners.removeAll(innersInsideOuter);
SortedSet<Ring> thisOuter = new TreeSet<Ring>();
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<Node> getOuterNodes() {
return getOuterRings().first().getBorder().getNodes();
}
}

View file

@ -0,0 +1,542 @@
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. <p />
*
*
*
* @author sander
*
*/
public class Ring implements Comparable<Ring>{
/**
* This is a list of the ways added by the user
* The order can be changed with methods from this class
*/
private ArrayList<Way> 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<Way> 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<Way> ways) {
this.ways = new ArrayList<Way>();
this.ways.addAll(ways);
}
/**
* Construct an empty Ring
*/
public Ring() {
this.ways = new ArrayList<Way>();
}
/**
* 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<Way> 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<Way> getClosedWays() {
// Add ways to close the ring
closeWays();
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
* @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<Node> 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 <p />
*
* 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<Way>();
return;
}
ArrayList<ArrayList<Way>> 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<Way> multiLine = multiLines.get(0);
closedWays = multiLine;
long[] endNodes = getMultiLineEndNodes(multiLine);
if (endNodes[0] != endNodes[1]) {
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;
}
/**
* Join the ways in connected strings for further processing
* @return A list with list of connected ways
*/
private static ArrayList<ArrayList<Way>> createMultiLines(List<Way> ways){
// make a list of multiLines (connecter pieces of way)
// One ArrayList<Way> is one multiLine
ArrayList<ArrayList<Way>> multiLines = new ArrayList<ArrayList<Way>>();
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<Way> firstMultiLine = new ArrayList<Way> ();
ArrayList<Way> secondMultiLine = new ArrayList<Way> ();
// iterate over the multiLines, and add the way to the correct one
for( ArrayList<Way> 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<Way> multiLine = new ArrayList<Way>();
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. <br />
* * The first node is the end node of the first way in the multiLine. <br />
* * The second node is the end node of the last way in the multiLine.
*/
private long[] getMultiLineEndNodes(ArrayList<Way> 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()};
}
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() ||
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<Ring> combineToRings(List<Way> ways){
ArrayList<ArrayList<Way>> multiLines = createMultiLines(ways);
SortedSet<Ring> result = new TreeSet<Ring> ();
for (ArrayList<Way> 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<Node> 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 <br />
* automatically added ways because of closing the Ring won't be added <br />
* Only ways with initialized points can be handled.
* @return a List with nodes
*/
public List<Node> collectPoints() {
ArrayList<Node> collected = new ArrayList<Node>();
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<Node> 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 <br />
* 1 if r is inside this Ring <br />
* 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;
}
}

View file

@ -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<Entity, String> 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<Entity, String> entities = ((Relation) e).getMemberEntities();
renderingTypes.encodeEntityWithType(e, mapZooms.getLevel(0).getMaxZoom(), typeUse, addtypeUse, namesUse, tempNameUse);
if (typeUse.size() > 0) {
List<List<Way>> completedRings = new ArrayList<List<Way>>();
List<List<Way>> incompletedRings = new ArrayList<List<Way>>();
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<Way> 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<Node> outerWaySrc = new ArrayList<Node>();
List<List<Node>> innerWays = new ArrayList<List<Node>>();
TIntArrayList typeToSave = new TIntArrayList(typeUse);
long baseId = 0;
for (List<Way> 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<Node> toCollect;
if (innerType) {
toCollect = new ArrayList<Node>();
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<Node> outerWay = outerWaySrc;
int zoomToSimplify = mapZooms.getLevel(level).getMaxZoom() - 1;
if (zoomToSimplify < 15) {
outerWay = simplifyCycleWay(outerWay, zoomToSimplify, zoomWaySmothness);
if (outerWay == null) {
continue nextZoom;
}
List<List<Node>> newinnerWays = new ArrayList<List<Node>>();
for (List<Node> 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<List<Way>> completedRings, Map<Entity, String> entities) {
List<List<Way>> innerWays = new ArrayList<List<Way>>();
Boundary outerBoundary = new Boundary();
Node toReturn = null;
for (List<Way> 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<Way> 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<Way> reverse(List<Way> l) {
Collections.reverse(l);
for(Way w : l){
w.getNodeIds().reverse();
Collections.reverse(w.getNodes());
}
return l;
}
private List<Way> appendLists(List<Way> w1, List<Way> w2){
w1.addAll(w2);
return w1;
}
//TODO Can the Multipolygon class be the one that replaces this?
private void combineMultiPolygons(Way w, List<List<Way>> completedRings, List<List<Way>> 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<Way> l = new ArrayList<Way>();
l.add(w);
boolean add = true;
for (int k = 0; k < incompletedRings.size();) {
boolean remove = false;
List<Way> 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<Multipolygon> multipolygons = original.splitPerOuterRing(logMapDataWarn);
for (Multipolygon m : multipolygons) {
// innerWays are new closed ways
List<List<Node>> innerWays = new ArrayList<List<Node>>();
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<Node> outerWay = m.getOuterNodes();
int zoomToSimplify = mapZooms.getLevel(level).getMaxZoom() - 1;
if (zoomToSimplify < 15) {
outerWay = simplifyCycleWay(outerWay, zoomToSimplify, zoomWaySmothness);
if (outerWay == null) {
continue nextZoom;
}
List<List<Node>> newinnerWays = new ArrayList<List<Node>>();
for (List<Node> 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<Node> simplifyCycleWay(List<Node> ns, int zoom, int zoomWaySmothness) throws SQLException {
if (checkForSmallAreas(ns, zoom + Math.min(zoomWaySmothness / 2, 3), 2, 4)) {
return null;