/*
* Copyright (C) 2010.
*
* 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 java.util.AbstractMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.LineClipper;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.MultiHashMap;
/**
* This is where we save the elements read from any of the file formats that
* are in OSM format. OSM format means that there are nodes, ways and relations
* and they have tags.
*
* Both the XML format and the binary format use this.
*
* In the early days of mkgmap, the nodes and ways were converted as soon
* as they were encountered in the input file. After relations that is not
* possible, you have to save up all the nodes and ways as they might be
* needed for relations.
*
* We also want access to the other ways/nodes to generate sea polygons,
* prepare for routing etc.
*
* @author Steve Ratcliffe
*/
public class ElementSaver
{
private static final Logger log =
Logger.
getLogger(ElementSaver.
class);
protected OSMId2ObjectMap
<Coord
> coordMap =
new OSMId2ObjectMap
<>();
protected Map<Long,
Node> nodeMap
;
protected Map<Long, Way
> wayMap
;
protected Map<Long,
Relation> relationMap
;
protected final MultiHashMap
<Long,
Map.Entry<String,
Relation>> deferredRelationMap =
new MultiHashMap
<>();
// This is an explicitly given bounding box from the input file command line etc.
private Area boundingBox
;
// This is a calculated bounding box
private int minLat =
Integer.
MAX_VALUE;
private int minLon =
Integer.
MAX_VALUE;
private int maxLat =
Integer.
MIN_VALUE;
private int maxLon =
Integer.
MIN_VALUE;
// Options
private final boolean ignoreTurnRestrictions
;
private final String[] deadEndArgs
;
/** name of the tag that contains a ;-separated list of tag names that should be removed after all elements have been processed */
public static final short TKM_REMOVETAGS = TagDict.
getInstance().
xlate("mkgmap:removetags");
public ElementSaver
(EnhancedProperties args
) {
if (args.
getProperty("preserve-element-order",
false)) {
nodeMap =
new LinkedHashMap<>(5000);
wayMap =
new LinkedHashMap<>(5000);
relationMap =
new LinkedHashMap<>();
} else {
nodeMap =
new HashMap<>();
wayMap =
new HashMap<>();
relationMap =
new HashMap<>();
}
ignoreTurnRestrictions = args.
getProperty("ignore-turn-restrictions",
false) ||
!args.
containsKey("route");
deadEndArgs = args.
getProperty("dead-ends",
"fixme,FIXME").
split(",");
}
/**
* Store the {@link Coord} with the associated OSM id.
* We use this to calculate a bounding box in the situation where none is
* given. In the usual case where there is a bounding box, then nothing
* is done.
*
* @param id the OSM id
* @param co The point.
*/
public void addPoint
(long id, Coord co
) {
coordMap.
put(id, co
);
if (co.
getLatitude() < minLat
)
minLat = co.
getLatitude();
if (co.
getLatitude() > maxLat
)
maxLat = co.
getLatitude();
if (co.
getLongitude() < minLon
)
minLon = co.
getLongitude();
if (co.
getLongitude() > maxLon
)
maxLon = co.
getLongitude();
}
/**
* Add the given node and save it. The node should have tags, if not it should be a member of a relation.
*
* @param node The osm node.
*/
public void addNode
(Node node
) {
nodeMap.
put(node.
getId(), node
);
}
/**
* Add the given way.
*
* @param way The osm way.
*/
public void addWay
(Way way
) {
wayMap.
put(way.
getId(), way
);
/*
Way old = wayMap.put(way.getId(), way);
if (old != null){
if (old == way)
log.error("way",way.toBrowseURL(),"was added again");
else
log.error("duplicate way",way.toBrowseURL(),"replaces previous way");
}
*/
}
/**
* Add the given relation.
*
* @param rel The osm relation.
*/
public void addRelation
(Relation rel
) {
String type = rel.
getTag("type");
if (type ==
null) {
// maybe set rel to null?
} else if ("multipolygon".
equals(type
) ||
"boundary".
equals(type
)) {
rel = createMultiPolyRelation
(rel
);
} else if("restriction".
equals(type
) || type.
startsWith("restriction:")) {
if (ignoreTurnRestrictions
)
rel =
null;
else if (rel.
getTag("restriction") ==
null && rel.
getTagsWithPrefix("restriction:",
false).
isEmpty()) {
log.
warn("ignoring unspecified/unsupported restriction " + rel.
toBrowseURL());
} else {
rel =
new RestrictionRelation
(rel
);
}
}
if(rel
!=
null) {
long id = rel.
getId();
relationMap.
put(rel.
getId(), rel
);
rel.
processElements();
List<Map.Entry<String,
Relation>> entries = deferredRelationMap.
remove(id
);
if (entries
!=
null) {
for (Map.Entry<String,
Relation> entry : entries
) {
entry.
getValue().
addElement(entry.
getKey(), rel
);
}
}
}
}
/**
* Create a multipolygon relation. Has to be here as they use shared maps.
* Would like to change how the constructor works so that was not needed.
* @param rel The original relation, that the result will replace.
* @return A new multi polygon relation, based on the input relation.
*/
public Relation createMultiPolyRelation
(Relation rel
) {
return new MultiPolygonRelation
(rel, wayMap, getBoundingBox
());
}
public SeaPolygonRelation createSeaPolyRelation
(Relation rel
) {
return new SeaPolygonRelation
(rel, wayMap, getBoundingBox
());
}
public void setBoundingBox
(Area bbox
) {
boundingBox = bbox
;
}
public Coord getCoord
(long id
) {
return coordMap.
get(id
);
}
public Node getNode
(long id
) {
return nodeMap.
get(id
);
}
public Way getWay
(long id
) {
return wayMap.
get(id
);
}
public Relation getRelation
(long id
) {
return relationMap.
get(id
);
}
public void finishLoading
() {
coordMap =
null;
}
/**
* After the input file is read, this is called to convert the saved information
* into the general intermediate format.
*
* @param converter The Converter to use.
*/
public void convert
(OsmConverter converter
) {
// We only do this if an explicit bounding box was given.
if (boundingBox
!=
null)
makeBoundaryNodes
();
converter.
setBoundingBox(getBoundingBox
());
converter.
augmentWith(this);
for (Relation r : relationMap.
values()) {
converter.
convertRelation(r
);
}
for (Node n : nodeMap.
values()) {
converter.
convertNode(n
);
for (String deadEndArg : deadEndArgs
) {
String[] arg = deadEndArg.
split("=",
2);
String key = arg
[0];
String value = arg.
length < 2 ||
"*".
equals(arg
[1]) ? "" : arg
[1];
String tagValue = n.
getTag(key
);
if (tagValue
!=
null && (tagValue.
equals(value
) ||
(value.
isEmpty()))) {
Coord location = n.
getLocation();
if (location
!=
null)
location.
setSkipDeadEndCheck(true);
break;
}
}
}
nodeMap =
null;
Iterator<Way
> wayIter = wayMap.
values().
iterator();
while (wayIter.
hasNext()){
Way way = wayIter.
next();
converter.
convertWay(way
);
wayIter.
remove();
}
wayMap =
null;
converter.
end();
relationMap =
null;
deferredRelationMap.
clear();
}
/**
*
* "soft clip" each way that crosses a boundary by adding a point
* at each place where it meets the boundary
*/
private void makeBoundaryNodes
() {
log.
info("Making boundary nodes");
int numBoundaryNodesDetected =
0;
int numBoundaryNodesAdded =
0;
for(Way way : wayMap.
values()) {
List<Coord
> points = way.
getPoints();
// clip each segment in the way against the bounding box
// to find the positions of the boundary nodes - loop runs
// backwards so we can safely insert points into way
for (int i = points.
size() -
1; i
>=
1; --i
) {
Coord
[] pair =
{ points.
get(i -
1), points.
get(i
) };
Coord
[] clippedPair = LineClipper.
clip(getBoundingBox
(), pair,
true);
// we're only interested in segments that touch the
// boundary
if (clippedPair
!=
null) {
// the segment touches the boundary or is
// completely inside the bounding box
if (clippedPair
[1] != points.
get(i
)) {
// the second point in the segment is outside
// of the boundary
assert clippedPair
[1].
getOnBoundary();
// insert boundary point before the second point
points.
add(i, clippedPair
[1]);
++numBoundaryNodesAdded
;
} else if (clippedPair
[1].
getOnBoundary()) {
++numBoundaryNodesDetected
;
}
if (clippedPair
[0] != points.
get(i -
1)) {
// the first point in the segment is outside
// of the boundary
assert clippedPair
[0].
getOnBoundary();
// insert boundary point after the first point
points.
add(i, clippedPair
[0]);
++numBoundaryNodesAdded
;
} else if (clippedPair
[0].
getOnBoundary()) {
++numBoundaryNodesDetected
;
}
}
}
}
log.
info("Making boundary nodes - finished (" + numBoundaryNodesAdded +
" added, " + numBoundaryNodesDetected +
" detected)");
}
public Map<Long,
Node> getNodes
() {
return nodeMap
;
}
public Map<Long, Way
> getWays
() {
return wayMap
;
}
public Map<Long,
Relation> getRelations
() {
return relationMap
;
}
/**
* Get the bounding box. This is either the one that was explicitly included in the input
* file, or if none was given, the calculated one.
*/
public Area getBoundingBox
() {
if (boundingBox
!=
null) {
return boundingBox
;
} else if (minLat
> maxLat
) {
return new Area(0,
0,
0,
0);
} else {
return getDataBoundingBox
();
}
}
/**
* Get the bounding box of all nodes. Returns null if no point was read.
*/
public Area getDataBoundingBox
() {
if (minLat
> maxLat
) {
return null;
} else {
// calculate an area that is slightly larger so that high precision coordinates
// are safely within the bbox.
return new Area(Math.
max(Utils.
toMapUnit(-
90.0), minLat-
1),
Math.
max(Utils.
toMapUnit(-
180.0), minLon-
1),
Math.
min(Utils.
toMapUnit(90.0), maxLat+
1),
Math.
min(Utils.
toMapUnit(180.0), maxLon+
1));
}
}
/**
* Handle the case that a relation refers to another relation which was not yet found in the input.
* The relation may be defined later in the input. Defer the lookup.
* @param id the id of the not yet known relation
* @param parentRel the parent relation
* @param role the role of the not yet known relation in the parent relation
*/
public void deferRelation
(long id,
Relation parentRel,
String role
) {
deferredRelationMap.
add(id,
new AbstractMap.
SimpleEntry<>(role, parentRel
));
}
/**
* Return the node object associated with the given id or create one.
* This is called for the node members of a relation. We always want a node for them, not just a coord.
* @param id the node id
* @return the existing node or a newly created one (without tags) or null if no coord is associated with the id
*/
public Node getOrCreateNode
(long id
) {
Node node = nodeMap.
get(id
);
if (node ==
null) {
// we didn't make a node for this point earlier,
// do it now (if it exists)
Coord co = getCoord
(id
);
if (co
!=
null) {
node =
new Node(id, co
);
addNode
(node
);
}
}
return node
;
}
}