/*
* Copyright (C) 2006, 2011.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package uk.me.parabola.mkgmap.reader.osm.boundary;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
import uk.me.parabola.mkgmap.reader.osm.Relation;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.util.Java2DConverter;
public class BoundaryRelation
extends MultiPolygonRelation
{
private static final Logger log =
Logger
.
getLogger(BoundaryRelation.
class);
private java.
awt.
geom.
Area outerResultArea
;
/** keeps the result of the multipolygon processing */
private Boundary boundary
;
public BoundaryRelation
(Relation other,
Map<Long, Way
> wayMap,
Area bbox
) {
super(other, wayMap, bbox
);
}
public Boundary getBoundary
() {
if (boundary ==
null) {
if (outerResultArea ==
null) {
return null;
}
boundary =
new Boundary
(outerResultArea,
this.
getEntryIteratable(),
"r"+
this.
getId());
outerResultArea =
null;
}
return boundary
;
}
/**
* Process the ways in this relation. Joins way with the role "outer" Adds
* ways with the role "inner" to the way with the role "outer"
*/
public void processElements
() {
log.
info("Processing multipolygon", toBrowseURL
());
List<Way
> allWays = getSourceWays
();
// // check if the multipolygon itself or the non inner member ways have a tag
// // if not it does not make sense to process it and we could save the time
// boolean shouldProcess = hasStyleRelevantTags(this);
// if (shouldProcess == false) {
// for (Way w : allWays) {
// shouldProcess = hasStyleRelevantTags(w);
// if (shouldProcess) {
// break;
// }
// }
// }
// if (shouldProcess==false) {
// log.info("Do not process multipolygon",getId(),"because it has no style relevant tags.");
// return;
// }
// create an Area for the bbox to clip the polygons
bboxArea = Java2DConverter.
createBoundsArea(getBbox
());
// join all single ways to polygons, try to close ways and remove non closed ways
polygons = joinWays
(allWays
);
outerWaysForLineTagging =
new HashSet<Way
>();
outerTags =
new HashMap<String,
String>();
removeOutOfBbox
(polygons
);
boolean changed =
true;
while (changed
) {
changed =
false;
while (connectUnclosedWays
(polygons
)) {
changed =
true;
}
closeWays
(polygons
);
}
removeUnclosedWays
(polygons
);
// now only closed ways are left => polygons only
// check if we have at least one polygon left
if (polygons.
isEmpty()) {
// do nothing
log.
info("Multipolygon " + toBrowseURL
()
+
" does not contain a closed polygon.");
tagOuterWays
();
cleanup
();
return;
}
removeWaysOutsideBbox
(polygons
);
if (polygons.
isEmpty()) {
// do nothing
log.
info("Multipolygon " + toBrowseURL
()
+
" is completely outside the bounding box. It is not processed.");
tagOuterWays
();
cleanup
();
return;
}
// the intersectingPolygons marks all intersecting/overlapping polygons
intersectingPolygons =
new HashSet<JoinedWay
>();
// check which polygons lie inside which other polygon
createContainsMatrix
(polygons
);
// unfinishedPolygons marks which polygons are not yet processed
unfinishedPolygons =
new BitSet(polygons.
size());
unfinishedPolygons.
set(0, polygons.
size());
// create bitsets which polygons belong to the outer and to the inner role
innerPolygons =
new BitSet();
taggedInnerPolygons =
new BitSet();
outerPolygons =
new BitSet();
taggedOuterPolygons =
new BitSet();
int wi =
0;
for (Way w : polygons
) {
String role = getRole
(w
);
if ("inner".
equals(role
)) {
innerPolygons.
set(wi
);
taggedInnerPolygons.
set(wi
);
} else if ("outer".
equals(role
)) {
outerPolygons.
set(wi
);
taggedOuterPolygons.
set(wi
);
} else {
// unknown role => it could be both
innerPolygons.
set(wi
);
outerPolygons.
set(wi
);
}
wi++
;
}
if (outerPolygons.
isEmpty()) {
log.
warn("Multipolygon", toBrowseURL
(),
"does not contain any way tagged with role=outer or empty role.");
cleanup
();
return;
}
Queue<PolygonStatus
> polygonWorkingQueue =
new LinkedBlockingQueue<PolygonStatus
>();
BitSet nestedOuterPolygons =
new BitSet();
BitSet nestedInnerPolygons =
new BitSet();
BitSet outmostPolygons
;
BitSet outmostInnerPolygons =
new BitSet();
boolean outmostInnerFound
;
do {
outmostInnerFound =
false;
outmostPolygons = findOutmostPolygons
(unfinishedPolygons
);
if (outmostPolygons.
intersects(taggedInnerPolygons
)) {
outmostInnerPolygons.
or(outmostPolygons
);
outmostInnerPolygons.
and(taggedInnerPolygons
);
if (log.
isDebugEnabled())
log.
debug("wrong inner polygons: " + outmostInnerPolygons
);
// do not process polygons tagged with role=inner but which are
// not contained by any other polygon
unfinishedPolygons.
andNot(outmostInnerPolygons
);
outmostPolygons.
andNot(outmostInnerPolygons
);
outmostInnerFound =
true;
}
} while (outmostInnerFound
);
if (!outmostPolygons.
isEmpty()) {
polygonWorkingQueue.
addAll(getPolygonStatus
(outmostPolygons,
"outer"));
}
boolean outmostPolygonProcessing =
true;
outerResultArea =
new java.
awt.
geom.
Area();
while (!polygonWorkingQueue.
isEmpty()) {
// the polygon is not contained by any other unfinished polygon
PolygonStatus currentPolygon = polygonWorkingQueue.
poll();
// this polygon is now processed and should not be used by any
// further step
unfinishedPolygons.
clear(currentPolygon.
index);
BitSet polygonContains =
new BitSet();
polygonContains.
or(containsMatrix.
get(currentPolygon.
index));
// use only polygon that are contained by the polygon
polygonContains.
and(unfinishedPolygons
);
// polygonContains is the intersection of the unfinished and
// the contained polygons
// get the holes
// these are all polygons that are in the main polygon
// and that are not contained by any other polygon
boolean holesOk
;
BitSet holeIndexes
;
do {
holeIndexes = findOutmostPolygons
(polygonContains
);
holesOk =
true;
if (currentPolygon.
outer) {
// for role=outer only role=inner is allowed
if (holeIndexes.
intersects(taggedOuterPolygons
)) {
BitSet addOuterNestedPolygons =
new BitSet();
addOuterNestedPolygons.
or(holeIndexes
);
addOuterNestedPolygons.
and(taggedOuterPolygons
);
nestedOuterPolygons.
or(addOuterNestedPolygons
);
holeIndexes.
andNot(addOuterNestedPolygons
);
// do not process them
unfinishedPolygons.
andNot(addOuterNestedPolygons
);
polygonContains.
andNot(addOuterNestedPolygons
);
// recalculate the holes again to get all inner polygons
// in the nested outer polygons
holesOk =
false;
}
} else {
// for role=inner both role=inner and role=outer is supported
// although inner in inner is not officially allowed
if (holeIndexes.
intersects(taggedInnerPolygons
)) {
// process inner in inner but issue a warning later
BitSet addInnerNestedPolygons =
new BitSet();
addInnerNestedPolygons.
or(holeIndexes
);
addInnerNestedPolygons.
and(taggedInnerPolygons
);
nestedInnerPolygons.
or(addInnerNestedPolygons
);
}
}
} while (!holesOk
);
ArrayList<PolygonStatus
> holes = getPolygonStatus
(holeIndexes,
(currentPolygon.
outer ? "inner" :
"outer"));
// these polygons must all be checked for holes
polygonWorkingQueue.
addAll(holes
);
if (currentPolygon.
outer) {
// add the original ways to the list of ways that get the line tags of the mp
// the joined ways may be changed by the auto closing algorithm
outerWaysForLineTagging.
addAll(currentPolygon.
polygon.
getOriginalWays());
}
if (currentPolygon.
outer) {
java.
awt.
geom.
Area toAdd = Java2DConverter.
createArea(currentPolygon.
polygon.
getPoints());
if (outerResultArea.
isEmpty())
outerResultArea = toAdd
;
else
outerResultArea.
add(toAdd
);
for (Way outerWay : currentPolygon.
polygon.
getOriginalWays()) {
if (outmostPolygonProcessing
) {
for (Entry
<String,
String> tag : outerWay.
getEntryIteratable()) {
outerTags.
put(tag.
getKey(), tag.
getValue());
}
outmostPolygonProcessing =
false;
} else {
for (String tag :
new ArrayList<String>(outerTags.
keySet())) {
if (outerTags.
get(tag
).
equals(outerWay.
getTag(tag
)) ==
false) {
outerTags.
remove(tag
);
}
}
}
}
} else {
outerResultArea.
subtract(Java2DConverter
.
createArea(currentPolygon.
polygon.
getPoints()));
}
}
// TODO tagging of the outer ways
// if (log.isLoggable(Level.WARNING) &&
// (outmostInnerPolygons.cardinality()+unfinishedPolygons.cardinality()+nestedOuterPolygons.cardinality()+nestedInnerPolygons.cardinality() >= 1)) {
// log.warn("Multipolygon", toBrowseURL(), "contains errors.");
//
// BitSet outerUnusedPolys = new BitSet();
// outerUnusedPolys.or(unfinishedPolygons);
// outerUnusedPolys.or(outmostInnerPolygons);
// outerUnusedPolys.or(nestedOuterPolygons);
// outerUnusedPolys.or(nestedInnerPolygons);
// outerUnusedPolys.or(unfinishedPolygons);
// // use only the outer polygons
// outerUnusedPolys.and(outerPolygons);
// for (JoinedWay w : getWaysFromPolygonList(outerUnusedPolys)) {
// outerWaysForLineTagging.addAll(w.getOriginalWays());
// }
//
// runIntersectionCheck(unfinishedPolygons);
// runOutmostInnerPolygonCheck(outmostInnerPolygons);
// runNestedOuterPolygonCheck(nestedOuterPolygons);
// runNestedInnerPolygonCheck(nestedInnerPolygons);
// runWrongInnerPolygonCheck(unfinishedPolygons, innerPolygons);
//
// // we have at least one polygon that could not be processed
// // Probably we have intersecting or overlapping polygons
// // one possible reason is if the relation overlaps the tile
// // bounds
// // => issue a warning
// List<JoinedWay> lostWays = getWaysFromPolygonList(unfinishedPolygons);
// for (JoinedWay w : lostWays) {
// log.warn("Polygon", w, "is not processed due to an unknown reason.");
// logWayURLs(Level.WARNING, "-", w);
// }
// }
//
if (hasStyleRelevantTags
(this)) {
outerTags.
clear();
for (Entry
<String,
String> mpTags : getEntryIteratable
()) {
if ("type".
equals(mpTags.
getKey())==
false) {
outerTags.
put(mpTags.
getKey(), mpTags.
getValue());
}
}
} else {
for (Entry
<String,
String> mpTags : outerTags.
entrySet()) {
addTag
(mpTags.
getKey(), mpTags.
getValue());
}
}
// Go through all original outer ways, create a copy, tag them
// with the mp tags and mark them only to be used for polyline processing
// This enables the style file to decide if the polygon information or
// the simple line information should be used.
for (Way orgOuterWay : outerWaysForLineTagging
) {
// Way lineTagWay = new Way(FakeIdGenerator.makeFakeId(), orgOuterWay.getPoints());
// lineTagWay.setName(orgOuterWay.getName());
// lineTagWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_LINE);
for (Entry
<String,
String> tag : outerTags.
entrySet()) {
// lineTagWay.addTag(tag.getKey(), tag.getValue());
// remove the tag from the original way if it has the same value
if (tag.
getValue().
equals(orgOuterWay.
getTag(tag.
getKey()))) {
removeTagsInOrgWays
(orgOuterWay, tag.
getKey());
}
}
// if (log.isDebugEnabled())
// log.debug("Add line way", lineTagWay.getId(), lineTagWay.toTagString());
// tileWayMap.put(lineTagWay.getId(), lineTagWay);
}
postProcessing
();
cleanup
();
}
protected boolean connectUnclosedWays
(List<JoinedWay
> allWays
) {
List<JoinedWay
> unclosed =
new ArrayList<JoinedWay
>();
for (JoinedWay w : allWays
) {
if (w.
isClosed() ==
false) {
unclosed.
add(w
);
}
}
// try to connect ways lying outside or on the bbox
if (unclosed.
size() >=
2) {
log.
debug("Checking",unclosed.
size(),
"unclosed ways for connections outside the bbox");
Map<Coord, JoinedWay
> outOfBboxPoints =
new HashMap<Coord, JoinedWay
>();
// check all ways for endpoints outside or on the bbox
for (JoinedWay w : unclosed
) {
Coord c1 = w.
getPoints().
get(0);
// if (bbox.insideBoundary(c1)==false) {
// log.debug("Point",c1,"of way",w.getId(),"outside bbox");
outOfBboxPoints.
put(c1, w
);
// }
Coord c2 = w.
getPoints().
get(w.
getPoints().
size()-
1);
// if (bbox.insideBoundary(c2)==false) {
// log.debug("Point",c2,"of way",w.getId(),"outside bbox");
outOfBboxPoints.
put(c2, w
);
// }
}
if (outOfBboxPoints.
size() < 2) {
log.
debug(outOfBboxPoints.
size(),
"point outside the bbox. No connection possible.");
return false;
}
List<ConnectionData
> coordPairs =
new ArrayList<ConnectionData
>();
ArrayList<Coord
> coords =
new ArrayList<Coord
>(outOfBboxPoints.
keySet());
for (int i =
0; i
< coords.
size(); i++
) {
for (int j = i +
1; j
< coords.
size(); j++
) {
ConnectionData cd =
new ConnectionData
();
cd.
c1 = coords.
get(i
);
cd.
c2 = coords.
get(j
);
cd.
w1 = outOfBboxPoints.
get(cd.
c1);
cd.
w2 = outOfBboxPoints.
get(cd.
c2);
// if (lineCutsBbox(cd.c1, cd.c2 )) {
// // Check if the way can be closed with one additional point
// // outside the bounding box.
// // The additional point is combination of the coords of both endpoints.
// // It works if the lines from the endpoints to the additional point does
// // not cut the bounding box.
// // This can be removed when the splitter guarantees to provide logical complete
// // multi-polygons.
// Coord edgePoint1 = new Coord(cd.c1.getLatitude(), cd.c2
// .getLongitude());
// Coord edgePoint2 = new Coord(cd.c2.getLatitude(), cd.c1
// .getLongitude());
//
// if (lineCutsBbox(cd.c1, edgePoint1) == false
// && lineCutsBbox(edgePoint1, cd.c2) == false) {
// cd.imC = edgePoint1;
// } else if (lineCutsBbox(cd.c1, edgePoint2) == false
// && lineCutsBbox(edgePoint2, cd.c2) == false) {
// cd.imC = edgePoint1;
// } else {
// // both endpoints are on opposite sides of the bounding box
// // automatically closing such points would create wrong polygons in most cases
// continue;
// }
// cd.distance = cd.c1.distance(cd.imC) + cd.imC.distance(cd.c2);
// } else {
cd.
distance = cd.
c1.
distance(cd.
c2);
// }
coordPairs.
add(cd
);
}
}
if (coordPairs.
isEmpty()) {
log.
debug("All potential connections cross the bbox. No connection possible.");
return false;
} else {
// retrieve the connection with the minimum distance
ConnectionData minCon =
Collections.
min(coordPairs,
new Comparator<ConnectionData
>() {
public int compare
(ConnectionData o1,
ConnectionData o2
) {
return Double.
compare(o1.
distance, o2.
distance);
}
});
if (minCon.
distance < getMaxCloseDist
()) {
if (minCon.
w1 == minCon.
w2) {
log.
debug("Close a gap in way", minCon.
w1);
if (minCon.
imC !=
null)
minCon.
w1.
getPoints().
add(minCon.
imC);
minCon.
w1.
closeWayArtificially();
} else {
log.
debug("Connect", minCon.
w1,
"with", minCon.
w2);
if (minCon.
w1.
getPoints().
get(0).
equals(minCon.
c1)) {
Collections.
reverse(minCon.
w1.
getPoints());
}
if (minCon.
w2.
getPoints().
get(0).
equals(minCon.
c2) ==
false) {
Collections.
reverse(minCon.
w2.
getPoints());
}
minCon.
w1.
getPoints().
addAll(minCon.
w2.
getPoints());
minCon.
w1.
addWay(minCon.
w2);
allWays.
remove(minCon.
w2);
}
return true;
}
}
}
return false;
}
private double getMaxCloseDist
() {
double dist =
1000;
String admString= getTag
("admin_level");
if ("2".
equals(admString
)) {
dist =
50000;
} else if ("3".
equals(admString
)) {
dist =
20000;
}else if ("4".
equals(admString
)) {
dist =
4000;
}
return dist
;
}
protected void closeWays
(ArrayList<JoinedWay
> wayList
) {
for (JoinedWay way : wayList
) {
if (way.
isClosed() || way.
getPoints().
size() < 3) {
continue;
}
Coord p1 = way.
getPoints().
get(0);
Coord p2 = way.
getPoints().
get(way.
getPoints().
size() -
1);
if (getBbox
().
insideBoundary(p1
) ==
false
&& getBbox
().
insideBoundary(p2
) ==
false) {
// both points lie outside the bbox or on the bbox
// check if both points are on the same side of the bounding box
if ((p1.
getLatitude() <= getBbox
().
getMinLat() && p2.
getLatitude() <= getBbox
()
.
getMinLat())
||
(p1.
getLatitude() >= getBbox
().
getMaxLat() && p2
.
getLatitude() >= getBbox
().
getMaxLat())
||
(p1.
getLongitude() <= getBbox
().
getMinLong() && p2
.
getLongitude() <= getBbox
().
getMinLong())
||
(p1.
getLongitude() >= getBbox
().
getMaxLong() && p2
.
getLongitude() >= getBbox
().
getMaxLong())) {
// they are on the same side outside of the bbox
// so just close them without worrying about if
// they intersect itself because the intersection also
// is outside the bbox
way.
closeWayArtificially();
log.
info("Endpoints of way", way,
"are both outside the bbox. Closing it directly.");
continue;
}
}
Line2D closingLine =
new Line2D.Float(p1.
getLongitude(), p1
.
getLatitude(), p2.
getLongitude(), p2.
getLatitude());
boolean intersects =
false;
Coord lastPoint =
null;
// don't use the first and the last point
// the closing line can intersect only in one point or complete.
// Both isn't interesting for this check
for (Coord thisPoint : way.
getPoints().
subList(1,
way.
getPoints().
size() -
1)) {
if (lastPoint
!=
null) {
if (closingLine.
intersectsLine(lastPoint.
getLongitude(),
lastPoint.
getLatitude(), thisPoint.
getLongitude(),
thisPoint.
getLatitude())) {
intersects =
true;
break;
}
}
lastPoint = thisPoint
;
}
if (!intersects
) {
// close the polygon
// the new way segment does not intersect the rest of the
// polygon
// calc the distance to close
double closeDist = way.
getPoints().
get(0).
distance(way.
getPoints().
get(way.
getPoints().
size()-
1));
if (closeDist
<= getMaxCloseDist
()) {
log.
info("Closing way", way
);
log.
info("from", way.
getPoints().
get(0).
toOSMURL());
log.
info("to", way.
getPoints().
get(way.
getPoints().
size() -
1)
.
toOSMURL());
// mark this ways as artificially closed
way.
closeWayArtificially();
}
}
}
}
private void removeOutOfBbox
(List<JoinedWay
> polygons
) {
ListIterator<JoinedWay
> pIter = polygons.
listIterator();
while (pIter.
hasNext()) {
JoinedWay w = pIter.
next();
if (w.
isClosed() ==
false) {
// the way is not closed
// check if one of start/endpoint is out of the bounding box
// in this case it is too risky to close it
if (getBbox
().
contains(w.
getPoints().
get(0)) ==
false
|| getBbox
().
contains(
w.
getPoints().
get(w.
getPoints().
size() -
1)) ==
false) {
pIter.
remove();
}
}
}
}
protected void cleanup
() {
super.
cleanup();
this.
getElements().
clear();
((ArrayList<?>)this.
getElements()).
trimToSize();
}
}