/*
* Copyright (C) 2011-2014.
*
* 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;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.Java2DConverter;
/**
* Representation of an OSM Multipolygon Relation.<br/>
* The different way of the multipolygon are joined to polygons and inner
* polygons are cut out from the outer polygons.
*
* @author WanMil
*/
public class MultiPolygonRelation extends Relation {
private static final Logger log = Logger.getLogger(MultiPolygonRelation.class);
public static final String STYLE_FILTER_TAG = "mkgmap:stylefilter";
public static final String STYLE_FILTER_LINE = "polyline";
public static final String STYLE_FILTER_POLYGON = "polygon";
/** A tag that is set with value true on each polygon that is created by the mp processing */
public static final String MP_CREATED_TAG = "mkgmap:mp_created";
private final Map<Long, Way> tileWayMap;
private final Map<Long, String> roleMap = new HashMap<Long, String>();
private Map<Long, Way> mpPolygons = new LinkedHashMap<Long, Way>();
protected ArrayList<BitSet> containsMatrix;
protected ArrayList<JoinedWay> polygons;
protected Set<JoinedWay> intersectingPolygons;
protected double largestSize;
protected JoinedWay largestOuterPolygon;
protected Set<Way> outerWaysForLineTagging;
protected Map<String, String> outerTags;
private final uk.me.parabola.imgfmt.app.Area bbox;
protected Area bboxArea;
private Coord cOfG = null;
// the sum of all outer polygons area size
private double mpAreaSize = 0;
/**
* A point that has a lower or equal squared distance from
* a line is treated as if it lies one the line.<br/>
* 1.0d is very exact. 2.0d covers rounding problems when converting
* OSM locations to mkgmap internal format. A larger value
* is more tolerant against imprecise OSM data.
*/
private static final double OVERLAP_TOLERANCE_DISTANCE = 2.0d;
/**
* Create an instance based on an existing relation. We need to do this
* because the type of the relation is not known until after all its tags
* are read in.
*
* @param other
* The relation to base this one on.
* @param wayMap
* Map of all ways.
* @param bbox
* The bounding box of the tile
*/
public MultiPolygonRelation(Relation other, Map<Long, Way> wayMap,
uk.me.parabola.imgfmt.app.Area bbox) {
this.tileWayMap = wayMap;
this.bbox = bbox;
setId(other.getId());
copyTags(other);
if (log.isDebugEnabled()) {
log.debug("Construct multipolygon", toBrowseURL(), toTagString());
}
for (Map.Entry<String, Element> pair : other.getElements()) {
String role = pair.getKey();
Element el = pair.getValue();
if (log.isDebugEnabled()) {
log.debug(" ", role, el.toBrowseURL(), el.toTagString());
}
if (roleMap.containsKey(el.getId()) )
log.warn("repeated member with id ", el.getId(), "in multipolygon relation",this.getId(),"is ignored");
else {
addElement(role, el);
roleMap.put(el.getId(), role);
}
}
}
/**
* Retrieves the center point of this multipolygon. This is set in the
* {@link #processElements()} methods so it returns <code>null</code>
* before that. It can also return <code>null</code> in case the
* multipolygon could not be processed.<br/>
* The returned point may lie outside the multipolygon area. It is just
* the center point of it.
*
* @return the center point of this multipolygon (maybe <code>null</code>)
*/
public Coord getCofG() {
return cOfG;
}
/**
* Retrieves the mp role of the given element.
*
* @param element
* the element
* @return the role of the element
*/
protected String getRole(Element element) {
String role = roleMap.get(element.getId());
if (role != null && ("outer".equals(role) || "inner".equals(role))) {
return role;
}
return null;
}
/**
* Try to join the two ways.
*
* @param joinWay
* the way to which tempWay is added in case both ways could be
* joined and checkOnly is false.
* @param tempWay
* the way to be added to joinWay
* @param checkOnly
* <code>true</code> checks only and does not perform the join
* operation
* @return <code>true</code> if tempWay way is (or could be) joined to
* joinWay
*/
private boolean joinWays(JoinedWay joinWay, JoinedWay tempWay,
boolean checkOnly) {
boolean reverseTempWay = false;
int insIdx = -1;
int firstTmpIdx = 1;
boolean joinable = false;
// use == or equals as comparator??
if (joinWay.getPoints().get(0) == tempWay.getPoints().get(0)) {
insIdx = 0;
reverseTempWay = true;
firstTmpIdx = 1;
joinable = true;
} else if (joinWay.getPoints().get(joinWay.getPoints().size() - 1) == tempWay
.getPoints().get(0)) {
insIdx = joinWay.getPoints().size();
reverseTempWay = false;
firstTmpIdx = 1;
joinable = true;
} else if (joinWay.getPoints().get(0) == tempWay.getPoints().get(
tempWay.getPoints().size() - 1)) {
insIdx = 0;
reverseTempWay = false;
firstTmpIdx = 0;
joinable = true;
} else if (joinWay.getPoints().get(joinWay.getPoints().size() - 1) == tempWay
.getPoints().get(tempWay.getPoints().size() - 1)) {
insIdx = joinWay.getPoints().size();
reverseTempWay = true;
firstTmpIdx = 0;
joinable = true;
}
if (!checkOnly && joinable){
int lastIdx = tempWay.getPoints().size();
if (firstTmpIdx == 0) {
// the last temp point is already contained in the joined way - do not copy it
lastIdx--;
}
List<Coord> tempCoords = tempWay.getPoints().subList(firstTmpIdx,lastIdx);
if (reverseTempWay) {
// the remp coords need to be reversed so copy the list
tempCoords = new ArrayList<Coord>(tempCoords);
// and reverse it
Collections.reverse(tempCoords);
}
joinWay.getPoints().addAll(insIdx, tempCoords);
joinWay.addWay(tempWay);
}
return joinable;
}
/**
* Combine a list of way segments to a list of maximally joined ways
*
* @param segments
* a list of closed or unclosed ways
* @return a list of closed ways
*/
protected ArrayList<JoinedWay> joinWays(List<Way> segments) {
// TODO check if the closed polygon is valid and implement a
// backtracking algorithm to get other combinations
ArrayList<JoinedWay> joinedWays = new ArrayList<JoinedWay>();
if (segments == null || segments.isEmpty()) {
return joinedWays;
}
// go through all segments and categorize them to closed and unclosed
// list
ArrayList<JoinedWay> unclosedWays = new ArrayList<JoinedWay>();
for (Way orgSegment : segments) {
JoinedWay jw = new JoinedWay(orgSegment);
roleMap.put(jw.getId(), getRole(orgSegment));
if (orgSegment.isClosed()) {
if (orgSegment.isComplete() == false) {
// the way is closed in planet but some points are missing in this tile
// we can close it artificially
if (log.isDebugEnabled())
log.debug("Close incomplete but closed polygon:",orgSegment);
jw.closeWayArtificially();
}
assert jw.hasIdenticalEndPoints() : "way is not closed";
joinedWays.add(jw);
} else {
unclosedWays.add(jw);
}
}
while (!unclosedWays.isEmpty()) {
JoinedWay joinWay = unclosedWays.remove(0);
// check if the current way is already closed or if it is the last
// way
if (joinWay.hasIdenticalEndPoints() || unclosedWays.isEmpty()) {
joinedWays.add(joinWay);
continue;
}
boolean joined = false;
// if we have a way that could be joined but which has a wrong role
// then store it here and check in the end if it's working
JoinedWay wrongRoleWay = null;
String joinRole = getRole(joinWay);
// go through all ways and check if there is a way that can be
// joined with it
// in this case join the two ways
// => add all points of tempWay to joinWay, remove tempWay and put
// joinWay to the beginning of the list
// (not optimal but understandable - can be optimized later)
for (JoinedWay tempWay : unclosedWays) {
if (tempWay.hasIdenticalEndPoints()) {
continue;
}
String tempRole = getRole(tempWay);
// if a role is not 'inner' or 'outer' then it is used as
// universal
// check if the roles of the ways are matching
if ((!"outer".equals(joinRole) && !"inner"
.equals(joinRole))
|| (!"outer".equals(tempRole) && !"inner"
.equals(tempRole))
|| (joinRole != null && joinRole.equals(tempRole))) {
// the roles are matching => try to join both ways
joined = joinWays(joinWay, tempWay, false);
} else {
// the roles are not matching => test if both ways would
// join
// as long as we don't have an alternative way with wrong
// role
// or if the alternative way is shorter then check if
// the way with the wrong role could be joined
if (wrongRoleWay == null
|| wrongRoleWay.getPoints().size() < tempWay
.getPoints().size()) {
if (joinWays(joinWay, tempWay, true)) {
// save this way => maybe we will use it in the end
// if we don't find any other way
wrongRoleWay = tempWay;
}
}
}
if (joined) {
// we have joined the way
unclosedWays.remove(tempWay);
break;
}
}
if (!joined && wrongRoleWay != null) {
log.warn("Join ways with different roles. Multipolygon: "
+ toBrowseURL());
log.warn("Way1 Role:", getRole(joinWay));
logWayURLs(Level.WARNING, "-", joinWay);
log.warn("Way2 Role:", getRole(wrongRoleWay));
logWayURLs(Level.WARNING, "-", wrongRoleWay);
joined = joinWays(joinWay, wrongRoleWay, false);
if (joined) {
// we have joined the way
unclosedWays.remove(wrongRoleWay);
break;
}
}
if (joined) {
if (joinWay.hasIdenticalEndPoints()) {
// it's closed => don't process it again
joinedWays.add(joinWay);
} else if (unclosedWays.isEmpty()) {
// no more ways to join with
// it's not closed but we cannot join it more
joinedWays.add(joinWay);
} else {
// it is not yet closed => process it once again
unclosedWays.add(0, joinWay);
}
} else {
// it's not closed but we cannot join it more
joinedWays.add(joinWay);
}
}
return joinedWays;
}
/**
* Try to close all unclosed ways in the given list of ways.
*
* @param wayList
* a list of ways
*/
protected void closeWays(ArrayList<JoinedWay> wayList) {
for (JoinedWay way : wayList) {
if (way.hasIdenticalEndPoints() || way.getPoints().size() < 3) {
continue;
}
Coord p1 = way.getPoints().get(0);
Coord p2 = way.getPoints().get(way.getPoints().size() - 1);
if (bbox.insideBoundary(p1) == false
&& bbox.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() <= bbox.getMinLat() && p2.getLatitude() <= bbox
.getMinLat())
|| (p1.getLatitude() >= bbox.getMaxLat() && p2
.getLatitude() >= bbox.getMaxLat())
|| (p1.getLongitude() <= bbox.getMinLong() && p2
.getLongitude() <= bbox.getMinLong())
|| (p1.getLongitude() >= bbox.getMaxLong() && p2
.getLongitude() >= bbox.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
if (log.isInfoEnabled()){
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();
}
}
}
protected static class ConnectionData {
public Coord c1;
public Coord c2;
public JoinedWay w1;
public JoinedWay w2;
// sometimes the connection of both points cannot be done directly but with an intermediate point
public Coord imC;
public double distance;
public ConnectionData() {
}
}
protected boolean connectUnclosedWays(List<JoinedWay> allWays) {
List<JoinedWay> unclosed = new ArrayList<JoinedWay>();
for (JoinedWay w : allWays) {
if (w.hasIdenticalEndPoints() == 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 IdentityHashMap<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.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) == minCon.c1) {
Collections.reverse(minCon.w1.getPoints());
}
if (minCon.w2.getPoints().get(0) != minCon.c2) {
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;
}
/**
* Removes all ways non closed ways from the given list (
* <code>{@link Way#hasIdenticalEndPoints()} == false</code>)
*
* @param wayList
* list of ways
*/
protected void removeUnclosedWays(ArrayList<JoinedWay> wayList) {
Iterator<JoinedWay> it = wayList.iterator();
boolean firstWarn = true;
while (it.hasNext()) {
JoinedWay tempWay = it.next();
if (!tempWay.hasIdenticalEndPoints()) {
// warn only if the way intersects the bounding box
boolean inBbox = tempWay.intersects(bbox);
if (inBbox) {
if (firstWarn) {
log.warn(
"Cannot join the following ways to closed polygons. Multipolygon",
toBrowseURL(), toTagString());
firstWarn = false;
}
logWayURLs(Level.WARNING, "- way:", tempWay);
logFakeWayDetails(Level.WARNING, tempWay);
}
it.remove();
if (inBbox) {
String role = getRole(tempWay);
if (role == null || "".equals(role) || "outer".equals(role)) {
// anyhow add the ways to the list for line tagging
outerWaysForLineTagging.addAll(tempWay.getOriginalWays());
}
}
}
}
}
/**
* Removes all ways that are completely outside the bounding box.
* This reduces error messages from problems on the tile bounds.
* @param wayList list of ways
*/
protected void removeWaysOutsideBbox(ArrayList<JoinedWay> wayList) {
ListIterator<JoinedWay> wayIter = wayList.listIterator();
while (wayIter.hasNext()) {
JoinedWay w = wayIter.next();
boolean remove = true;
// check all points
for (Coord c : w.getPoints()) {
if (bbox.contains(c)) {
// if one point is in the bounding box the way should not be removed
remove = false;
break;
}
}
if (remove) {
// check if the polygon contains the complete bounding box
if (w.getBounds().contains(bboxArea.getBounds())) {
remove = false;
}
}
if (remove) {
if (log.isDebugEnabled()) {
log.debug("Remove way", w.getId(),
"because it is completely outside the bounding box.");
}
wayIter.remove();
}
}
}
/**
* Find all polygons that are not contained by any other polygon.
*
* @param candidates
* all polygons that should be checked
* @param roleFilter
* an additional filter
* @return all polygon indexes that are not contained by any other polygon
*/
private BitSet findOutmostPolygons(BitSet candidates, BitSet roleFilter) {
BitSet realCandidates = ((BitSet) candidates.clone());
realCandidates.and(roleFilter);
return findOutmostPolygons(realCandidates);
}
/**
* Finds all polygons that are not contained by any other polygons and that match
* to the given role. All polygons with index given by <var>candidates</var>
* are used.
*
* @param candidates
* indexes of the polygons that should be used
* @return the bits of all outermost polygons are set to true
*/
protected BitSet findOutmostPolygons(BitSet candidates) {
BitSet outmostPolygons = new BitSet();
// go through all candidates and check if they are contained by any
// other candidate
for (int candidateIndex = candidates.nextSetBit(0); candidateIndex >= 0; candidateIndex = candidates
.nextSetBit(candidateIndex + 1)) {
// check if the candidateIndex polygon is not contained by any
// other candidate polygon
boolean isOutmost = true;
for (int otherCandidateIndex = candidates.nextSetBit(0); otherCandidateIndex >= 0; otherCandidateIndex = candidates
.nextSetBit(otherCandidateIndex + 1)) {
if (contains(otherCandidateIndex, candidateIndex)) {
// candidateIndex is not an outermost polygon because it is
// contained by the otherCandidateIndex polygon
isOutmost = false;
break;
}
}
if (isOutmost) {
// this is an outermost polygon
// put it to the bitset
outmostPolygons.set(candidateIndex);
}
}
return outmostPolygons;
}
protected ArrayList<PolygonStatus> getPolygonStatus(BitSet outmostPolygons,
String defaultRole) {
ArrayList<PolygonStatus> polygonStatusList = new ArrayList<PolygonStatus>();
for (int polyIndex = outmostPolygons.nextSetBit(0); polyIndex >= 0; polyIndex = outmostPolygons
.nextSetBit(polyIndex + 1)) {
// polyIndex is the polygon that is not contained by any other
// polygon
JoinedWay polygon = polygons.get(polyIndex);
String role = getRole(polygon);
// if the role is not explicitly set use the default role
if (role == null || "".equals(role)) {
role = defaultRole;
}
polygonStatusList.add(new PolygonStatus("outer".equals(role), polyIndex, polygon));
}
// sort by role and then by number of points, this improves performance
// in the routines which add the polygons to areas
if (polygonStatusList.size() > 2){
Collections.sort(polygonStatusList, new Comparator<PolygonStatus>() {
public int compare(PolygonStatus o1, PolygonStatus o2) {
if (o1.outer != o2.outer)
return (o1.outer) ? -1 : 1;
return o1.polygon.getPoints().size() - o2.polygon.getPoints().size();
}
});
}
return polygonStatusList;
}
/**
* Creates a list of all original ways of the multipolygon.
* @return all source ways
*/
protected List<Way> getSourceWays() {
ArrayList<Way> allWays = new ArrayList<Way>();
for (Map.Entry<String, Element> r_e : getElements()) {
if (r_e.getValue() instanceof Way) {
if (((Way)r_e.getValue()).getPoints().isEmpty()) {
log.warn("Way",r_e.getValue(),"has no points and cannot be used for the multipolygon",toBrowseURL());
} else {
allWays.add((Way) r_e.getValue());
}
} else if (r_e.getValue() instanceof Node == false ||
("admin_centre".equals(r_e.getKey()) == false && "label".equals(r_e.getKey()) == false)) {
log.warn("Non way member in role", r_e.getKey(), r_e.getValue().toBrowseURL(),
"in multipolygon", toBrowseURL(), toTagString());
}
}
return allWays;
}
// unfinishedPolygons marks which polygons are not yet processed
protected BitSet unfinishedPolygons;
// create bitsets which polygons belong to the outer and to the inner role
protected BitSet innerPolygons;
protected BitSet taggedInnerPolygons;
protected BitSet outerPolygons;
protected BitSet taggedOuterPolygons;
/**
* 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 it makes sense to process the mp
if (isMpProcessable(allWays) == 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>();
closeWays(polygons);
while (connectUnclosedWays(polygons)) {
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) {
w.setFullArea(w.getFullArea()); // trigger setting area before start cutting...
// do like this to disguise function with side effects
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;
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());
}
// calculate the size of the polygon
double outerAreaSize = currentPolygon.polygon.getSizeOfArea();
if (outerAreaSize > largestSize) {
// subtract the holes
for (PolygonStatus hole : holes) {
outerAreaSize -= hole.polygon.getSizeOfArea();
}
// is it still larger than the largest known polygon?
if (outerAreaSize > largestSize) {
largestOuterPolygon = currentPolygon.polygon;
largestSize = outerAreaSize;
}
}
// check if the polygon is an outer polygon or
// if there are some holes
boolean processPolygon = currentPolygon.outer
|| (holes.isEmpty()==false);
if (processPolygon) {
List<Way> singularOuterPolygons;
if (holes.isEmpty()) {
singularOuterPolygons = Collections
.singletonList((Way) new JoinedWay(currentPolygon.polygon));
} else {
List<Way> innerWays = new ArrayList<Way>(holes.size());
for (PolygonStatus polygonHoleStatus : holes) {
innerWays.add(polygonHoleStatus.polygon);
}
singularOuterPolygons = cutOutInnerPolygons(currentPolygon.polygon,
innerWays);
}
if (singularOuterPolygons.isEmpty()==false) {
// handle the tagging
if (currentPolygon.outer && hasStyleRelevantTags(this)) {
// use the tags of the multipolygon
for (Way p : singularOuterPolygons) {
// overwrite all tags
p.copyTags(this);
p.deleteTag("type");
}
// remove the multipolygon tags in the original ways of the current polygon
removeTagsInOrgWays(this, currentPolygon.polygon);
} else {
// use the tags of the original ways
currentPolygon.polygon.mergeTagsFromOrgWays();
for (Way p : singularOuterPolygons) {
// overwrite all tags
p.copyTags(currentPolygon.polygon);
}
// remove the current polygon tags in its original ways
removeTagsInOrgWays(currentPolygon.polygon, currentPolygon.polygon);
}
if (currentPolygon.outer && outmostPolygonProcessing) {
// this is the outer most polygon - copy its tags. They will be used
// later for tagging of the lines
// all cut polygons have the same tags - copy them from the first polygon
Way outerWay = singularOuterPolygons.get(0);
for (Entry<String, String> tag : outerWay.getTagEntryIterator()) {
outerTags.put(tag.getKey(), tag.getValue());
}
outmostPolygonProcessing = false;
}
long fullArea = currentPolygon.polygon.getFullArea();
for (Way mpWay : singularOuterPolygons) {
// put the cut out polygons to the
// final way map
if (log.isDebugEnabled())
log.debug(mpWay.getId(),mpWay.toTagString());
mpWay.setFullArea(fullArea);
// mark this polygons so that only polygon style rules are applied
mpWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_POLYGON);
mpWay.addTag(MP_CREATED_TAG, "true");
if (currentPolygon.outer) {
mpWay.addTag("mkgmap:mp_role", "outer");
if (isAreaSizeCalculated())
mpAreaSize += calcAreaSize(mpWay.getPoints());
} else {
mpWay.addTag("mkgmap:mp_role", "inner");
}
getMpPolygons().put(mpWay.getId(), mpWay);
}
}
}
}
if (log.isLoggable(Level.WARNING) &&
(outmostInnerPolygons.cardinality()+unfinishedPolygons.cardinality()+nestedOuterPolygons.cardinality()+nestedInnerPolygons.cardinality() >= 1)) {
log.warn("Multipolygon", toBrowseURL(), toTagString(), "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) == false) {
// add tags to the multipolygon that are taken from the outer ways
// they may be required by some hooks (e.g. Area2POIHook)
for (Entry<String, String> tags : outerTags.entrySet()) {
addTag(tags.getKey(), tags.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(getOriginalId(), orgOuterWay.getPoints());
lineTagWay.setFakeId();
lineTagWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_LINE);
lineTagWay.addTag(MP_CREATED_TAG, "true");
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 void postProcessing() {
if (isAreaSizeCalculated()) {
// assign the area size of the whole multipolygon to all outer polygons
String mpAreaSizeStr = new DecimalFormat("0.0####################",
DecimalFormatSymbols.getInstance(Locale.US)).format(mpAreaSize);
for (Way w : mpPolygons.values()) {
if ("outer".equals(w.getTag("mkgmap:mp_role"))) {
w.addTag("mkgmap:cache_area_size", mpAreaSizeStr);
}
}
}
for (Way w : mpPolygons.values()) {
w.deleteTag("mkgmap:mp_role");
}
// copy all polygons created by the multipolygon algorithm to the global way map
tileWayMap.putAll(mpPolygons);
if (largestOuterPolygon != null) {
// check if the mp contains a node with role "label"
for (Map.Entry<String, Element> r_e : getElements()) {
if (r_e.getValue() instanceof Node && "label".equals(r_e.getKey())) {
// yes => use the label node as reference point
cOfG = ((Node)r_e.getValue()).getLocation();
break;
}
}
if (cOfG == null) {
// use the center of the largest polygon as reference point
cOfG = largestOuterPolygon.getCofG();
}
}
}
private void runIntersectionCheck(BitSet unfinishedPolys) {
if (intersectingPolygons.isEmpty()) {
// nothing to do
return;
}
log.warn("Some polygons are intersecting. This is not allowed in multipolygons.");
boolean oneOufOfBbox = false;
for (JoinedWay polygon : intersectingPolygons) {
int pi = polygons.indexOf(polygon);
unfinishedPolys.clear(pi);
boolean outOfBbox = false;
for (Coord c : polygon.getPoints()) {
if (!bbox.contains(c)) {
outOfBbox = true;
oneOufOfBbox = true;
break;
}
}
logWayURLs(Level.WARNING, (outOfBbox ? "*" : "-"), polygon);
}
for (JoinedWay polygon : intersectingPolygons) {
// print out the details of the original ways
logFakeWayDetails(Level.WARNING, polygon);
}
if (oneOufOfBbox) {
log.warn("Some of these intersections/overlaps may be caused by incomplete data on bounding box edges (*).");
}
}
private void runNestedOuterPolygonCheck(BitSet nestedOuterPolygons) {
// just print out warnings
// the check has been done before
for (int wiIndex = nestedOuterPolygons.nextSetBit(0); wiIndex >= 0; wiIndex = nestedOuterPolygons
.nextSetBit(wiIndex + 1)) {
JoinedWay outerWay = polygons.get(wiIndex);
log.warn("Polygon", outerWay, "carries role outer but lies inside an outer polygon. Potentially its role should be inner.");
logFakeWayDetails(Level.WARNING, outerWay);
}
}
private void runNestedInnerPolygonCheck(BitSet nestedInnerPolygons) {
// just print out warnings
// the check has been done before
for (int wiIndex = nestedInnerPolygons.nextSetBit(0); wiIndex >= 0; wiIndex = nestedInnerPolygons
.nextSetBit(wiIndex + 1)) {
JoinedWay innerWay = polygons.get(wiIndex);
log.warn("Polygon", innerWay, "carries role", getRole(innerWay), "but lies inside an inner polygon. Potentially its role should be outer.");
logFakeWayDetails(Level.WARNING, innerWay);
}
}
private void runOutmostInnerPolygonCheck(BitSet outmostInnerPolygons) {
// just print out warnings
// the check has been done before
for (int wiIndex = outmostInnerPolygons.nextSetBit(0); wiIndex >= 0; wiIndex = outmostInnerPolygons
.nextSetBit(wiIndex + 1)) {
JoinedWay innerWay = polygons.get(wiIndex);
log.warn("Polygon", innerWay, "carries role", getRole(innerWay), "but is not inside any other polygon. Potentially it does not belong to this multipolygon.");
logFakeWayDetails(Level.WARNING, innerWay);
}
}
private void runWrongInnerPolygonCheck(BitSet unfinishedPolygons,
BitSet innerPolygons) {
// find all unfinished inner polygons that are not contained by any
BitSet wrongInnerPolygons = findOutmostPolygons(unfinishedPolygons, innerPolygons);
if (log.isDebugEnabled()) {
log.debug("unfinished", unfinishedPolygons);
log.debug("inner", innerPolygons);
// other polygon
log.debug("wrong", wrongInnerPolygons);
}
if (!wrongInnerPolygons.isEmpty()) {
// we have an inner polygon that is not contained by any outer polygon
// check if
for (int wiIndex = wrongInnerPolygons.nextSetBit(0); wiIndex >= 0; wiIndex = wrongInnerPolygons
.nextSetBit(wiIndex + 1)) {
BitSet containedPolygons = new BitSet();
containedPolygons.or(unfinishedPolygons);
containedPolygons.and(containsMatrix.get(wiIndex));
JoinedWay innerWay = polygons.get(wiIndex);
if (containedPolygons.isEmpty()) {
log.warn("Polygon", innerWay, "carries role", getRole(innerWay),
"but is not inside any outer polygon. Potentially it does not belong to this multipolygon.");
logFakeWayDetails(Level.WARNING, innerWay);
} else {
log.warn("Polygon", innerWay, "carries role", getRole(innerWay),
"but is not inside any outer polygon. Potentially the roles are interchanged with the following",
(containedPolygons.cardinality() > 1 ? "ways" : "way"), ".");
for (int wrIndex = containedPolygons.nextSetBit(0); wrIndex >= 0; wrIndex = containedPolygons
.nextSetBit(wrIndex + 1)) {
logWayURLs(Level.WARNING, "-", polygons.get(wrIndex));
unfinishedPolygons.set(wrIndex);
wrongInnerPolygons.set(wrIndex);
}
logFakeWayDetails(Level.WARNING, innerWay);
}
unfinishedPolygons.clear(wiIndex);
wrongInnerPolygons.clear(wiIndex);
}
}
}
protected void cleanup() {
mpPolygons = null;
roleMap.clear();
containsMatrix = null;
polygons = null;
bboxArea = null;
intersectingPolygons = null;
outerWaysForLineTagging = null;
outerTags = null;
unfinishedPolygons = null;
innerPolygons = null;
taggedInnerPolygons = null;
outerPolygons = null;
taggedOuterPolygons = null;
largestOuterPolygon = null;
}
private CutPoint calcNextCutPoint(AreaCutData areaData) {
if (areaData.innerAreas == null || areaData.innerAreas.isEmpty()) {
return null;
}
Rectangle2D outerBounds = areaData.outerArea.getBounds2D();
if (areaData.innerAreas.size() == 1) {
// make it short if there is only one inner area
CutPoint cutPoint1 = new CutPoint(CoordinateAxis.LATITUDE, outerBounds);
cutPoint1.addArea(areaData.innerAreas.get(0));
CutPoint cutPoint2 = new CutPoint(CoordinateAxis.LONGITUDE, outerBounds);
cutPoint2.addArea(areaData.innerAreas.get(0));
if (cutPoint1.compareTo(cutPoint2) > 0) {
return cutPoint1;
} else {
return cutPoint2;
}
}
ArrayList<Area> innerStart = new ArrayList<Area>(areaData.innerAreas);
// first try to cut out all polygons that intersect the boundaries of the outer polygon
// this has the advantage that the outer polygon need not be split into two halves
for (CoordinateAxis axis : CoordinateAxis.values()) {
CutPoint edgeCutPoint = new CutPoint(axis, outerBounds);
// go through the inner polygon list and use all polygons that intersect the outer polygons bbox at the start
Collections.sort(innerStart, (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START: COMP_LAT_START));
for (Area anInnerStart : innerStart) {
if (axis.getStart30(anInnerStart) <= axis.getStart30(outerBounds)) {
// found a touching area
edgeCutPoint.addArea(anInnerStart);
} else {
break;
}
}
if (edgeCutPoint.getNumberOfAreas() > 0) {
// there at least one intersecting inner polygon
return edgeCutPoint;
}
Collections.sort(innerStart, (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_STOP: COMP_LAT_STOP));
// go through the inner polygon list and use all polygons that intersect the outer polygons bbox at the stop
for (Area anInnerStart : innerStart) {
if (axis.getStop30(anInnerStart) >= axis.getStop30(outerBounds)) {
// found a touching area
edgeCutPoint.addArea(anInnerStart);
} else {
break;
}
}
if (edgeCutPoint.getNumberOfAreas() > 0) {
// there at least one intersecting inner polygon
return edgeCutPoint;
}
}
ArrayList<CutPoint> bestCutPoints = new ArrayList<CutPoint>(CoordinateAxis.values().length);
for (CoordinateAxis axis : CoordinateAxis.values()) {
CutPoint bestCutPoint = new CutPoint(axis, outerBounds);
CutPoint currentCutPoint = new CutPoint(axis, outerBounds);
Collections.sort(innerStart, (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START: COMP_LAT_START));
for (Area anInnerStart : innerStart) {
currentCutPoint.addArea(anInnerStart);
if (currentCutPoint.compareTo(bestCutPoint) > 0) {
bestCutPoint = currentCutPoint.duplicate();
}
}
bestCutPoints.add(bestCutPoint);
}
return Collections.max(bestCutPoints);
}
/**
* Cut out all inner polygons from the outer polygon. This will divide the outer
* polygon in several polygons.
*
* @param outerPolygon
* the outer polygon
* @param innerPolygons
* a list of inner polygons
* @return a list of polygons that make the outer polygon cut by the inner
* polygons
*/
private List<Way> cutOutInnerPolygons(Way outerPolygon, List<Way> innerPolygons) {
if (innerPolygons.isEmpty()) {
Way outerWay = new JoinedWay(outerPolygon);
if (log.isDebugEnabled()) {
log.debug("Way", outerPolygon.getId(), "splitted to way", outerWay.getId());
}
return Collections.singletonList(outerWay);
}
// use the java.awt.geom.Area class because it's a quick
// implementation of what's needed
// this list contains all non overlapping and singular areas
// of the outerPolygon
Queue<AreaCutData> areasToCut = new LinkedList<AreaCutData>();
Collection<Area> finishedAreas = new ArrayList<Area>(innerPolygons.size());
// create a list of Area objects from the outerPolygon (clipped to the bounding box)
List<Area> outerAreas = createAreas(outerPolygon, true);
// create the inner areas
List<Area> innerAreas = new ArrayList<Area>(innerPolygons.size()+2);
for (Way innerPolygon : innerPolygons) {
// don't need to clip to the bounding box because
// these polygons are just used to cut out holes
innerAreas.addAll(createAreas(innerPolygon, false));
}
// initialize the cut data queue
if (innerAreas.isEmpty()) {
// this is a multipolygon without any inner areas
// nothing to cut
finishedAreas.addAll(outerAreas);
} else if (outerAreas.size() == 1) {
// there is one outer area only
// it is checked before that all inner areas are inside this outer area
AreaCutData initialCutData = new AreaCutData();
initialCutData.outerArea = outerAreas.get(0);
initialCutData.innerAreas = innerAreas;
areasToCut.add(initialCutData);
} else {
// multiple outer areas
for (Area outerArea : outerAreas) {
AreaCutData initialCutData = new AreaCutData();
initialCutData.outerArea = outerArea;
initialCutData.innerAreas = new ArrayList<Area>(innerAreas
.size());
for (Area innerArea : innerAreas) {
if (outerArea.getBounds2D().intersects(
innerArea.getBounds2D())) {
initialCutData.innerAreas.add(innerArea);
}
}
if (initialCutData.innerAreas.isEmpty()) {
// this is either an error
// or the outer area has been cut into pieces on the tile bounds
finishedAreas.add(outerArea);
} else {
areasToCut.add(initialCutData);
}
}
}
while (!areasToCut.isEmpty()) {
AreaCutData areaCutData = areasToCut.poll();
CutPoint cutPoint = calcNextCutPoint(areaCutData);
if (cutPoint == null) {
finishedAreas.add(areaCutData.outerArea);
continue;
}
assert cutPoint.getNumberOfAreas() > 0 : "Number of cut areas == 0 in mp "+getId();
// cut out the holes
if (cutPoint.getAreas().size() == 1)
areaCutData.outerArea.subtract(cutPoint.getAreas().get(0));
else {
// first combine the areas that should be subtracted
Path2D.Double path = new Path2D.Double();
for (Area cutArea : cutPoint.getAreas()) {
path.append(cutArea, false);
}
Area combinedCutAreas = new Area(path);
areaCutData.outerArea.subtract(combinedCutAreas);
}
if (areaCutData.outerArea.isEmpty()) {
// this outer area space can be abandoned
continue;
}
// the inner areas of the cut point have been processed
// they are no longer needed
for (Area cutArea : cutPoint.getAreas()) {
ListIterator<Area> areaIter = areaCutData.innerAreas.listIterator();
while (areaIter.hasNext()) {
Area a = areaIter.next();
if (a == cutArea) {
areaIter.remove();
break;
}
}
}
// remove all does not seem to work. It removes more than the identical areas.
// areaCutData.innerAreas.removeAll(cutPoint.getAreas());
if (areaCutData.outerArea.isSingular()) {
// the area is singular
// => no further splits necessary
if (areaCutData.innerAreas.isEmpty()) {
// this area is finished and needs no further cutting
finishedAreas.add(areaCutData.outerArea);
} else {
// read this area to further processing
areasToCut.add(areaCutData);
}
} else {
// we need to cut the area into two halves to get singular areas
Rectangle2D r1 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, true);
Rectangle2D r2 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, false);
// Now find the intersection of these two boxes with the
// original polygon. This will make two new areas, and each
// area will be one (or more) polygons.
Area a1 = new Area(r1);
Area a2 = new Area(r2);
a1.intersect(areaCutData.outerArea);
a2.intersect(areaCutData.outerArea);
if (areaCutData.innerAreas.isEmpty()) {
finishedAreas.addAll(Java2DConverter.areaToSingularAreas(a1));
finishedAreas.addAll(Java2DConverter.areaToSingularAreas(a2));
} else {
ArrayList<Area> cuttedAreas = new ArrayList<Area>();
cuttedAreas.addAll(Java2DConverter.areaToSingularAreas(a1));
cuttedAreas.addAll(Java2DConverter.areaToSingularAreas(a2));
for (Area nextOuterArea : cuttedAreas) {
ArrayList<Area> nextInnerAreas = null;
// go through all remaining inner areas and check if they
// must be further processed with the nextOuterArea
for (Area nonProcessedInner : areaCutData.innerAreas) {
if (nextOuterArea.intersects(nonProcessedInner.getBounds2D())) {
if (nextInnerAreas == null) {
nextInnerAreas = new ArrayList<Area>();
}
nextInnerAreas.add(nonProcessedInner);
}
}
if (nextInnerAreas == null || nextInnerAreas.isEmpty()) {
finishedAreas.add(nextOuterArea);
} else {
AreaCutData outCutData = new AreaCutData();
outCutData.outerArea = nextOuterArea;
outCutData.innerAreas= nextInnerAreas;
areasToCut.add(outCutData);
}
}
}
}
}
// convert the java.awt.geom.Area back to the mkgmap way
List<Way> cuttedOuterPolygon = new ArrayList<Way>(finishedAreas.size());
Long2ObjectOpenHashMap<Coord> commonCoordMap = new Long2ObjectOpenHashMap<>();
for (Area area : finishedAreas) {
Way w = singularAreaToWay(area, getOriginalId());
if (w != null) {
w.setFakeId();
// make sure that equal coords are changed to identical coord instances
// this allows merging in the ShapeMerger
// TODO: maybe better merge here?
int n = w.getPoints().size();
for (int i = 0; i < n; i++){
Coord p = w.getPoints().get(i);
long key = Utils.coord2Long(p);
Coord replacement = commonCoordMap.get(key);
if (replacement == null)
commonCoordMap.put(key, p);
else {
assert p.highPrecEquals(replacement);
w.getPoints().set(i, replacement);
}
}
w.copyTags(outerPolygon);
cuttedOuterPolygon.add(w);
if (log.isDebugEnabled()) {
log.debug("Way", outerPolygon.getId(), "splitted to way", w.getId());
}
}
}
return cuttedOuterPolygon;
}
/**
* Create the areas that are enclosed by the way. Usually the result should
* only be one area but some ways contain intersecting lines. To handle these
* erroneous cases properly the method might return a list of areas.
*
* @param w a closed way
* @param clipBbox true if the areas should be clipped to the bounding box; false else
* @return a list of enclosed ares
*/
private List<Area> createAreas(Way w, boolean clipBbox) {
Area area = Java2DConverter.createArea(w.getPoints());
if (clipBbox && !bboxArea.contains(area.getBounds2D())) {
// the area intersects the bounding box => clip it
area.intersect(bboxArea);
}
List<Area> areaList = Java2DConverter.areaToSingularAreas(area);
if (log.isDebugEnabled()) {
log.debug("Bbox clipped way",w.getId()+"=>",areaList.size(),"distinct area(s).");
}
return areaList;
}
/**
* Convert an area to an mkgmap way. The caller must ensure that the area is singular.
* Otherwise only the first part of the area is converted.
*
* @param area
* the area
* @param wayId
* the wayid for the new way
* @return a new mkgmap way
*/
private Way singularAreaToWay(Area area, long wayId) {
List<Coord> points = Java2DConverter.singularAreaToPoints(area);
if (points == null || points.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Empty area", wayId + ".", toBrowseURL());
}
return null;
}
return new Way(wayId, points);
}
/**
* Retrieves if the given element contains tags that may be relevant
* for style processing. If it has no relevant tag it will probably be
* dropped by the style.
*
* @param element the OSM element
* @return <code>true</code> has style relevant tags
*/
protected boolean hasStyleRelevantTags(Element element) {
if (element instanceof MultiPolygonRelation) {
// in case it is a multipolygon the TAGS_INCOMPLETE_TAG declares
// that the mp has additional tags removed by the file loader
if (element.tagIsLikeYes(OsmHandler.TAGS_INCOMPLETE_TAG)) {
return true;
}
}
for (Map.Entry<String, String> tagEntry : element.getTagEntryIterator()) {
String tagName = tagEntry.getKey();
// all tags are style relevant
// except: type (for relations), mkgmap:*
boolean isStyleRelevant = (element instanceof Relation && tagName.equals("type")) == false
&& tagName.startsWith("mkgmap:") == false;
if (isStyleRelevant) {
return true;
}
}
return false;
}
/**
* Checks if this mp should be processed or if it is needless to process it
* because there is no result.
* @param ways the list of ways of the mp
* @return <code>true</code> the mp processing will have a result;
* <code>false</code> the mp processing will fail
*/
private boolean isMpProcessable(Collection<Way> ways) {
// Check if the multipolygon itself or the member ways have a
// tag. If not it does not make sense to process the mp because
// the output will not change anything
if (hasStyleRelevantTags(this)) {
return true;
}
for (Way w : ways) {
if (hasStyleRelevantTags(w)) {
return true;
}
}
return false;
}
/**
* Creates a matrix which polygon contains which polygon. A polygon does not
* contain itself.
*
* @param polygonList
* a list of polygons
*/
protected void createContainsMatrix(List<JoinedWay> polygonList) {
containsMatrix = new ArrayList<BitSet>();
for (int i = 0; i < polygonList.size(); i++) {
containsMatrix.add(new BitSet());
}
long t1 = System.currentTimeMillis();
if (log.isDebugEnabled())
log.debug("createContainsMatrix listSize:", polygonList.size());
// use this matrix to check which matrix element has been
// calculated
ArrayList<BitSet> finishedMatrix = new ArrayList<BitSet>(polygonList
.size());
for (int i = 0; i < polygonList.size(); i++) {
BitSet matrixRow = new BitSet();
// a polygon does not contain itself
matrixRow.set(i);
finishedMatrix.add(matrixRow);
}
for (int rowIndex = 0; rowIndex < polygonList.size(); rowIndex++) {
JoinedWay potentialOuterPolygon = polygonList.get(rowIndex);
BitSet containsColumns = containsMatrix.get(rowIndex);
BitSet finishedCol = finishedMatrix.get(rowIndex);
// the polygon need to be created only sometimes
// so use a lazy creation to improve performance
WayAndLazyPolygon lazyPotOuterPolygon = new WayAndLazyPolygon(potentialOuterPolygon);
// get all non calculated columns of the matrix
for (int colIndex = finishedCol.nextClearBit(0); colIndex >= 0
&& colIndex < polygonList.size(); colIndex = finishedCol
.nextClearBit(colIndex + 1)) {
JoinedWay innerPolygon = polygonList.get(colIndex);
if (potentialOuterPolygon.getBounds().intersects(
innerPolygon.getBounds()))
{
boolean contains = contains(lazyPotOuterPolygon, innerPolygon);
if (contains) {
containsColumns.set(colIndex);
// we also know that the inner polygon does not contain the
// outer polygon
// so we can set the finished bit for this matrix
// element
finishedMatrix.get(colIndex).set(rowIndex);
// additionally we know that the outer polygon contains all
// polygons that are contained by the inner polygon
containsColumns.or(containsMatrix.get(colIndex));
finishedCol.or(containsColumns);
}
} else {
// both polygons do not intersect
// we can flag both matrix elements as finished
finishedMatrix.get(colIndex).set(rowIndex);
finishedMatrix.get(rowIndex).set(colIndex);
}
// this matrix element is calculated now
finishedCol.set(colIndex);
}
}
if (log.isDebugEnabled()) {
long t2 = System.currentTimeMillis();
log.debug("createMatrix for", polygonList.size(), "polygons took",
(t2 - t1), "ms");
log.debug("Containsmatrix:");
int i = 0;
boolean noContained = true;
for (BitSet b : containsMatrix) {
if (b.isEmpty()==false) {
log.debug(i,"contains",b);
noContained = false;
}
i++;
}
if (noContained) {
log.debug("Matrix is empty");
}
}
}
/**
* This is a helper class that creates a high precision polygon for a way
* on request only.
*/
private static class WayAndLazyPolygon {
private final JoinedWay way;
private Polygon polygon;
public WayAndLazyPolygon(JoinedWay way) {
this.way = way;
}
public final JoinedWay getWay() {
return this.way;
}
public final Polygon getPolygon() {
if (this.polygon == null) {
this.polygon = Java2DConverter.createHighPrecPolygon(this.way.getPoints());
}
return this.polygon;
}
}
/**
* Checks if the polygon with polygonIndex1 contains the polygon with polygonIndex2.
*
* @return true if polygon(polygonIndex1) contains polygon(polygonIndex2)
*/
private boolean contains(int polygonIndex1, int polygonIndex2) {
return containsMatrix.get(polygonIndex1).get(polygonIndex2);
}
/**
* Checks if polygon1 contains polygon2.
*
* @param polygon1
* a closed way
* @param polygon2
* a 2nd closed way
* @return true if polygon1 contains polygon2
*/
private boolean contains(WayAndLazyPolygon polygon1, JoinedWay polygon2) {
if (!polygon1.getWay().hasIdenticalEndPoints()) {
return false;
}
// check if the bounds of polygon2 are completely inside/enclosed the bounds
// of polygon1
if (!polygon1.getWay().getBounds().contains(polygon2.getBounds())) {
return false;
}
// check first if one point of polygon2 is in polygon1
// ignore intersections outside the bounding box
// so it is necessary to check if there is at least one
// point of polygon2 in polygon1 ignoring all points outside the bounding box
boolean onePointContained = false;
boolean allOnLine = true;
for (Coord px : polygon2.getPoints()) {
if (polygon1.getPolygon().contains(px.getHighPrecLon(), px.getHighPrecLat())){
// there's one point that is in polygon1 and in the bounding
// box => polygon1 may contain polygon2
onePointContained = true;
if (!locatedOnLine(px, polygon1.getWay().getPoints())) {
allOnLine = false;
break;
}
} else if (bbox.contains(px)) {
// we have to check if the point is on one line of the polygon1
if (!locatedOnLine(px, polygon1.getWay().getPoints())) {
// there's one point that is not in polygon1 but inside the
// bounding box => polygon1 does not contain polygon2
//allOnLine = false;
return false;
}
}
}
if (allOnLine) {
onePointContained = false;
// all points of polygon2 lie on lines of polygon1
// => the middle of each line polygon must NOT lie outside polygon1
ArrayList<Coord> middlePoints2 = new ArrayList<Coord>(polygon2.getPoints().size());
Coord p1 = null;
for (Coord p2 : polygon2.getPoints()) {
if (p1 != null) {
Coord pm = p1.makeBetweenPoint(p2, 0.5);
middlePoints2.add(pm);
}
p1 = p2;
}
for (Coord px : middlePoints2) {
if (polygon1.getPolygon().contains(px.getHighPrecLon(), px.getHighPrecLat())){
// there's one point that is in polygon1 and in the bounding
// box => polygon1 may contain polygon2
onePointContained = true;
break;
} else if (bbox.contains(px)) {
// we have to check if the point is on one line of the polygon1
if (!locatedOnLine(px, polygon1.getWay().getPoints())) {
// there's one point that is not in polygon1 but inside the
// bounding box => polygon1 does not contain polygon2
return false;
}
}
}
}
if (!onePointContained) {
// no point of polygon2 is in polygon1 => polygon1 does not contain polygon2
return false;
}
Iterator<Coord> it1 = polygon1.getWay().getPoints().iterator();
Coord p1_1 = it1.next();
while (it1.hasNext()) {
Coord p1_2 = p1_1;
p1_1 = it1.next();
if (!polygon2.linePossiblyIntersectsWay(p1_1, p1_2)) {
// don't check it - this segment of the outer polygon
// definitely does not intersect the way
continue;
}
int lonMin = Math.min(p1_1.getLongitude(), p1_2.getLongitude());
int lonMax = Math.max(p1_1.getLongitude(), p1_2.getLongitude());
int latMin = Math.min(p1_1.getLatitude(), p1_2.getLatitude());
int latMax = Math.max(p1_1.getLatitude(), p1_2.getLatitude());
// check all lines of way1 and way2 for intersections
Iterator<Coord> it2 = polygon2.getPoints().iterator();
Coord p2_1 = it2.next();
// for speedup we divide the area around the second line into
// a 3x3 matrix with lon(-1,0,1) and lat(-1,0,1).
// -1 means below min lon/lat of bbox line p1_1-p1_2
// 0 means inside the bounding box of the line p1_1-p1_2
// 1 means above max lon/lat of bbox line p1_1-p1_2
int lonField = p2_1.getLongitude() < lonMin ? -1 : p2_1
.getLongitude() > lonMax ? 1 : 0;
int latField = p2_1.getLatitude() < latMin ? -1 : p2_1
.getLatitude() > latMax ? 1 : 0;
int prevLonField = lonField;
int prevLatField = latField;
while (it2.hasNext()) {
Coord p2_2 = p2_1;
p2_1 = it2.next();
int changes = 0;
// check if the field of the 3x3 matrix has changed
if ((lonField >= 0 && p1_1.getLongitude() < lonMin)
|| (lonField <= 0 && p1_1.getLongitude() > lonMax)) {
changes++;
lonField = p1_1.getLongitude() < lonMin ? -1 : p1_1
.getLongitude() > lonMax ? 1 : 0;
}
if ((latField >= 0 && p1_1.getLatitude() < latMin)
|| (latField <= 0 && p1_1.getLatitude() > latMax)) {
changes++;
latField = p1_1.getLatitude() < latMin ? -1 : p1_1
.getLatitude() > latMax ? 1 : 0;
}
// an intersection is possible if
// latField and lonField has changed
// or if we come from or go to the inner matrix field
boolean intersectionPossible = (changes == 2)
|| (latField == 0 && lonField == 0)
|| (prevLatField == 0 && prevLonField == 0);
boolean intersects = intersectionPossible
&& linesCutEachOther(p1_1, p1_2, p2_1, p2_2);
if (intersects) {
if ((polygon1.getWay().isClosedArtificially() && !it1.hasNext())
|| (polygon2.isClosedArtificially() && !it2.hasNext())) {
// don't care about this intersection
// one of the polygons is closed by this mp code and the
// closing segment causes the intersection
log.info("Polygon", polygon1, "may contain polygon", polygon2,
". Ignoring artificial generated intersection.");
} else if ((!bbox.contains(p1_1))
|| (!bbox.contains(p1_2))
|| (!bbox.contains(p2_1))
|| (!bbox.contains(p2_2))) {
// at least one point is outside the bounding box
// we ignore the intersection because the ways may not
// be complete
// due to removals of the tile splitter or osmosis
log.info("Polygon", polygon1, "may contain polygon", polygon2,
". Ignoring because at least one point is outside the bounding box.");
} else {
// store them in the intersection polygons set
// the error message will be printed out in the end of
// the mp handling
intersectingPolygons.add(polygon1.getWay());
intersectingPolygons.add(polygon2);
return false;
}
}
prevLonField = lonField;
prevLatField = latField;
}
}
// don't have any intersection
// => polygon1 contains polygon2
return true;
}
/**
* Checks if the point p is located on one line of the given points.
* @param p a point
* @param points a list of points; all consecutive points are handled as lines
* @return true if p is located on one line given by points
*/
private boolean locatedOnLine(Coord p, List<Coord> points) {
Coord cp1 = null;
for (Coord cp2 : points) {
if (p.highPrecEquals(cp2)) {
return true;
}
try {
if (cp1 == null) {
// first init
continue;
}
if (p.getHighPrecLon() < Math.min(cp1.getHighPrecLon(), cp2.getHighPrecLon())) {
continue;
}
if (p.getHighPrecLon() > Math.max(cp1.getHighPrecLon(), cp2.getHighPrecLon())) {
continue;
}
if (p.getHighPrecLat() < Math.min(cp1.getHighPrecLat(), cp2.getHighPrecLat())) {
continue;
}
if (p.getHighPrecLat() > Math.max(cp1.getHighPrecLat(), cp2.getHighPrecLat())) {
continue;
}
double dist = Line2D.ptSegDistSq(cp1.getHighPrecLon(), cp1.getHighPrecLat(),
cp2.getHighPrecLon(), cp2.getHighPrecLat(),
p.getHighPrecLon(), p.getHighPrecLat());
if (dist <= OVERLAP_TOLERANCE_DISTANCE) {
log.debug("Point", p, "is located on line between", cp1, "and",
cp2, ". Distance:", dist);
return true;
}
} finally {
cp1 = cp2;
}
}
return false;
}
private boolean lineCutsBbox(Coord p1_1, Coord p1_2) {
Coord nw = new Coord(bbox.getMaxLat(), bbox.getMinLong());
Coord sw = new Coord(bbox.getMinLat(), bbox.getMinLong());
Coord se = new Coord(bbox.getMinLat(), bbox.getMaxLong());
Coord ne = new Coord(bbox.getMaxLat(), bbox.getMaxLong());
return linesCutEachOther(nw, sw, p1_1, p1_2)
|| linesCutEachOther(sw, se, p1_1, p1_2)
|| linesCutEachOther(se, ne, p1_1, p1_2)
|| linesCutEachOther(ne, nw, p1_1, p1_2);
}
/**
* Check if the line p1_1 to p1_2 cuts line p2_1 to p2_2 in two pieces and vice versa.
* This is a form of intersection check where it is allowed that one line ends on the
* other line or that the two lines overlap.
* @param p1_1 first point of line 1
* @param p1_2 second point of line 1
* @param p2_1 first point of line 2
* @param p2_2 second point of line 2
* @return true if both lines intersect somewhere in the middle of each other
*/
private boolean linesCutEachOther(Coord p1_1, Coord p1_2, Coord p2_1,
Coord p2_2) {
int width1 = p1_2.getHighPrecLon() - p1_1.getHighPrecLon();
int width2 = p2_2.getHighPrecLon() - p2_1.getHighPrecLon();
int height1 = p1_2.getHighPrecLat() - p1_1.getHighPrecLat();
int height2 = p2_2.getHighPrecLat() - p2_1.getHighPrecLat();
int denominator = ((height2 * width1) - (width2 * height1));
if (denominator == 0) {
// the lines are parallel
// they might overlap but this is ok for this test
return false;
}
int x1Mx3 = p1_1.getHighPrecLon() - p2_1.getHighPrecLon();
int y1My3 = p1_1.getHighPrecLat() - p2_1.getHighPrecLat();
double isx = (double)((width2 * y1My3) - (height2 * x1Mx3))
/ denominator;
if (isx < 0 || isx > 1) {
return false;
}
double isy = (double)((width1 * y1My3) - (height1 * x1Mx3))
/ denominator;
if (isy < 0 || isy > 1) {
return false;
}
return true;
}
private List<JoinedWay> getWaysFromPolygonList(BitSet selection) {
if (selection.isEmpty()) {
return Collections.emptyList();
}
List<JoinedWay> wayList = new ArrayList<JoinedWay>(selection
.cardinality());
for (int i = selection.nextSetBit(0); i >= 0; i = selection.nextSetBit(i + 1)) {
wayList.add(polygons.get(i));
}
return wayList;
}
private void logWayURLs(Level level, String preMsg, Way way) {
if (log.isLoggable(level)) {
if (way instanceof JoinedWay) {
if (((JoinedWay) way).getOriginalWays().isEmpty()) {
log.warn("Way", way, "does not contain any original ways");
}
for (Way segment : ((JoinedWay) way).getOriginalWays()) {
if (preMsg == null || preMsg.length() == 0) {
log.log(level, segment.toBrowseURL());
} else {
log.log(level, preMsg, segment.toBrowseURL());
}
}
} else {
if (preMsg == null || preMsg.length() == 0) {
log.log(level, way.toBrowseURL());
} else {
log.log(level, preMsg, way.toBrowseURL());
}
}
}
}
/**
* Logs the details of the original ways of a way with a fake id. This is
* primarily necessary for the sea multipolygon because it consists of
* faked ways only. In this case logging messages can be improved by the
* start and end points of the faked ways.
* @param logLevel the logging level
* @param fakeWay a way composed by other ways with faked ids
*/
private void logFakeWayDetails(Level logLevel, JoinedWay fakeWay) {
if (log.isLoggable(logLevel) == false) {
return;
}
// only log if this is an artificial multipolygon
if (FakeIdGenerator.isFakeId(getId()) == false) {
return;
}
boolean containsOrgFakeWay = false;
for (Way orgWay : fakeWay.getOriginalWays()) {
if (FakeIdGenerator.isFakeId(orgWay.getId())) {
containsOrgFakeWay = true;
}
}
if (containsOrgFakeWay == false) {
return;
}
// the fakeWay consists only of other faked ways
// there should be more information about these ways
// so that it is possible to retrieve the original
// OSM ways
// => log the start and end points
for (Way orgWay : fakeWay.getOriginalWays()) {
log.log(logLevel, " Way",orgWay.getId(),"is composed of other artificial ways. Details:");
log.log(logLevel, " Start:",orgWay.getPoints().get(0).toOSMURL());
if (orgWay.hasEqualEndPoints()) {
// the way is closed so start and end are equal - log the point in the middle of the way
int mid = orgWay.getPoints().size()/2;
log.log(logLevel, " Mid: ",orgWay.getPoints().get(mid).toOSMURL());
} else {
log.log(logLevel, " End: ",orgWay.getPoints().get(orgWay.getPoints().size()-1).toOSMURL());
}
}
}
protected void tagOuterWays() {
Map<String, String> tags;
if (hasStyleRelevantTags(this)) {
tags = new HashMap<String, String>();
for (Entry<String, String> relTag : getTagEntryIterator()) {
tags.put(relTag.getKey(), relTag.getValue());
}
} else {
tags = JoinedWay.getMergedTags(outerWaysForLineTagging);
}
// 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(getOriginalId(), orgOuterWay.getPoints());
lineTagWay.setFakeId();
lineTagWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_LINE);
lineTagWay.addTag(MP_CREATED_TAG, "true");
for (Entry<String,String> tag : tags.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);
}
}
/**
* Marks all tags of the original ways of the given JoinedWay that are also
* contained in the given tagElement for removal.
*
* @param tagElement
* an element contains the tags to be removed
* @param way
* a joined way
*/
private void removeTagsInOrgWays(Element tagElement, JoinedWay way) {
for (Entry<String, String> tag : tagElement.getTagEntryIterator()) {
removeTagInOrgWays(way, tag.getKey(), tag.getValue());
}
}
/**
* Mark the given tag of all original ways of the given JoinedWay.
*
* @param way
* a joined way
* @param tagname
* the tag to be removed (<code>null</code> means remove all
* tags)
* @param tagvalue
* the value of the tag to be removed (<code>null</code> means
* don't check the value)
*/
private void removeTagInOrgWays(JoinedWay way, String tagname,
String tagvalue) {
for (Way w : way.getOriginalWays()) {
if (w instanceof JoinedWay) {
// remove the tags recursively
removeTagInOrgWays((JoinedWay) w, tagname, tagvalue);
continue;
}
boolean remove = false;
if (tagname == null) {
// remove all tags
remove = true;
} else if (tagvalue == null) {
// remove the tag without comparing the value
remove = w.getTag(tagname) != null;
} else if (tagvalue.equals(w.getTag(tagname))) {
remove = true;
}
if (remove) {
if (tagname == null) {
// remove all tags
if (log.isDebugEnabled())
log.debug("Will remove all tags from", w.getId(), w
.toTagString());
removeTagsInOrgWays(w, tagname);
} else {
if (log.isDebugEnabled())
log.debug("Will remove", tagname + "="
+ w.getTag(tagname), "from way", w.getId(), w
.toTagString());
removeTagsInOrgWays(w, tagname);
}
}
}
}
protected void removeTagsInOrgWays(Way way, String tag) {
if (tag == null) {
way.addTag(ElementSaver.MKGMAP_REMOVE_TAG, ElementSaver.MKGMAP_REMOVE_TAG_ALL_KEY);
return;
}
if (tag.isEmpty()) {
return;
}
String removedTagsTag = way.getTag(ElementSaver.MKGMAP_REMOVE_TAG);
if (ElementSaver.MKGMAP_REMOVE_TAG_ALL_KEY.equals(removedTagsTag)) {
// cannot add more tags to remove
return;
}
if (removedTagsTag == null) {
way.addTag(ElementSaver.MKGMAP_REMOVE_TAG, tag);
} else if (removedTagsTag.equals(tag) == false) {
way.addTag(ElementSaver.MKGMAP_REMOVE_TAG, removedTagsTag+";"+tag);
}
}
/**
* Flag if the area size of the mp should be calculated and added as tag.
* @return {@code true} area size should be calculated; {@code false} area size should not be calculated
*/
protected boolean isAreaSizeCalculated() {
return true;
}
protected Map<Long, Way> getTileWayMap() {
return tileWayMap;
}
protected Map<Long, Way> getMpPolygons() {
return mpPolygons;
}
protected uk.me.parabola.imgfmt.app.Area getBbox() {
return bbox;
}
/**
* Calculates a unitless number that gives a value for the size
* of the area. The calculation does not correct to any earth
* coordinate system. It uses the simple rectangular coordinate
* system of garmin coordinates.
*
* @param polygon the points of the area
* @return the size of the area (unitless)
*/
public static double calcAreaSize(List<Coord> polygon) {
if (polygon.size() < 4 || polygon.get(0) != polygon.get(polygon.size()-1)) {
return 0; // line or not closed
}
long area = 0;
Iterator<Coord> polyIter = polygon.iterator();
Coord c2 = polyIter.next();
while (polyIter.hasNext()) {
Coord c1 = c2;
c2 = polyIter.next();
area += (long) (c2.getHighPrecLon() + c1.getHighPrecLon())
* (c1.getHighPrecLat() - c2.getHighPrecLat());
}
// convert from high prec to value in map units
double areaSize = (double) area / (2 * (1<<Coord.DELTA_SHIFT) * (1<<Coord.DELTA_SHIFT));
return Math.abs(areaSize);
}
/**
* This is a helper class that gives access to the original
* segments of a joined way.
*/
public static final class JoinedWay extends Way {
private final List<Way> originalWays;
private boolean closedArtificially;
private int minLat;
private int maxLat;
private int minLon;
private int maxLon;
private Rectangle bounds;
public JoinedWay(Way originalWay) {
super(originalWay.getOriginalId(), originalWay.getPoints());
setFakeId();
originalWays = new ArrayList<Way>();
addWay(originalWay);
// we have to initialize the min/max values
Coord c0 = originalWay.getPoints().get(0);
minLat = maxLat = c0.getLatitude();
minLon = maxLon = c0.getLongitude();
updateBounds(originalWay.getPoints());
}
public void addPoint(int index, Coord point) {
getPoints().add(index, point);
updateBounds(point);
}
public void addPoint(Coord point) {
super.addPoint(point);
updateBounds(point);
}
private void updateBounds(List<Coord> pointList) {
for (Coord c : pointList) {
updateBounds(c.getLatitude(),c.getLongitude());
}
}
private void updateBounds (JoinedWay other){
updateBounds(other.minLat,other.minLon);
updateBounds(other.maxLat,other.maxLon);
}
private void updateBounds(int lat, int lon) {
if (lat < minLat) {
minLat = lat;
bounds = null;
} else if (lat > maxLat) {
maxLat = lat;
bounds = null;
}
if (lon < minLon) {
minLon = lon;
bounds = null;
} else if (lon > maxLon) {
maxLon = lon;
bounds = null;
}
}
private void updateBounds(Coord point) {
updateBounds(point.getLatitude(), point.getLongitude());
}
/**
* Checks if this way intersects the given bounding box at least with
* one point.
*
* @param bbox
* the bounding box
* @return <code>true</code> if this way intersects or touches the
* bounding box; <code>false</code> else
*/
public boolean intersects(uk.me.parabola.imgfmt.app.Area bbox) {
return (maxLat >= bbox.getMinLat() &&
minLat <= bbox.getMaxLat() &&
maxLon >= bbox.getMinLong() &&
minLon <= bbox.getMaxLong());
}
public Rectangle getBounds() {
if (bounds == null) {
// note that we increase the rectangle by 1 because intersects
// checks
// only the interior
bounds = new Rectangle(minLon - 1, minLat - 1, maxLon - minLon
+ 2, maxLat - minLat + 2);
}
return bounds;
}
public boolean linePossiblyIntersectsWay(Coord p1, Coord p2) {
return getBounds().intersectsLine(p1.getLongitude(),
p1.getLatitude(), p2.getLongitude(), p2.getLatitude());
}
public void addWay(Way way) {
if (way instanceof JoinedWay) {
for (Way w : ((JoinedWay) way).getOriginalWays()) {
addWay(w);
}
updateBounds((JoinedWay)way);
} else {
if (log.isDebugEnabled()) {
log.debug("Joined", this.getId(), "with", way.getId());
}
this.originalWays.add(way);
}
}
public void closeWayArtificially() {
addPoint(getPoints().get(0));
closedArtificially = true;
}
public boolean isClosedArtificially() {
return closedArtificially;
}
public static Map<String,String> getMergedTags(Collection<Way> ways) {
Map<String,String> mergedTags = new HashMap<String, String>();
boolean first = true;
for (Way way : ways) {
if (first) {
// the tags of the first way are copied completely
for (Map.Entry<String, String> tag : way.getTagEntryIterator()) {
mergedTags.put(tag.getKey(), tag.getValue());
}
first = false;
} else {
// for all other ways all non matching tags are removed
ArrayList<String> tagsToRemove = null;
for (Map.Entry<String, String> tag : mergedTags.entrySet()) {
String wayTagValue = way.getTag(tag.getKey());
if (!tag.getValue().equals(wayTagValue)) {
// the tags are different
if (wayTagValue!= null) {
if (tagsToRemove == null) {
tagsToRemove=new ArrayList<String>();
}
tagsToRemove.add(tag.getKey());
}
}
}
if (tagsToRemove!=null) {
for (String tag : tagsToRemove) {
mergedTags.remove(tag);
}
}
}
}
return mergedTags;
}
/**
* Tags this way with a merge of the tags of all original ways.
*/
public void mergeTagsFromOrgWays() {
if (log.isDebugEnabled()) {
log.debug("Way",getId(),"merge tags from",getOriginalWays().size(),"ways");
}
removeAllTags();
Map<String,String> mergedTags = getMergedTags(getOriginalWays());
for (Entry<String,String> tag : mergedTags.entrySet()) {
addTag(tag.getKey(),tag.getValue());
}
}
public List<Way> getOriginalWays() {
return originalWays;
}
/**
* Retrieves a measurement of the area covered by this polygon. The
* returned value has no unit. It is just a rough comparable value
* because it uses a rectangular coordinate system without correction.
* @return size of the covered areas (0 if the way is not closed)
*/
public double getSizeOfArea() {
return MultiPolygonRelation.calcAreaSize(getPoints());
}
public String toString() {
StringBuilder sb = new StringBuilder(200);
sb.append(getId());
sb.append("(");
sb.append(getPoints().size());
sb.append("P)(");
boolean first = true;
for (Way w : getOriginalWays()) {
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(w.getId());
sb.append("[");
sb.append(w.getPoints().size());
sb.append("P]");
}
sb.append(")");
return sb.toString();
}
}
public static class PolygonStatus {
public final boolean outer;
public final int index;
public final JoinedWay polygon;
public PolygonStatus(boolean outer, int index, JoinedWay polygon) {
this.outer = outer;
this.index = index;
this.polygon = polygon;
}
public String toString() {
return polygon+"_"+outer;
}
}
private static class AreaCutData {
Area outerArea;
List<Area> innerAreas;
}
private static final int CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD = 1<<(11 + Coord.DELTA_SHIFT);
private static final int CUT_POINT_CLASSIFICATION_BAD_THRESHOLD = 1<< (8 + Coord.DELTA_SHIFT);
private static class CutPoint implements Comparable<CutPoint>{
private int startPoint30 = Integer.MAX_VALUE; // 30 bits precision map units
private int stopPoint30 = Integer.MIN_VALUE; // 30 bits precision map units
private Integer cutPoint30 = null; // 30 bits precision map units
private final LinkedList<Area> areas;
private final Comparator<Area> comparator;
private final CoordinateAxis axis;
private Rectangle2D bounds;
private final Rectangle2D outerBounds;
private Double minAspectRatio;
public CutPoint(CoordinateAxis axis, Rectangle2D outerBounds) {
this.axis = axis;
this.outerBounds = outerBounds;
this.areas = new LinkedList<Area>();
this.comparator = (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_STOP : COMP_LAT_STOP);
}
public CutPoint duplicate() {
CutPoint newCutPoint = new CutPoint(this.axis, this.outerBounds);
newCutPoint.areas.addAll(areas);
newCutPoint.startPoint30 = startPoint30;
newCutPoint.stopPoint30 = stopPoint30;
return newCutPoint;
}
private boolean isGoodCutPoint() {
// It is better if the cutting line is on a multiple of 2048.
// Otherwise MapSource and QLandkarteGT paints gaps between the cuts
return getCutPoint30() % CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD == 0;
}
private boolean isBadCutPoint() {
int d1 = getCutPoint30() - startPoint30;
int d2 = stopPoint30 - getCutPoint30();
return Math.min(d1, d2) < CUT_POINT_CLASSIFICATION_BAD_THRESHOLD;
}
private boolean isStartCut() {
return (startPoint30 <= axis.getStart30(outerBounds));
}
private boolean isStopCut() {
return (stopPoint30 >= axis.getStop30(outerBounds));
}
/**
* Calculates the point where the cut should be applied.
* @return the point of cut
*/
private int getCutPoint30() {
if (cutPoint30 != null) {
// already calculated => just return it
return cutPoint30;
}
if (startPoint30 == stopPoint30) {
// there is no choice => return the one possible point
cutPoint30 = startPoint30;
return cutPoint30;
}
if (isStartCut()) {
// the polygons can be cut out at the start of the sector
// thats good because the big polygon need not to be cut into two halves
cutPoint30 = startPoint30;
return cutPoint30;
}
if (isStopCut()) {
// the polygons can be cut out at the end of the sector
// thats good because the big polygon need not to be cut into two halves
cutPoint30 = startPoint30;
return cutPoint30;
}
// try to cut with a good aspect ratio so try the middle of the polygon to be cut
int midOuter30 = axis.getStart30(outerBounds)+(axis.getStop30(outerBounds) - axis.getStart30(outerBounds)) / 2;
cutPoint30 = midOuter30;
if (midOuter30 < startPoint30) {
// not possible => the start point is greater than the middle so correct to the startPoint
cutPoint30 = startPoint30;
if (((cutPoint30 & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1)) + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD) <= stopPoint30) {
cutPoint30 = ((cutPoint30 & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1)) + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD);
}
} else if (midOuter30 > stopPoint30) {
// not possible => the stop point is smaller than the middle so correct to the stopPoint
cutPoint30 = stopPoint30;
if ((cutPoint30 & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1)) >= startPoint30) {
cutPoint30 = (cutPoint30 & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1));
}
}
// try to find a cut point that is a multiple of 2048 to
// avoid that gaps are painted by MapSource and QLandkarteGT
// between the cutting lines
int cutMod = cutPoint30 % CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD;
if (cutMod == 0) {
return cutPoint30;
}
int cut1 = (cutMod > 0 ? cutPoint30-cutMod : cutPoint30 - CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD- cutMod);
if (cut1 >= startPoint30 && cut1 <= stopPoint30) {
cutPoint30 = cut1;
return cutPoint30;
}
int cut2 = (cutMod > 0 ? cutPoint30 + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD -cutMod : cutPoint30 - cutMod);
if (cut2 >= startPoint30 && cut2 <= stopPoint30) {
cutPoint30 = cut2;
return cutPoint30;
}
return cutPoint30;
}
public Rectangle2D getCutRectangleForArea(Area toCut, boolean firstRect) {
return getCutRectangleForArea(toCut.getBounds2D(), firstRect);
}
public Rectangle2D getCutRectangleForArea(Rectangle2D areaRect, boolean firstRect) {
double cp = (double) getCutPoint30() / (1<<Coord.DELTA_SHIFT);
if (axis == CoordinateAxis.LONGITUDE) {
double newWidth = cp-areaRect.getX();
if (firstRect) {
return new Rectangle2D.Double(areaRect.getX(), areaRect.getY(), newWidth, areaRect.getHeight());
} else {
return new Rectangle2D.Double(areaRect.getX()+newWidth, areaRect.getY(), areaRect.getWidth()-newWidth, areaRect.getHeight());
}
} else {
double newHeight = cp-areaRect.getY();
if (firstRect) {
return new Rectangle2D.Double(areaRect.getX(), areaRect.getY(), areaRect.getWidth(), newHeight);
} else {
return new Rectangle2D.Double(areaRect.getX(), areaRect.getY()+newHeight, areaRect.getWidth(), areaRect.getHeight()-newHeight);
}
}
}
public List<Area> getAreas() {
return areas;
}
public void addArea(Area area) {
// remove all areas that do not overlap with the new area
while (!areas.isEmpty() && axis.getStop30(areas.getFirst()) < axis.getStart30(area)) {
// remove the first area
areas.removeFirst();
}
areas.add(area);
Collections.sort(areas, comparator);
startPoint30 = axis.getStart30(Collections.max(areas,
(axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START
: COMP_LAT_START)));
stopPoint30 = axis.getStop30(areas.getFirst());
// reset the cached value => need to be recalculated the next time they are needed
bounds = null;
cutPoint30 = null;
minAspectRatio = null;
}
public int getNumberOfAreas() {
return this.areas.size();
}
/**
* Retrieves the minimum aspect ratio of the outer bounds after cutting.
*
* @return minimum aspect ratio of outer bound after cutting
*/
public double getMinAspectRatio() {
if (minAspectRatio == null) {
// first get the left/upper cut
Rectangle2D r1 = getCutRectangleForArea(outerBounds, true);
double s1_1 = CoordinateAxis.LATITUDE.getSizeOfSide(r1);
double s1_2 = CoordinateAxis.LONGITUDE.getSizeOfSide(r1);
double ar1 = Math.min(s1_1, s1_2) / Math.max(s1_1, s1_2);
// second get the right/lower cut
Rectangle2D r2 = getCutRectangleForArea(outerBounds, false);
double s2_1 = CoordinateAxis.LATITUDE.getSizeOfSide(r2);
double s2_2 = CoordinateAxis.LONGITUDE.getSizeOfSide(r2);
double ar2 = Math.min(s2_1, s2_2) / Math.max(s2_1, s2_2);
// get the minimum
minAspectRatio = Math.min(ar1, ar2);
}
return minAspectRatio;
}
public int compareTo(CutPoint o) {
if (this == o) {
return 0;
}
// prefer a cut at the boundaries
if (isStartCut() && o.isStartCut() == false) {
return 1;
}
else if (isStartCut() == false && o.isStartCut()) {
return -1;
}
else if (isStopCut() && o.isStopCut() == false) {
return 1;
}
else if (isStopCut() == false && o.isStopCut()) {
return -1;
}
// handle the special case that a cut has no area
if (getNumberOfAreas() == 0) {
if (o.getNumberOfAreas() == 0) {
return 0;
} else {
return -1;
}
} else if (o.getNumberOfAreas() == 0) {
return 1;
}
if (isBadCutPoint() != o.isBadCutPoint()) {
if (isBadCutPoint()) {
return -1;
} else
return 1;
}
double dAR = getMinAspectRatio() - o.getMinAspectRatio();
if (dAR != 0) {
return (dAR > 0 ? 1 : -1);
}
if (isGoodCutPoint() != o.isGoodCutPoint()) {
if (isGoodCutPoint())
return 1;
else
return -1;
}
// prefer the larger area that is split
double ss1 = axis.getSizeOfSide(getBounds2D());
double ss2 = o.axis.getSizeOfSide(o.getBounds2D());
if (ss1-ss2 != 0)
return Double.compare(ss1,ss2);
int ndiff = getNumberOfAreas()-o.getNumberOfAreas();
return ndiff;
}
private Rectangle2D getBounds2D() {
if (bounds == null) {
// lazy init
bounds = new Rectangle2D.Double();
for (Area a : areas)
bounds.add(a.getBounds2D());
}
return bounds;
}
public String toString() {
return axis +" "+getNumberOfAreas()+" "+startPoint30+" "+stopPoint30+" "+getCutPoint30();
}
}
private static enum CoordinateAxis {
LATITUDE(false), LONGITUDE(true);
private CoordinateAxis(boolean useX) {
this.useX = useX;
}
private final boolean useX;
public int getStart30(Area area) {
return getStart30(area.getBounds2D());
}
public int getStart30(Rectangle2D rect) {
double val = (useX ? rect.getX() : rect.getY());
return (int)Math.round(val * (1<<Coord.DELTA_SHIFT));
}
public int getStop30(Area area) {
return getStop30(area.getBounds2D());
}
public int getStop30(Rectangle2D rect) {
double val = (useX ? rect.getMaxX() : rect.getMaxY());
return (int)Math.round(val * (1<<Coord.DELTA_SHIFT));
}
public double getSizeOfSide(Rectangle2D rect) {
if (useX) {
int lat30 = (int)Math.round(rect.getY() * (1<<Coord.DELTA_SHIFT));
Coord c1 = Coord.makeHighPrecCoord(lat30, getStart30(rect));
Coord c2 = Coord.makeHighPrecCoord(lat30, getStop30(rect));
return c1.distance(c2);
} else {
int lon30 = (int)Math.round(rect.getX() * (1<<Coord.DELTA_SHIFT));
Coord c1 = Coord.makeHighPrecCoord(getStart30(rect), lon30);
Coord c2 = Coord.makeHighPrecCoord(getStop30(rect), lon30);
return c1.distance(c2);
}
}
}
private static final AreaComparator COMP_LONG_START = new AreaComparator(
true, CoordinateAxis.LONGITUDE);
private static final AreaComparator COMP_LONG_STOP = new AreaComparator(
false, CoordinateAxis.LONGITUDE);
private static final AreaComparator COMP_LAT_START = new AreaComparator(
true, CoordinateAxis.LATITUDE);
private static final AreaComparator COMP_LAT_STOP = new AreaComparator(
false, CoordinateAxis.LATITUDE);
private static class AreaComparator implements Comparator<Area> {
private final CoordinateAxis axis;
private final boolean startPoint;
public AreaComparator(boolean startPoint, CoordinateAxis axis) {
this.startPoint = startPoint;
this.axis = axis;
}
public int compare(Area o1, Area o2) {
if (o1 == o2) {
return 0;
}
if (startPoint) {
int cmp = axis.getStart30(o1) - axis.getStart30(o2);
if (cmp == 0) {
return axis.getStop30(o1) - axis.getStop30(o2);
} else {
return cmp;
}
} else {
int cmp = axis.getStop30(o1) - axis.getStop30(o2);
if (cmp == 0) {
return axis.getStart30(o1) - axis.getStart30(o2);
} else {
return cmp;
}
}
}
}
}