/* * 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.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Iterator; 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; /** * 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<Coord>(); protected Map<Long, Node> nodeMap; protected Map<Long, Way> wayMap; protected Map<Long, Relation> relationMap; protected final Map<Long, List<Map.Entry<String,Relation>>> deferredRelationMap = new HashMap<Long, List<Map.Entry<String,Relation>>>(); // This is an explicitly given bounding box from the input file command line etc. private Area boundingBox; // This is a calculated bounding box, which is only available if there is // no given bounding box. private int minLat = Utils.toMapUnit(180.0); private int minLon = Utils.toMapUnit(180.0); private int maxLat = Utils.toMapUnit(-180.0); private int maxLon = Utils.toMapUnit(-180.0); // Options private final boolean ignoreTurnRestrictions; /** name of the tag that contains a ;-separated list of tagnames that should be removed after all elements have been processed */ public static final String MKGMAP_REMOVE_TAG = "mkgmap:removetags"; /** tagvalue of the {@link ElementSaver#MKGMAP_REMOVE_TAG} if all tags should be removed */ public static final String MKGMAP_REMOVE_TAG_ALL_KEY = "mkgmap:ALL"; public ElementSaver(EnhancedProperties args) { if (args.getProperty("preserve-element-order", false)) { nodeMap = new LinkedHashMap<Long, Node>(5000); wayMap = new LinkedHashMap<Long, Way>(5000); relationMap = new LinkedHashMap<Long, Relation>(); } else { nodeMap = new HashMap<Long, Node>(); wayMap = new HashMap<Long, Way>(); relationMap = new HashMap<Long, Relation>(); } ignoreTurnRestrictions = args.getProperty("ignore-turn-restrictions", false); } /** * 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 (boundingBox == null) { 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. * * @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) { } 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()); for (Relation r : relationMap.values()) converter.convertRelation(r); short fixmeTagKey = TagDict.getInstance().xlate("fixme"); short fixmeTagKey2 = TagDict.getInstance().xlate("FIXME"); for (Node n : nodeMap.values()){ converter.convertNode(n); if (n.getTag(fixmeTagKey) != null || n.getTag(fixmeTagKey2) != null){ n.getLocation().setFixme(true); } } 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 == Utils.toMapUnit(180.0) && maxLat == Utils.toMapUnit(-180.0)) { return new Area(0, 0, 0, 0); } 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)); } } public void deferRelation(long id, Relation rel, String role) { // The relation may be defined later in the input. // Defer the lookup. Map.Entry<String,Relation> entry = new AbstractMap.SimpleEntry<String,Relation>(role, rel); List<Map.Entry<String,Relation>> entries = deferredRelationMap.get(id); if (entries == null) { entries = new ArrayList<Map.Entry<String,Relation>>(); deferredRelationMap.put(id, entries); } entries.add(entry); } }