Rev 2436 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* Copyright (C) 2012.
*
* 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.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.Tags;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.GpxCreator;
import uk.me.parabola.util.Java2DConverter;
/**
* A quadtree implementation to handle areas formed by boundaries.
* Each node of the Quadtree stores a list of NodeElems. A NodeElem
* stores one area and the related location relevant tags.
* @author GerdP
*
*/
public class BoundaryQuadTree {
private static final Logger log = Logger.getLogger(BoundaryQuadTree.class);
private static final boolean DEBUG = false;
// debugging aid
private static final String DEBUG_TREEPATH = "?";
private static final boolean DO_ALL_TESTS = false;
// maps the "normal" tags of the boundaries that are saved in this tree to
// the boundaryId
private final HashMap<String, Tags> boundaryTags = new LinkedHashMap<String,Tags>();
// maps the location relevant info to the boundaryId
private final HashMap<String, BoundaryLocationInfo> preparedLocationInfo;
// property controlled preparer
private final BoundaryLocationPreparer preparer;
private final Node root;
// the bounding box of the quadtree
private final Rectangle bbox;
private final String bbox_key;
// tags that can be returned in the get method
public final static String[] mkgmapTagsArray = {
"mkgmap:admin_level1",
"mkgmap:admin_level2",
"mkgmap:admin_level3",
"mkgmap:admin_level4",
"mkgmap:admin_level5",
"mkgmap:admin_level6",
"mkgmap:admin_level7",
"mkgmap:admin_level8",
"mkgmap:admin_level9",
"mkgmap:admin_level10",
"mkgmap:admin_level11",
"mkgmap:postcode"
};
// 11: the position of "mkgmap:postcode" in the above array
public final static short POSTCODE_ONLY = 1 << 11;
/**
* Create a quadtree with the data in an open stream.
* @param inpStream the open stream with QUADTREE_DATA_FORMAT
* @param fileBbox The bounding box for the quadTree
* @param searchBbox The bounding box for the quadTree, only data within this box is used
* @param props if not null, use it to set location names
*/
public BoundaryQuadTree(DataInputStream inpStream,
uk.me.parabola.imgfmt.app.Area fileBbox,
uk.me.parabola.imgfmt.app.Area searchBbox, EnhancedProperties props)
throws IOException {
preparedLocationInfo = new LinkedHashMap<String, BoundaryLocationInfo> ();
preparer = new BoundaryLocationPreparer(props);
assert fileBbox != null: "parameter fileBbox must not be null";
this.bbox = new Rectangle(fileBbox.getMinLong(), fileBbox.getMinLat(),
fileBbox.getMaxLong() - fileBbox.getMinLong(), fileBbox.getMaxLat()
- fileBbox.getMinLat());
this.bbox_key = BoundaryUtil.getKey(this.bbox.y, this.bbox.x);
root = new Node(this.bbox);
readStreamQuadTreeFormat(inpStream,searchBbox);
}
/**
* Create a quadtree for a given bounding box and a list of boundaries.
* Involves costly calculations to split the areas.
* @param givenBbox The bounding box for the quadTree, only data within this box is used
* @param boundaries A list of boundaries. For better performance, the list should be sorted so that small areas come first.
* @param props if not null, use it to set location names
*/
public BoundaryQuadTree(uk.me.parabola.imgfmt.app.Area givenBbox,
List<Boundary> boundaries, EnhancedProperties props) {
preparer = new BoundaryLocationPreparer(props);
assert givenBbox != null: "parameter givenBbox must not be null";
this.bbox = new Rectangle(givenBbox.getMinLong(), givenBbox.getMinLat(),
givenBbox.getMaxLong() - givenBbox.getMinLong(),
givenBbox.getMaxLat() - givenBbox.getMinLat());
this.bbox_key = BoundaryUtil.getKey(this.bbox.y, this.bbox.x);
root = new Node(this.bbox);
// extract the location relevant tags
preparedLocationInfo = preparer.getPreparedLocationInfo(boundaries);
if (boundaries == null || boundaries.size() == 0)
return;
HashMap<String,Boundary> bMap = new HashMap<String,Boundary>();
for (Boundary b: boundaries){
bMap.put(b.getId(), b);
boundaryTags.put(b.getId(), b.getTags());
}
sortBoundaryTagsMap();
// add the boundaries in a specific order
for (String id: boundaryTags.keySet()){
root.add (bMap.get(id).getArea(), id, null);
}
bMap = null;
root.split("_");
}
/**
* Return location relevant Tags for the point defined by Coord
* @param co the point
* @return a reference to the internal Tags or null if the point was not found.
* The returned Tags must not be modified by the caller.
*/
public Tags get(Coord co){
Tags res = root.get(co/*, "_"*/);
if (res == null && bbox.contains(co.getLongitude(),co.getLatitude())){
// we did not find the point, probably it lies on a boundary and
// the clauses regarding insideness of areas make it "invisible"
// try again a few other nearby points
Coord neighbour1 = new Coord(co.getLatitude()-1, co.getLongitude());
Coord neighbour2 = new Coord(co.getLatitude() , co.getLongitude()-1);
Coord neighbour3 = new Coord(co.getLatitude()+1, co.getLongitude());
Coord neighbour4 = new Coord(co.getLatitude() , co.getLongitude()+1);
res = root.get(neighbour1/*, "_"*/);
if (res == null)
res = root.get(neighbour2/*, "_"*/);
if (res == null)
res = root.get(neighbour3/*, "_"*/);
if (res == null)
res = root.get(neighbour4/*, "_"*/);
}
return res;
}
/**
* Return a map with boundary IDs and the related tags.
* @return the map. It is a LinkedHashMap, the order is created with
* AdminLevelCollator and then reversed.
*/
public Map<String, Tags> getTagsMap() {
return new LinkedHashMap<String, Tags>(boundaryTags);
}
/**
* Create a map with boundary Ids and the related area parts that
* are stored in the tree. When the parts are added, they normally
* give the original boundary (clipped by the bounding box of the quadtree).
* Exception: For overlapping boundaries with equal admin_levels only one
* boundary will contain the complete area information.
* @return A HashMap mapping BoundaryIds to a List with all area parts
*/
public Map<String, List<Area>> getAreas(){
Map<String, List<Area>> areas = new HashMap<String, List<Area>>();
root.getAreas(areas, "_", null);
return areas;
}
/**
* For BoundaryMerger: Add the data of another tree into this tree.
* @param other the other instance of BoundaryQuadTree
*/
public void merge(BoundaryQuadTree other){
if (bbox.equals(other.bbox) == false){
log.error("Cannot merge tree with different bounding box");
return;
}
for (Entry <String, BoundaryLocationInfo> entry : other.preparedLocationInfo.entrySet()){
if (this.preparedLocationInfo.containsKey(entry.getKey()) == false){
this.preparedLocationInfo.put(entry.getKey(),entry.getValue());
}
}
// add the others tags
for (Entry <String, Tags> entry : other.boundaryTags.entrySet()){
if (this.boundaryTags.containsKey(entry.getKey()) == false){
this.boundaryTags.put(entry.getKey(),entry.getValue());
}
}
sortBoundaryTagsMap();
root.mergeNodes(other.root, "_");
}
/**
* Return the area that is covered by a given admin level
* @param admLevel reasonable values are 2 .. 11 (inclusive)
* @return
*/
public Area getCoveredArea (Integer admLevel){
return root.getCoveredArea(admLevel, "_");
}
/**
* Return boundary names relevant for the point defined by Coord
* @param co the point
* @return A string with a boundary Id, optionally followed by pairs of admlevel:boundary Id.
* Sample: r1184826;6:r62579;4:r62372;2:r51477
*/
public String getBoundaryNames(Coord co){
return root.getBoundaryNames(co);
}
/**
* Save the BoundaryQuadTree to an open stream. The format is QUADTREE_DATA_FORMAT.
* @param stream
* @throws IOException
*/
public void save(OutputStream stream)throws IOException{
// save the tag infos of all boundaries first
for (Entry<String,Tags> entry : boundaryTags.entrySet()){
writeBoundaryTags(stream, entry.getValue(), entry.getKey());
}
// now write the area info for those boundaries that have positions in the quadtree
root.save(stream, "_");
}
/**
* Sort the boundary-Tags-Map so that zip-code-only boundaries appear first, followed by
* admin_level-11,10,9,...2
*/
private void sortBoundaryTagsMap(){
// make sure that the merged LinkedHashMap is sorted as mergeBoundaries() needs it
ArrayList<String> ids = new ArrayList<String>(boundaryTags.keySet());
Collections.sort(ids, new AdminLevelCollator());
Collections.reverse(ids);
HashMap<String,Tags> tmp = new LinkedHashMap<String,Tags>(boundaryTags);
boundaryTags.clear();
for (String id: ids){
boundaryTags.put(id,tmp.get(id));
}
}
/**
* Write the TAGS sections of the QUADTREE_DATA_FORMAT. Each
* section starts with the "TAGS" eye catcher followed by the
* boundary id, the number of tags and the tags as
* key/value pairs.
* @param stream an already opened OutputStream
* @param tags the boundary tags
* @param id the boundaryId
* @throws IOException
*/
private void writeBoundaryTags(OutputStream stream, Tags tags, String id) throws IOException{
DataOutputStream dOutStream = new DataOutputStream(stream);
dOutStream.writeUTF("TAGS");
dOutStream.writeUTF(id);
// write the tags
int noOfTags = tags.size();
dOutStream.writeInt(noOfTags);
Iterator<Entry<String, String>> tagIter = tags.entryIterator();
while (tagIter.hasNext()) {
Entry<String, String> tag = tagIter.next();
dOutStream.writeUTF(tag.getKey());
dOutStream.writeUTF(tag.getValue());
noOfTags--;
}
assert noOfTags == 0 : "Remaining tags: " + noOfTags + " size: "
+ tags.size() + " " + tags.toString();
dOutStream.flush();
}
/**
* Read a stream in QUADTREE_DATA_FORMAT
* @param inpStream the already opened DataInputStream
* @param bbox a bounding box. Areas not intersecting the bbox are
* ignored.
* @throws IOException
*/
private void readStreamQuadTreeFormat(DataInputStream inpStream,
uk.me.parabola.imgfmt.app.Area bbox) throws IOException{
boolean isFirstArea = true;
try {
while (true) {
String type = inpStream.readUTF();
if (type.equals("TAGS")){
String id = inpStream.readUTF();
Tags tags = new Tags();
int noOfTags = inpStream.readInt();
for (int i = 0; i < noOfTags; i++) {
String name = inpStream.readUTF();
String value = inpStream.readUTF();
tags.put(name, value.intern());
}
boundaryTags.put(id, tags);
}
else if (type.equals("AREA")){
if (isFirstArea){
isFirstArea = false;
prepareLocationInfo();
}
int minLat = inpStream.readInt();
int minLong = inpStream.readInt();
int maxLat = inpStream.readInt();
int maxLong = inpStream.readInt();
log.debug("Next boundary. Lat min:",minLat,"max:",maxLat,"Long min:",minLong,"max:",maxLong);
uk.me.parabola.imgfmt.app.Area rBbox = new uk.me.parabola.imgfmt.app.Area(
minLat, minLong, maxLat, maxLong);
int bSize = inpStream.readInt();
log.debug("Size:",bSize);
if ( bbox == null || bbox.intersects(rBbox)) {
log.debug("Bbox intersects. Load the boundary");
String treePath = inpStream.readUTF();
String id = inpStream.readUTF();
String refs = inpStream.readUTF();
if (refs.isEmpty())
refs = null;
Area area = BoundaryUtil.readAreaAsPath(inpStream);
if (area != null && area.isEmpty() == false)
root.add(area, refs, id, treePath);
} else {
log.debug("Bbox does not intersect. Skip",bSize);
inpStream.skipBytes(bSize);
}
}
else{
log.error("unknown type field " + type );
}
}
} catch (EOFException exp) {
// it's always thrown at the end of the file
// log.error("Got EOF at the end of the file");
}
}
/**
* Fill the map preparedLocationInfo with data from the boundary tags.
*/
private void prepareLocationInfo() {
for (Entry<String, Tags> entry : boundaryTags.entrySet()) {
BoundaryLocationInfo info = preparer.parseTags(entry.getValue());
preparedLocationInfo.put(entry.getKey(), info);
}
}
/**
* A node for the BoundaryQuadTree. Many methods use a so-called treePath to identify the position
* of the node in the tree. A treePath _021 means root->childs[0]->childs[2]->childs[1].
* This path is also saved in the QUADTREE_DATA_FORMAT.
* @author GerdP
*
*/
private class Node {
private Node [] childs;
private List<NodeElem> nodes;
// bounding box of this part of the tree
private final Rectangle bbox;
private final uk.me.parabola.imgfmt.app.Area bounds;
private short depth;
private boolean isLeaf;
/**
* Create an empty node for the given bbox
* @param bbox
*/
private Node (Rectangle bbox){
this.bounds = new uk.me.parabola.imgfmt.app.Area (bbox.y, bbox.x, bbox.y+bbox.height, bbox.x+bbox.width);
this.bbox = new Rectangle(bbox);
isLeaf = true;
}
/**
* Constructor that is used by the split method. The parameters give the corners of a bounding box.
* @param minLat
* @param minLong
* @param maxLat
* @param maxLong
*/
private Node (int minLat, int minLong, int maxLat, int maxLong){
this.bounds = new uk.me.parabola.imgfmt.app.Area (minLat, minLong, maxLat, maxLong);
this.bbox = new Rectangle(minLong, minLat, maxLong - minLong, maxLat - minLat);
this.isLeaf = true;
}
/**
* Travel through the tree, save all usable areas of all leaves
* @param stream the open OutputStream
* @param treePath the path to this tree node
* @throws IOException
*/
private void save(OutputStream stream, String treePath )throws IOException{
if (isLeaf){
if (nodes != null){
for (NodeElem nodeElem :nodes){
if (nodeElem.isValid())
nodeElem.save(stream, treePath);
}
}
}
else {
for (int i = 0; i < 4; i++){
childs[i].save(stream, treePath + i);
}
}
}
/**
* Return boundary names relevant for the point defined by Coord
* @param co the point
* @return A string with a boundary Id, optionally followed by pairs of admlevel:boundary Id.
* Sample: r1184826;6:r62579;4:r62372;2:r51477
*/
private String getBoundaryNames(Coord co) {
if (this.bounds.contains(co) == false)
return null;
if (isLeaf){
if (nodes == null || nodes.size() == 0)
return null;
int lon = co.getLongitude();
int lat = co.getLatitude();
for (NodeElem nodeElem: nodes){
if (nodeElem.tagMask > 0){
if (nodeElem.area.contains(lon,lat)){
String res = new String (nodeElem.boundaryId);
if (nodeElem.locationDataSrc != null)
res += ";" + nodeElem.locationDataSrc;
return res;
}
}
}
}
else {
for (int i = 0; i < 4; i++){
String res = childs[i].getBoundaryNames(co);
if (res != null)
return res;
}
}
return null;
}
/**
* Return location relevant Tags for the point defined by Coord
* @param co the point
* @return a reference to the internal Tags or null if the point was not found.
* The returned Tags must not be modified by the caller.
*/
private Tags get(Coord co/*, String treePath*/){
if (this.bounds.contains(co) == false)
return null;
if (isLeaf){
if (nodes == null || nodes.size() == 0)
return null;
int lon = co.getLongitude();
int lat = co.getLatitude();
for (NodeElem nodeElem: nodes){
if (nodeElem.tagMask > 0){
if (nodeElem.area.contains(lon,lat)){
return nodeElem.locTags;
}
}
}
}
else {
for (int i = 0; i < 4; i++){
Tags res = childs[i].get(co/*, treePath+i*/);
if (res != null)
return res;
}
}
return null;
}
/**
* Debugging helper: Print node Tags and maybe create gpx
* @param prefix identifies the calling routine
* @param treePath
*/
private void printNodes(String prefix, String treePath){
int n = 0;
for (NodeElem nodeElem: nodes){
if (treePath.equals(DEBUG_TREEPATH)){
nodeElem.saveGPX(prefix,treePath);
}
String res = new String();
for (int i = mkgmapTagsArray.length-1; i >= 0 ; --i){
String tagVal = nodeElem.locTags.get(mkgmapTagsArray[i] );
if (tagVal != null){
res += i+1 + "=" + tagVal + ";";
}
}
System.out.println(prefix + " " + treePath + " " + n + ":" + nodeElem.boundaryId + " " + nodeElem.tagMask + " " + res );
++n;
}
}
/**
* Test if all areas in one node are distinct areas. This is wanted, but not
* absolutely needed.
* @param treePath Position in the quadtree. Used for GPX.
* @return false if any area intersects with another area and the
* intersection has a dimension.
*/
private boolean testIfDistinct(String treePath){
boolean ok = true;
for (int i=0; i< nodes.size()-1; i++){
for (int j=i+1; j < nodes.size(); j++){
Area a = new Area (nodes.get(i).area);
a.intersect(nodes.get(j).area);
if (a.isEmpty())
continue;
Path2D.Double path = new Path2D.Double(a);
a = new Area(path);
if (a.isEmpty())
continue;
if (a.getBounds2D().getHeight() < 0.1d && a.getBounds2D().getWidth() < 0.1d)
continue;
ok = false;
log.error("boundaries still intersect in tree path "
+ treePath + " " + nodes.get(i).boundaryId + " "
+ nodes.get(j).boundaryId + " bbox: " + a.getBounds2D());
NodeElem tmpNodeElem = new NodeElem(nodes.get(i).boundaryId+"_"+nodes.get(j).boundaryId,
new Area(a.getBounds2D()), null);
tmpNodeElem.saveGPX("intersection_rect",treePath);
}
}
if (DEBUG){
if (!ok){
for (NodeElem nodeElem: nodes){
nodeElem.saveGPX("not_distinct",treePath);
}
}
}
return ok;
}
/**
* Add an area and the related tags to the tree. The position in the tree is known
* and passed via the treePath.
* @param area the part of the boundary area that should be added to the tree.
* @param refs a string that contains information about other boundaries that share the
* same area
* @param boundaryId id of the originating boundary
* @param treePath empty string: calculate position, else the first character is used as index of the child
*/
private void add(Area area, String refs, String boundaryId, String treePath){
Node node = this;
String path = treePath;
while(path.isEmpty() == false){
int idx = Integer.valueOf(path.substring(0, 1));
path = path.substring(1);
if (node.childs == null)
node.allocChilds();
node = node.childs[idx];
}
if (node.nodes == null){
node.nodes = new ArrayList<NodeElem>();
}
NodeElem nodeElem = new NodeElem(boundaryId, area, refs);
assert (area.getBounds2D().getWidth() == 0 || area.getBounds2D().getHeight() == 0 || this.bbox.intersects(area.getBounds2D())) : "boundary bbox doesn't fit into quadtree "+ bbox + " " + area.getBounds2D();
node.nodes.add(nodeElem);
}
/**
* Add an area and the related tags to the tree.
* @param area the part of the boundary area that should be added to the tree.
* @param locTags the location relevant tags from the boundary
* @param boundaryId id of the originating boundary
*/
private void add(Area area, String boundaryId, String refs){
if (!isLeaf){
// should not happen
for (int i = 0; i < 4; i++){
childs[i].add(area, boundaryId, refs);
}
return;
}
// only add areas that intersect with this part of the tree
if (area.intersects(this.bbox) == false)
return;
Area a;
if (area.contains(bbox))
a = new Area(this.bbox); // quadtree bbox lies entirely in area
else {
a = new Area(area);
Area bboxArea = new Area(this.bbox);
// check if area lies entirely in quadtree bbox
if (bboxArea.contains(area.getBounds2D()) == false){
// worst case: area and bbox partly intersect
a.intersect(bboxArea);
}
}
if (a.isEmpty() == false){
if (nodes == null)
nodes = new ArrayList<NodeElem>();
NodeElem nodeElem = new NodeElem(boundaryId, a, refs);
nodes.add(nodeElem);
}
}
/**
* Merge this subtree with another subtree.
* @param other A Node of another BoundaryQuadTree
* @param treePath position of this node in its tree
*/
private void mergeNodes(Node other, String treePath){
if (!this.isLeaf && !other.isLeaf){
for (int i = 0; i < 4; i++){
childs[i].mergeNodes(other.childs[i], treePath+i);
}
}
else{
// (sub) tree is different, rebuild it as combination of
// both trees.
HashMap<String,List<Area>> areas = new HashMap<String, List<Area>>();
this.getAreas(areas, treePath,null);
other.getAreas(areas, treePath,null);
isLeaf = true;
nodes = null;
childs = null;
for (String id: boundaryTags.keySet()){
List<Area> aList = areas.get(id);
if (aList == null)
continue;
Path2D.Double path = new Path2D.Double();
for (Area area : aList){
BoundaryUtil.addToPath(path, area);
}
add(new Area(path), id, null);
}
split(treePath);
}
}
/**
* Calculate the area that is covered by boundaries of a given adminlevel
* @param admLevel the admin_level, a value from 2 to 11 (including)
* @param treePath A string that helps to identify the position in the quadtree
* @return a new Area instance (might be empty)
*/
private Area getCoveredArea(Integer admLevel, String treePath){
HashMap<String,List<Area>> areas = new HashMap<String, List<Area>>();
this.getAreas(areas, treePath, admLevel);
Path2D.Double path = new Path2D.Double();
for (Entry <String, List<Area>> entry : areas.entrySet()){
for (Area area: entry.getValue()){
BoundaryUtil.addToPath(path,area);
}
}
Area combinedArea = new Area(path);
return combinedArea;
}
/**
* See BoundaryQuadTree {@link #BoundaryQuadTree.getAreas()}
* @param areas
* @param treePath
* @param admLevel
*/
private void getAreas(Map<String, List<Area>> areas, String treePath, Integer admLevel){
if (!this.isLeaf ){
for (int i = 0; i < 4; i++){
childs[i].getAreas(areas, treePath+i, admLevel);
}
return;
}
if (nodes == null || nodes.size() == 0)
return;
Short testMask = null;
if (admLevel != null)
testMask = (short) (1<<(admLevel-1));
for (NodeElem nodeElem : nodes){
String id = nodeElem.boundaryId;
if (testMask != null && (nodeElem.tagMask & testMask) == 0)
continue;
List<Area> aList = areas.get(id);
Area a = new Area(nodeElem.area);
if (aList == null){
aList = new ArrayList<Area>(4);
areas.put(id, aList);
}
aList.add(a);
if (testMask != null)
continue;
String refInfo = nodeElem.locationDataSrc;
if (refInfo != null) {
String[] relBounds = refInfo.split(Pattern.quote(";"));
for (String relBound : relBounds) {
String[] relParts = relBound.split(Pattern.quote(":"));
if (relParts.length != 2) {
log.error("Wrong format in locationDataSrc. Value: " + refInfo);
continue;
}
id = relParts[1];
aList = areas.get(id);
a = new Area(nodeElem.area);
if (aList == null){
aList = new ArrayList<Area>(4);
areas.put(id, aList);
}
aList.add(a);
}
}
}
}
/***
* Merge information from the boundaries saved by BoundarySaver.
* This method is used when bnd file is in raw format.
* For intersections, create new areas with the merged
* location tags, and subtract the parts from the source
* areas. The result should be a reduced number of distinct areas.
* @param treePath Identifies the position in the tree
*/
private void makeDistinct(String treePath){
if (isLeaf == false || nodes == null || nodes.size() <= 1)
return;
if (DEBUG){
printNodes("start", treePath);
}
long t1 = System.currentTimeMillis();
mergeEqualIds();
mergeLastRectangles();
if (DEBUG)
printNodes("prep", treePath);
List<NodeElem> reworked = new ArrayList<NodeElem>();
// detect intersection of areas, merge tag info
for (int i=0; i < nodes.size(); i++){
NodeElem toAdd = nodes.get(i);
if (DEBUG){
if (treePath.equals(DEBUG_TREEPATH) || DEBUG_TREEPATH.equals("all")){
for (NodeElem nodeElem: reworked){
nodeElem.saveGPX("debug"+i,treePath);
}
}
}
for (int j=0; j < reworked.size(); j++){
if (toAdd.isValid() == false)
break;
NodeElem currElem = reworked.get(j);
if (currElem.srcPos == i || currElem.area.isEmpty())
continue;
Rectangle2D rCurr = currElem.area.getBounds2D();
Rectangle2D rAdd = rCurr.createIntersection(toAdd.area.getBounds2D());
if (rAdd.isEmpty()){
continue;
}
// the bounding boxes intersect, so we have to find out if the areas also intersect
Area toAddxCurr = new Area(currElem.area);
toAddxCurr.intersect(toAdd.area);
if (!isWritable(toAddxCurr)){
continue; // empty or only too small fragments
}
Area toAddMinusCurr = new Area(toAdd.area);
toAddMinusCurr.subtract(currElem.area);
if (toAddMinusCurr.isEmpty()){
// toadd is fully covered by curr
if (toAdd.tagMask == POSTCODE_ONLY){
// if we get here, toAdd has only zip code that is already known
// in larger or equal area of currElem
toAdd.area.reset(); // ignore this
break;
}
}
// test if toAdd contains usable tag(s)
String chkMsg = currElem.checkAddTags(toAdd, bounds);
// warning: intersection of areas with equal levels
if (chkMsg != null){
if (DEBUG){
// save debug GPX for areas that wiil
// appear in warning message below
toAdd.saveGPX("warn_toAdd",treePath);
currElem.saveGPX("warn_curr",treePath);
}
log.warn(chkMsg);
}
Area currMinusToAdd = new Area(currElem.area);
currMinusToAdd.subtract(toAdd.area);
// remove intersection part from toAdd
toAdd.area = toAddMinusCurr;
if (!isWritable(currMinusToAdd)){
// curr is fully covered by toAdd
if (toAdd.tagMask != POSTCODE_ONLY){
currElem.addLocInfo(toAdd);
}
continue; // no need to create new intersection area
}
NodeElem intersect = new NodeElem(currElem, toAddxCurr, i);
if (DEBUG){
if (chkMsg != null)
intersect.saveGPX("warn_inter", treePath);
}
// remove intersection part also from curr
currElem.area = currMinusToAdd;
if (toAdd.tagMask != POSTCODE_ONLY){
// combine tag info in intersection
intersect.addLocInfo(toAdd);
reworked.add(intersect);
}
}
if (toAdd.isValid())
reworked.add(toAdd);
}
nodes = reworked;
// free memory for nodes with empty or too small areas
removeEmptyAreas(treePath);
long dt = System.currentTimeMillis()-t1;
if (dt > 1000)
log.info(bbox_key, ": merge required long time:", dt, "ms");
if (DEBUG)
printNodes("end", treePath);
//double check ?
if (DO_ALL_TESTS){
testIfDistinct(treePath);
}
}
/**
* Combine the areas with equal boundary IDs.
* We can assume that equal IDs are paired when add is
* called with sorted input.
*/
private void mergeEqualIds(){
int start = nodes.size()-1;
for (int i = start; i > 0; i--){
if (nodes.get(i).boundaryId.equals(nodes.get(i-1).boundaryId)){
nodes.get(i-1).area.add(nodes.get(i).area);
nodes.remove(i);
}
}
}
/**
* Optimization:
* The last nodes are likely to fully cover the quadtree bbox.
* Merge the tag information for them to avoid some splitting
* and later merging.
*/
private void mergeLastRectangles(){
boolean done;
//step1: merge nodes that fully cover the quadtree area
do{
done = true;
if (nodes.size()<= 1)
break;
NodeElem lastNode = nodes.get(nodes.size()-1);
NodeElem prevNode = nodes.get(nodes.size()-2);
// don't merge admin_level tags into zip-code only boundary
if (prevNode.tagMask != POSTCODE_ONLY && lastNode.area.isRectangular() && prevNode.area.isRectangular()){
// two areas are rectangles, it is likely that they are equal to the bounding box
// In this case we add the tags to the existing area instead of creating a new one
if (prevNode.area.equals(lastNode.area)){
prevNode.addLocInfo(lastNode);
nodes.remove(nodes.size()-1);
done = false;
}
}
} while (!done);
}
/**
* The mergeBoundaries() algorithm can create empty
* areas (points, lines, or extremely small intersections).
* These are removed here.
* @param treePath
*/
private void removeEmptyAreas(String treePath){
for (int j = nodes.size()-1; j >= 0 ; j--){
boolean removeThis = false;
NodeElem chkRemove = nodes.get(j);
if (chkRemove.isValid() == false)
removeThis = true;
else if (this.bbox.intersects(chkRemove.area.getBounds2D()) == false){
// we might get here because of errors in java.awt.geom.Area
// sometimes, Area.subtract() seems to produce an area which
// lies outside of original areas
removeThis = true;
}else if (!isWritable(chkRemove.area)){
removeThis = true;
}
if (removeThis){
nodes.remove(j);
}
}
}
/**
* allocate 4 childs with bounding boxes that have 1/4 of the
* size of the parent.
*/
private void allocChilds(){
childs = new Node[4];
Coord center = bounds.getCenter();
childs[0] = new Node(bounds.getMinLat(), bounds.getMinLong(),
center.getLatitude(), center.getLongitude());
childs[1] = new Node(center.getLatitude(), bounds.getMinLong(),
bounds.getMaxLat(), center.getLongitude());
childs[2] = new Node(bounds.getMinLat(), center.getLongitude(),
center.getLatitude(), bounds.getMaxLong());
childs[3] = new Node(center.getLatitude(), center.getLongitude(),
bounds.getMaxLat(), bounds.getMaxLong());
for (int i = 0; i < 4; i++){
childs[i].depth = (short) (this.depth + 1);
}
isLeaf = false;
}
/**
* Split the tree into 4 equally sized parts and
* distribute the data.
*/
private void split(String treePath){
if (isLeaf == true){
if (nodes == null)
return;
if (DEBUG){
String fname = "gpx/" + treePath+ "/bbox"+treePath;
List<List<Coord>> polys = Java2DConverter.areaToShapes(Java2DConverter.createBoundsArea(bounds));
GpxCreator.createGpx(fname, polys.get(0));
}
// subject to tuning
if (depth >= 5 || nodes.size() <= 7 || bounds.getHeight() < 10 || bounds.getWidth() < 10 ){
makeDistinct(treePath);
return ;
}
mergeLastRectangles();
allocChilds();
for (int i = 0; i < 4; i++){
for (NodeElem nodeElem: nodes){
childs[i].add(nodeElem.area, nodeElem.boundaryId, nodeElem.locationDataSrc);
}
}
// return memory to GC
nodes = null;
}
// finally try splitting the sub trees
for (int i = 0; i < 4; i++){
childs[i].split(treePath+i);
}
}
}
private class NodeElem{
// the intersections of the boundaries with the bounding box of this node
private Area area;
// location relevant tags of boundaries that intersect with the bounding box of this node
private Tags locTags;
// a bit mask that helps comparing Tags
private short tagMask;
// boundary that was initially used
private final String boundaryId;
// data for the intersects_with tag
private String locationDataSrc;
private int srcPos;
/**
* Create a node elem.
* @param boundaryId The boundary Id
* @param area the (part of the) boundary area stored in this node
* @param refs A string containing boundaryIds and admin-level infos
* of all boundaries with lower admin levels that share the same area.
*/
NodeElem (String boundaryId, Area area, String refs){
srcPos = -1;
this.boundaryId = boundaryId;
this.area = area;
this.locationDataSrc = refs;
calcLocTags();
}
/**
* Create a new node elem as a partly copy of an existing
* NodeElem and a new area.
* @param other the existing NodeElem instance
* @param area the new area
* @param srcPos identifies the position of other in the
* nodes list of the Node.
*/
NodeElem (NodeElem other, Area area, int srcPos){
this.area = area;
this.srcPos = srcPos;
this.boundaryId = other.boundaryId;
this.tagMask = other.tagMask;
this.locationDataSrc = other.locationDataSrc;
this.locTags = other.locTags.copy();
}
/**
* check if a NodeElem contains usable info.
* @return false if either the area is not usable or
* the tags should be ignored.
*/
private boolean isValid(){
if (tagMask == 0 || area == null || area.isEmpty())
return false;
return true;
}
/**
* Add the location relevant data of another NodeElem
* @param toAdd the other NodeElem
*/
private void addLocInfo(NodeElem toAdd){
addLocationDataString(toAdd);
addMissingTags(toAdd.locTags);
tagMask |= toAdd.tagMask;
}
/**
* Calculate the tags that are location relevant.
* Problem: If the tree is created by BoundaryPreparer, we do not know how to calculate
* the name because we don't know which tag to use for this, so be aware that this
* may return different results compared to the LocationHook.
* @param boundary
*/
private void calcLocTags(){
locTags = new Tags();
tagMask = 0;
BoundaryLocationInfo bInfo = preparedLocationInfo.get(boundaryId);
if (bInfo == null){
log.error("unknown boundaryId " + boundaryId);
return;
}
if (bInfo.getZip() != null){
locTags.put("mkgmap:postcode",bInfo.getZip());
}
if (bInfo.getAdmLevel() != BoundaryLocationPreparer.UNSET_ADMIN_LEVEL){
locTags.put(BoundaryQuadTree.mkgmapTagsArray[bInfo.getAdmLevel()-1], bInfo.getName());
}
if (locationDataSrc != null && locationDataSrc.isEmpty() == false){
// the common format of refInfo is
// 2:r19884;4:r20039;6:r998818
String[] relBounds = locationDataSrc.split(Pattern.quote(";"));
for (String relBound : relBounds) {
String[] relParts = relBound.split(Pattern.quote(":"));
if (relParts.length != 2) {
log.error("Wrong format. Value: " + locationDataSrc);
continue;
}
BoundaryLocationInfo addInfo = preparedLocationInfo.get(relParts[1]);
if (addInfo == null) {
log.warn("Referenced boundary not known:", relParts[1]);
continue;
}
int addAdmLevel = addInfo.getAdmLevel();
String addAdmName = null;
if (addAdmLevel != BoundaryLocationPreparer.UNSET_ADMIN_LEVEL){
addAdmName = addInfo.getName();
}
String addZip = addInfo.getZip();
if (addAdmName != null){
if (locTags.get(BoundaryQuadTree.mkgmapTagsArray[addAdmLevel-1]) == null)
locTags.put(BoundaryQuadTree.mkgmapTagsArray[addAdmLevel-1], addAdmName);
}
if (addZip != null){
if (locTags.get("mkgmap:postcode") == null)
locTags.put("mkgmap:postcode", addZip);
}
}
}
tagMask = calcLocationTagsMask();
}
/**
* Merge the locationDataSrc of two NodeElems.
* The caller has to make sure that the merge makes sense.
* @param toAdd The other NodeElem
*/
private void addLocationDataString (NodeElem toAdd){
BoundaryLocationInfo info = preparedLocationInfo.get(toAdd.boundaryId);
assert info.getAdmLevel() > 0 : "cannot use admLevel";
String admLevel = info.getAdmLevel() + ":" + toAdd.boundaryId;
if (this.locationDataSrc == null)
this.locationDataSrc = admLevel;
else
this.locationDataSrc += ";" + admLevel;
if (toAdd.locationDataSrc != null){
this.locationDataSrc += ";" + toAdd.locationDataSrc;
}
}
/**
* Write a nodeElem an AREA segment of the QUADTREE_DATA_FORMAT.
* @param stream the already opened OutputStream
* @param treePath identifies the position within the tree
* @throws IOException
*/
private void save(OutputStream stream, String treePath) throws IOException{
ByteArrayOutputStream oneItemStream = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(oneItemStream);
String id = this.boundaryId;
dos.writeUTF(treePath.substring(1));
dos.writeUTF(id);
if (this.locationDataSrc == null)
dos.writeUTF("");
else
dos.writeUTF(this.locationDataSrc);
BoundarySaver.writeArea(dos, this.area);
dos.close();
// now start to write into the real stream
// first write the bounding box so that is possible to skip the
// complete entry
uk.me.parabola.imgfmt.app.Area bbox = Java2DConverter.createBbox(this.area);
DataOutputStream dOutStream = new DataOutputStream(stream);
dOutStream.writeUTF("AREA");
dOutStream.writeInt(bbox.getMinLat());
dOutStream.writeInt(bbox.getMinLong());
dOutStream.writeInt(bbox.getMaxLat());
dOutStream.writeInt(bbox.getMaxLong());
// write the size of the boundary block so that it is possible to
// skip it
byte[] data = oneItemStream.toByteArray();
assert data.length > 0 : "bSize is not > 0 : " + data.length;
dOutStream.writeInt(data.length);
// write the boundary block
dOutStream.write(data);
dOutStream.flush();
}
/**
* calculate a handy short value that represents the available location tags
* @return a bit mask, a bit with value 1 means the corresponding entry in {@link locationTagNames }
* is available
*/
private short calcLocationTagsMask(){
short res = 0;
for (int i = 0; i < mkgmapTagsArray.length; i++){
if (locTags.get(mkgmapTagsArray[i] ) != null)
res |= (1 << i);
}
return res;
}
/**
* For debugging: Save the area in gpx format
* @param desc used as directory name
* @param treePath
*/
private void saveGPX(String desc, String treePath){
if (DEBUG){
if (area == null)
return;
List<List<Coord>> singlePolys = Java2DConverter.areaToShapes(area);
Collections.reverse(singlePolys);
int cntPoly = 0;
for (List<Coord> polyPart : singlePolys) {
String attr = Way.clockwise(polyPart) ? "o" : "i";
String fname = "gpx/" + treePath+ "/" + desc + "_" + area.getBounds().x + "_" + area.getBounds().y + "_" + boundaryId+ "_" + cntPoly + "_"+attr;
GpxCreator.createGpx(fname, polyPart);
++cntPoly;
}
}
}
/**
* Handle errors in OSM data. Two boundaries with equal levels should not intersect.
* Special case: zip-code-only boundaries with same zip code
* @param other the other NodeElem
* @param bounds a bounding box for the intersection of the two areas. Used
* to create the error message.
* @return null if no error, else a String with an error message
*/
private String checkAddTags(NodeElem other, uk.me.parabola.imgfmt.app.Area bounds){
String errMsg = null;
int errAdmLevel = 0;
// case c) toAdd area is fully covered by currElem area
for (int k = 0; k < mkgmapTagsArray.length; k++){
int testMask = 1 << k;
if ((testMask & other.tagMask) != 0 && (this.tagMask & testMask) != 0){
if (testMask == POSTCODE_ONLY){
String zipKey = mkgmapTagsArray[k];
if (other.locTags.get(zipKey).equals(this.locTags.get(zipKey)) == false){
errMsg = "different " + zipKey;
break;
}
}
else{
errAdmLevel = k+1;
errMsg = new String ("same admin_level (" + errAdmLevel + ")");
break;
}
}
}
if (errMsg != null){
String url = bounds.getCenter().toOSMURL() + "&";
url += (other.boundaryId.startsWith("w")) ? "way" : "relation";
url += "=" + other.boundaryId.substring(1);
//http://www.openstreetmap.org/?lat=49.394988&lon=6.551425&zoom=18&layers=M&relation=122907
errMsg= "incorrect data: " + url + " intersection of boundaries with " + errMsg + " " + other.boundaryId + " " + this.boundaryId + " " ;
if (errAdmLevel != 0 && this.locationDataSrc != null)
errMsg += this.locationDataSrc;
}
return errMsg;
}
/**
* Add tags from src to locTags if they are missing
* @param src the Tags to be added
*/
private void addMissingTags(Tags src){
Iterator<Entry<String,String>> tagIter = src.entryIterator();
while (tagIter.hasNext()) {
Entry<String,String> tag = tagIter.next();
if (locTags.get(tag.getKey()) == null){
locTags.put(tag.getKey(),tag.getValue());
}
}
}
}
/***
* Used to sort BoundaryLocationInfo. Input are boundaryIds.
* @author gerd
*
*/
public class AdminLevelCollator implements Comparator<String> {
public int compare(String o1, String o2) {
if (o1.equals(o2)) {
return 0;
}
BoundaryLocationInfo i1 = preparedLocationInfo.get(o1);
BoundaryLocationInfo i2 = preparedLocationInfo.get(o2);
int adminLevel1 = i1.getAdmLevel();
int adminLevel2 = i2.getAdmLevel();
if (i1.getName() == null || i1.getName() == "?") {
// admin_level tag is set but no valid name available
adminLevel1= BoundaryLocationPreparer.UNSET_ADMIN_LEVEL;
}
if (i2.getName() == null || i2.getName() == "?") {
// admin_level tag is set but no valid name available
adminLevel2= BoundaryLocationPreparer.UNSET_ADMIN_LEVEL;
}
if (adminLevel1 > adminLevel2)
return 1;
if (adminLevel1 < adminLevel2)
return -1;
if (i1.getAdmLevel() == 2){
// prefer countries that are known by the Locator
if (i1.isISOName() == true && i2.isISOName() == false)
return 1;
if (i1.isISOName() == false && i2.isISOName() == true)
return -1;
}
boolean post1set = i1.getZip() != null;
boolean post2set = i2.getZip() != null;
if (post1set && !post2set)
return 1;
if (!post1set && post2set)
return -1;
// if all is equal, prefer the lower boundaryId
return o1.compareTo(o2);
}
}
/**
* test if the conversion to a Path2D and back gives an empty area. If the
* area is not already empty this routine simulates the writing and reading
* and tests if the result is empty. Returns true is the area is not empty.
*/
public static boolean isWritable(Area area){
if (area.isEmpty())
return false;
Path2D.Double path = new Path2D.Double(area);
Area testArea = new Area(path);
if (testArea.isEmpty()){
return false;
}
return true;
}
}