/*
* 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.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
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;
import uk.me.parabola.util.ShapeSplitter;
/**
* 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;
private static final boolean DO_CLIP =
true;
private static final boolean DO_NOT_CLIP =
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<>();
// 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<> ();
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<>();
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, DO_NOT_CLIP
);
}
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<>(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<>();
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 a new Area instance (might be empty)
*/
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<>(boundaryTags.
keySet());
Collections.
sort(ids,
new AdminLevelCollator
());
Collections.
reverse(ids
);
HashMap<String,Tags
> tmp =
new LinkedHashMap<>(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 static 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 searchBBox a bounding box. Areas not intersecting the bbox are
* ignored.
* @throws IOException
*/
private void readStreamQuadTreeFormat
(DataInputStream inpStream,
uk.
me.
parabola.
imgfmt.
app.
Area searchBBox
) 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();
if (log.
isDebugEnabled()){
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 ( searchBBox ==
null || searchBBox.
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.
warn(refs,id,treePath,
"invalid or empty or too small area");
}
} 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.
getArea().
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.
getArea().
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
).
getArea());
a.
intersect(nodes.
get(j
).
getArea());
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.
parseInt(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 =
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 a shape and the related tags to the tree.
* @param shape the part of the boundary area that should be added to the tree.
* @param boundaryId id of the originating boundary
* @param refs A string containing boundaryIds and admin-level info
* of all boundaries with lower admin levels that share the same area.
* @param clipOption true: clip the shape with the bounding box of the nodeElem,
* false: use shape without clipping
*/
private void add
(Shape shape,
String boundaryId,
String refs,
boolean clipOption
){
assert isLeaf
;
Path2D.
Double path
;
if (clipOption
){
path = ShapeSplitter.
clipShape (shape, bbox
);
// only add areas that intersect with this part of the tree
if (path ==
null)
return;
}
else
path =
new Path2D.
Double(shape
);
if (nodes ==
null)
nodes =
new ArrayList<>();
NodeElem nodeElem =
new NodeElem
(boundaryId, path, 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<>();
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
){
path.
append(area,
false);
}
add
(new Area(path
), id,
null, DO_NOT_CLIP
);
}
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<>();
this.
getAreas(areas, treePath, admLevel
);
if (areas.
isEmpty() ==
false){
Path2D.
Double path =
new Path2D.
Double(PathIterator.
WIND_NON_ZERO,
1024 * 1024);
for (Entry
<String,
List<Area>> entry : areas.
entrySet()){
for (Area area: entry.
getValue()){
path.
append(area,
false);
}
}
Area combinedArea =
new Area(path
);
return combinedArea
;
}
return new Area();
}
/**
* 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.
getArea());
if (aList ==
null){
aList =
new ArrayList<>(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.
getArea());
if (aList ==
null){
aList =
new ArrayList<>(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();
if (DEBUG
){
if (treePath.
equals(DEBUG_TREEPATH
) || DEBUG_TREEPATH.
equals("all")){
for (NodeElem nodeElem: nodes
){
nodeElem.
saveGPX("start",treePath
);
}
}
}
mergeEqualIds
();
mergeLastRectangles
();
if (DEBUG
)
printNodes
("prep", treePath
);
List<NodeElem
> reworked =
new ArrayList<>();
// 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.
getArea().
isEmpty())
continue;
Rectangle2D rCurr = currElem.
getArea().
getBounds2D();
Rectangle2D rAdd = rCurr.
createIntersection(toAdd.
getArea().
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.
getArea());
toAddxCurr.
intersect(toAdd.
getArea());
if (!isWritable
(toAddxCurr
)){
continue; // empty or only too small fragments
}
Area toAddMinusCurr =
new Area(toAdd.
getArea());
toAddMinusCurr.
subtract(currElem.
getArea());
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.
getArea().
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.
getArea());
currMinusToAdd.
subtract(toAdd.
getArea());
// remove intersection part from toAdd
toAdd.
setArea(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.
setArea(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,
" : makeDistinct 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).
getArea().
add(nodes.
get(i
).
getArea());
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.
getArea().
isRectangular() && prevNode.
getArea().
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.
getArea().
equals(lastNode.
getArea())){
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.
getArea().
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.
getArea())){
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 (NodeElem nodeElem: nodes
){
Rectangle shapeBBox = nodeElem.
shape.
getBounds();
for (int i =
0; i
< 4; i++
){
if (childs
[i
].
bbox.
intersects(shapeBBox
))
childs
[i
].
add(nodeElem.
shape, nodeElem.
boundaryId, nodeElem.
locationDataSrc, DO_CLIP
);
}
}
// 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
;
private Shape shape
; // for temp. use when splitting
// 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 element.
* @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 info
* 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 node element.
* @param boundaryId The boundary Id
* @param shape the (part of the) boundary area stored in this node
* @param refs A string containing boundaryIds and admin level info
* of all boundaries with lower admin levels that share the same area.
*/
NodeElem
(String boundaryId,
Shape shape,
String refs
){
srcPos = -
1;
this.
boundaryId = boundaryId
;
this.
shape = shape
;
this.
locationDataSrc = refs
;
calcLocTags
();
}
/**
* Create a new node element 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)
return false;
Area checkArea = getArea
();
if (checkArea ==
null || checkArea.
isEmpty()
|| checkArea.
getBounds2D().
getWidth() <= BoundaryUtil.
MIN_DIMENSION && checkArea.
getBounds2D().
getHeight() <= BoundaryUtil.
MIN_DIMENSION)
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();
try(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.
getArea());
}
// 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 outBBox = Java2DConverter.
createBbox(this.
getArea());
DataOutputStream dOutStream =
new DataOutputStream(stream
);
dOutStream.
writeUTF("AREA");
dOutStream.
writeInt(outBBox.
getMinLat());
dOutStream.
writeInt(outBBox.
getMinLong());
dOutStream.
writeInt(outBBox.
getMaxLat());
dOutStream.
writeInt(outBBox.
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();
}
private Area getArea
(){
if (shape
!=
null){
area =
new Area(shape
);
shape =
null;
}
return area
;
}
private void setArea
(Area area
) {
this.
area = area
;
this.
shape =
null;
}
/**
* 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;
}
}