Fix indexing of multipolygon with multiple outer rings

issue 1265
This commit is contained in:
Sander Deryckere 2012-09-12 15:44:11 +02:00
parent 0a8669edae
commit 107c814dca
3 changed files with 225 additions and 231 deletions

View file

@ -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<Way> 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<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;
}
/**
@ -122,7 +155,7 @@ public class Multipolygon {
* get the Inner Rings
* @return the inner rings
*/
private SortedSet<Ring> getInnerRings() {
public SortedSet<Ring> getInnerRings() {
groupInRings();
return innerRings;
}
@ -131,7 +164,7 @@ public class Multipolygon {
* get the outer rings
* @return outer rings
*/
private SortedSet<Ring> getOuterRings() {
public SortedSet<Ring> 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<Ring> 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<Multipolygon> splitPerOuterRing() {
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)) {
@ -289,15 +349,31 @@ public class Multipolygon {
}
}
SortedSet<Ring> thisOuter = new TreeSet<Ring>();
// 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

@ -86,6 +86,20 @@ public class Ring implements Comparable<Ring>{
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<Ring>{
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<Ring>{
* 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<Ring>{
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<Ring>{
n2 = multiLine.get(lastIdx).getFirstNodeId();
}
return new long[] {n1, n2};
}

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;