Rev 4548 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* 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;
}
}