package maps.osm;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.io.File;
import java.io.IOException;
/**
An OpenStreetMap map.
*/
public class OSMMap {
private static final Collection<String> ROAD_MARKERS = new HashSet<String>();
static {
ROAD_MARKERS.add("motorway");
ROAD_MARKERS.add("motorway_link");
ROAD_MARKERS.add("trunk");
ROAD_MARKERS.add("trunk_link");
ROAD_MARKERS.add("primary");
ROAD_MARKERS.add("primary_link");
ROAD_MARKERS.add("secondary");
ROAD_MARKERS.add("secondary_link");
ROAD_MARKERS.add("tertiary");
ROAD_MARKERS.add("unclassified");
ROAD_MARKERS.add("road");
ROAD_MARKERS.add("residential");
ROAD_MARKERS.add("living_street");
ROAD_MARKERS.add("service");
ROAD_MARKERS.add("track");
ROAD_MARKERS.add("services");
ROAD_MARKERS.add("pedestrian");
}
private Map<Long, OSMNode> nodes;
private Map<Long, OSMRoad> roads;
private Map<Long, OSMBuilding> buildings;
private boolean boundsCalculated;
private double minLat;
private double maxLat;
private double minLon;
private double maxLon;
/**
Construct an empty map.
*/
public OSMMap() {
boundsCalculated = false;
nodes = new HashMap<Long, OSMNode>();
roads = new HashMap<Long, OSMRoad>();
buildings = new HashMap<Long, OSMBuilding>();
}
/**
Construct a map from an XML document.
@param doc The document to read.
*/
public OSMMap(Document doc) throws OSMException {
this();
read(doc);
}
/**
Construct a map from an XML file.
@param file The file to read.
*/
public OSMMap(File file) throws OSMException, DocumentException, IOException {
this();
SAXReader reader = new SAXReader();
Document doc = reader.read(file);
read(doc);
}
/**
Construct a copy of an OSMMap over a bounded area.
@param other The map to copy.
@param minLat The minimum latitude of the new map.
@param minLon The minimum longitude of the new map.
@param maxLat The maximum latitude of the new map.
@param maxLon The maximum longitude of the new map.
*/
public OSMMap(OSMMap other, double minLat, double minLon, double maxLat, double maxLon) {
this.minLat = minLat;
this.minLon = minLon;
this.maxLat = maxLat;
this.maxLon = maxLon;
boundsCalculated = true;
nodes = new HashMap<Long, OSMNode>();
roads = new HashMap<Long, OSMRoad>();
buildings = new HashMap<Long, OSMBuilding>();
// Copy all nodes inside the bounds
for (OSMNode next : other.nodes.values()) {
double lat = next.getLatitude();
double lon = next.getLongitude();
long id = next.getID();
if (lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon) {
this.nodes.put(id, new OSMNode(id, lat, lon));
}
}
// Now copy the bits of roads and buildings that do not have missing nodes
for (OSMRoad next : other.roads.values()) {
List<Long> ids = new ArrayList<Long>(next.getNodeIDs());
for (Iterator<Long> it = ids.iterator(); it.hasNext();) {
Long nextID = it.next();
if (!nodes.containsKey(nextID)) {
it.remove();
}
}
if (!ids.isEmpty()) {
roads.put(next.getID(), new OSMRoad(next.getID(), ids));
}
}
for (OSMBuilding next : other.buildings.values()) {
boolean allFound = true;
for (Long nextID : next.getNodeIDs()) {
if (!nodes.containsKey(nextID)) {
allFound = false;
}
}
if (allFound) {
buildings.put(next.getID(), new OSMBuilding(next.getID(), new ArrayList<Long>(next.getNodeIDs())));
}
}
}
/**
Read an XML document and populate this map.
@param doc The document to read.
*/
public void read(Document doc) throws OSMException {
boundsCalculated = false;
nodes = new HashMap<Long, OSMNode>();
roads = new HashMap<Long, OSMRoad>();
buildings = new HashMap<Long, OSMBuilding>();
Element root = doc.getRootElement();
if (!"osm".equals(root.getName())) {
throw new OSMException("Invalid map file: root element must be 'osm', not " + root.getName());
}
for (Object next : root.elements("node")) {
Element e = (Element)next;
OSMNode node = processNode(e);
}
for (Object next : root.elements("way")) {
Element e = (Element)next;
processWay(e);
}
}
/**
Turn this map into XML.
@return A new XML document.
*/
public Document toXML() {
Element root = DocumentHelper.createElement("osm");
Element bounds = root.addElement("bounds");
calculateBounds();
bounds.addAttribute("minlat", String.valueOf(minLat));
bounds.addAttribute("maxlat", String.valueOf(maxLat));
bounds.addAttribute("minlon", String.valueOf(minLon));
bounds.addAttribute("maxlon", String.valueOf(maxLon));
for (OSMNode next : nodes.values()) {
Element node = root.addElement("node");
node.addAttribute("id", String.valueOf(next.getID()));
node.addAttribute("lat", String.valueOf(next.getLatitude()));
node.addAttribute("lon", String.valueOf(next.getLongitude()));
}
for (OSMRoad next : roads.values()) {
Element node = root.addElement("way");
node.addAttribute("id", String.valueOf(next.getID()));
for (Long nextID : next.getNodeIDs()) {
node.addElement("nd").addAttribute("ref", String.valueOf(nextID));
}
node.addElement("tag").addAttribute("k", "highway").addAttribute("v", "primary");
}
for (OSMBuilding next : buildings.values()) {
Element node = root.addElement("way");
node.addAttribute("id", String.valueOf(next.getID()));
for (Long nextID : next.getNodeIDs()) {
node.addElement("nd").addAttribute("ref", String.valueOf(nextID));
}
node.addElement("tag").addAttribute("k", "building").addAttribute("v", "yes");
}
return DocumentHelper.createDocument(root);
}
/**
Get the minimum longitude in this map.
@return The minimum longitude.
*/
public double getMinLongitude() {
calculateBounds();
return minLon;
}
/**
Get the maximum longitude in this map.
@return The maximum longitude.
*/
public double getMaxLongitude() {
calculateBounds();
return maxLon;
}
/**
Get the centre longitude in this map.
@return The centre longitude.
*/
public double getCentreLongitude() {
calculateBounds();
return (maxLon + minLon) / 2;
}
/**
Get the minimum latitude in this map.
@return The minimum latitude.
*/
public double getMinLatitude() {
calculateBounds();
return minLat;
}
/**
Get the maximum latitude in this map.
@return The maximum latitude.
*/
public double getMaxLatitude() {
calculateBounds();
return maxLat;
}
/**
Get the centre latitude in this map.
@return The centre latitude.
*/
public double getCentreLatitude() {
calculateBounds();
return (maxLat + minLat) / 2;
}
/**
Get all nodes in the map.
@return All nodes.
*/
public Collection<OSMNode> getNodes() {
return new HashSet<OSMNode>(nodes.values());
}
/**
Remove a node.
@param node The node to remove.
*/
public void removeNode(OSMNode node) {
nodes.remove(node.getID());
}
/**
Get a node by ID.
@param id The ID of the node.
@return The node with the given ID or null.
*/
public OSMNode getNode(Long id) {
return nodes.get(id);
}
/**
Get the nearest node to a point.
@param lat The latitude of the point.
@param lon The longitude of the point.
@return The nearest node.
*/
public OSMNode getNearestNode(double lat, double lon) {
double smallest = Double.MAX_VALUE;
OSMNode best = null;
for (OSMNode next : nodes.values()) {
double d1 = next.getLatitude() - lat;
double d2 = next.getLongitude() - lon;
double d = (d1 * d1) + (d2 * d2);
if (d < smallest) {
best = next;
smallest = d;
}
}
return best;
}
/**
Replace a node and update all references.
@param old The node to replace.
@param replacement The replacement node.
*/
public void replaceNode(OSMNode old, OSMNode replacement) {
for (OSMRoad r : roads.values()) {
r.replace(old.getID(), replacement.getID());
}
for (OSMBuilding b : buildings.values()) {
b.replace(old.getID(), replacement.getID());
}
removeNode(old);
}
/**
Get all roads.
@return All roads.
*/
public Collection<OSMRoad> getRoads() {
return new HashSet<OSMRoad>(roads.values());
}
/**
Remove a road.
@param road The road to remove.
*/
public void removeRoad(OSMRoad road) {
roads.remove(road.getID());
}
/**
Get all buildings.
@return All buildings.
*/
public Collection<OSMBuilding> getBuildings() {
return new HashSet<OSMBuilding>(buildings.values());
}
/**
Remove a building.
@param building The building to remove.
*/
public void removeBuilding(OSMBuilding building) {
buildings.remove(building.getID());
}
private void calculateBounds() {
if (boundsCalculated) {
return;
}
minLat = Double.POSITIVE_INFINITY;
maxLat = Double.NEGATIVE_INFINITY;
minLon = Double.POSITIVE_INFINITY;
maxLon = Double.NEGATIVE_INFINITY;
for (OSMNode node : nodes.values()) {
minLat = Math.min(minLat, node.getLatitude());
maxLat = Math.max(maxLat, node.getLatitude());
minLon = Math.min(minLon, node.getLongitude());
maxLon = Math.max(maxLon, node.getLongitude());
}
boundsCalculated = true;
}
private OSMNode processNode(Element e) {
long id = Long.parseLong(e.attributeValue("id"));
double lat = Double.parseDouble(e.attributeValue("lat"));
double lon = Double.parseDouble(e.attributeValue("lon"));
OSMNode node = new OSMNode(id, lat, lon);
nodes.put(id, node);
return node;
}
private void processWay(Element e) {
long id = Long.parseLong(e.attributeValue("id"));
List<Long> ids = new ArrayList<Long>();
for (Object next : e.elements("nd")) {
Element nd = (Element)next;
Long nextID = Long.parseLong(nd.attributeValue("ref"));
ids.add(nextID);
}
// Is this way a road or a building?
boolean road = false;
boolean building = false;
for (Object next : e.elements("tag")) {
Element tag = (Element)next;
building = building || tagSignifiesBuilding(tag);
road = road || tagSignifiesRoad(tag);
}
if (building) {
buildings.put(id, new OSMBuilding(id, ids));
}
else if (road) {
roads.put(id, new OSMRoad(id, ids));
}
}
private boolean tagSignifiesRoad(Element tag) {
String key = tag.attributeValue("k");
String value = tag.attributeValue("v");
if (!"highway".equals(key)) {
return false;
}
return ROAD_MARKERS.contains(value);
}
private boolean tagSignifiesBuilding(Element tag) {
String key = tag.attributeValue("k");
String value = tag.attributeValue("v");
if ("building".equals(key)) {
return "yes".equals(value);
}
if ("rcr:building".equals(key)) {
return "1".equals(value);
}
return false;
}
}