/*
* Copyright 2010, 2011, 2012 mapsforge.org
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.map.writer;
import gnu.trove.map.hash.TShortIntHashMap;
import gnu.trove.procedure.TShortIntProcedure;
import gnu.trove.set.hash.TShortHashSet;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.mapsforge.map.writer.model.OSMTag;
import org.mapsforge.map.writer.osmosis.MapFileWriterTask;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Reorders and maps tag ids according to their frequency in the input data. Ids are remapped so that the most frequent
* entities receive the lowest ids.
*
* @author bross
*/
public final class OSMTagMapping {
private static OSMTagMapping mapping;
private static final Logger LOGGER = Logger.getLogger(OSMTagMapping.class.getName());
// we use LinkedHashMaps as they guarantee to uphold the
// insertion order when iterating over the key or value "set"
private final Map<String, OSMTag> stringToPoiTag = new LinkedHashMap<String, OSMTag>();
private final Map<String, OSMTag> stringToWayTag = new LinkedHashMap<String, OSMTag>();
private final Map<Short, OSMTag> idToPoiTag = new LinkedHashMap<Short, OSMTag>();
private final Map<Short, OSMTag> idToWayTag = new LinkedHashMap<Short, OSMTag>();
private final Map<Short, Set<OSMTag>> poiZoomOverrides = new LinkedHashMap<Short, Set<OSMTag>>();
private final Map<Short, Set<OSMTag>> wayZoomOverrides = new LinkedHashMap<Short, Set<OSMTag>>();
private final Map<Short, Short> optimizedPoiIds = new LinkedHashMap<Short, Short>();
private final Map<Short, Short> optimizedWayIds = new LinkedHashMap<Short, Short>();
private short poiID = 0;
private short wayID = 0;
private static final String XPATH_EXPRESSION_DEFAULT_ZOOM = "/tag-mapping/@default-zoom-appear";
private static final String XPATH_EXPRESSION_POIS = "//pois/osm-tag["
+ "(../@enabled='true' or not(../@enabled)) and (./@enabled='true' or not(./@enabled)) "
+ "or (../@enabled='false' and ./@enabled='true')]";
private static final String XPATH_EXPRESSION_WAYS = "//ways/osm-tag["
+ "(../@enabled='true' or not(../@enabled)) and (./@enabled='true' or not(./@enabled)) "
+ "or (../@enabled='false' and ./@enabled='true')]";
/**
* @return a new instance
*/
public static synchronized OSMTagMapping getInstance() {
if (mapping == null) {
mapping = getInstance(MapFileWriterTask.class.getClassLoader().getResource("tag-mapping.xml"));
}
return mapping;
}
/**
* @param tagConf
* the {@link URL} to a file that contains a tag configuration
* @return a new instance
*/
public static OSMTagMapping getInstance(URL tagConf) {
if (mapping != null) {
throw new IllegalStateException("mapping already initialized");
}
mapping = new OSMTagMapping(tagConf);
return mapping;
}
private OSMTagMapping(URL tagConf) {
try {
byte defaultZoomAppear;
// ---- Parse XML file ----
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(tagConf.openStream());
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression xe = xpath.compile(XPATH_EXPRESSION_DEFAULT_ZOOM);
defaultZoomAppear = Byte.parseByte((String) xe.evaluate(document, XPathConstants.STRING));
final HashMap<Short, Set<String>> tmpPoiZoomOverrides = new HashMap<Short, Set<String>>();
final HashMap<Short, Set<String>> tmpWayZoomOverrides = new HashMap<Short, Set<String>>();
// ---- Get list of poi nodes ----
xe = xpath.compile(XPATH_EXPRESSION_POIS);
NodeList pois = (NodeList) xe.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < pois.getLength(); i++) {
NamedNodeMap attributes = pois.item(i).getAttributes();
String key = attributes.getNamedItem("key").getTextContent();
String value = attributes.getNamedItem("value").getTextContent();
String[] equivalentValues = null;
if (attributes.getNamedItem("equivalent-values") != null) {
equivalentValues = attributes.getNamedItem("equivalent-values").getTextContent().split(",");
}
byte zoom = attributes.getNamedItem("zoom-appear") == null ? defaultZoomAppear : Byte
.parseByte(attributes.getNamedItem("zoom-appear").getTextContent());
boolean renderable = attributes.getNamedItem("renderable") == null ? true : Boolean
.parseBoolean(attributes.getNamedItem("renderable").getTextContent());
boolean forcePolygonLine = attributes.getNamedItem("force-polygon-line") == null ? false : Boolean
.parseBoolean(attributes.getNamedItem("force-polygon-line").getTextContent());
OSMTag osmTag = new OSMTag(this.poiID, key, value, zoom, renderable, forcePolygonLine);
if (this.stringToPoiTag.containsKey(osmTag.tagKey())) {
LOGGER.warning("duplicate osm-tag found in tag-mapping configuration (ignoring): " + osmTag);
continue;
}
LOGGER.finest("adding poi: " + osmTag);
this.stringToPoiTag.put(osmTag.tagKey(), osmTag);
if (equivalentValues != null) {
for (String equivalentValue : equivalentValues) {
this.stringToPoiTag.put(OSMTag.tagKey(key, equivalentValue), osmTag);
}
}
this.idToPoiTag.put(Short.valueOf(this.poiID), osmTag);
// also fill optimization mapping with identity
this.optimizedPoiIds.put(Short.valueOf(this.poiID), Short.valueOf(this.poiID));
// check if this tag overrides the zoom level spec of another tag
NodeList zoomOverrideNodes = pois.item(i).getChildNodes();
for (int j = 0; j < zoomOverrideNodes.getLength(); j++) {
Node overriddenNode = zoomOverrideNodes.item(j);
if (overriddenNode instanceof Element) {
String keyOverridden = overriddenNode.getAttributes().getNamedItem("key").getTextContent();
String valueOverridden = overriddenNode.getAttributes().getNamedItem("value").getTextContent();
Set<String> s = tmpPoiZoomOverrides.get(Short.valueOf(this.poiID));
if (s == null) {
s = new HashSet<String>();
tmpPoiZoomOverrides.put(Short.valueOf(this.poiID), s);
}
s.add(OSMTag.tagKey(keyOverridden, valueOverridden));
}
}
this.poiID++;
}
// ---- Get list of way nodes ----
xe = xpath.compile(XPATH_EXPRESSION_WAYS);
NodeList ways = (NodeList) xe.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < ways.getLength(); i++) {
NamedNodeMap attributes = ways.item(i).getAttributes();
String key = attributes.getNamedItem("key").getTextContent();
String value = attributes.getNamedItem("value").getTextContent();
String[] equivalentValues = null;
if (attributes.getNamedItem("equivalent-values") != null) {
equivalentValues = attributes.getNamedItem("equivalent-values").getTextContent().split(",");
}
byte zoom = attributes.getNamedItem("zoom-appear") == null ? defaultZoomAppear : Byte
.parseByte(attributes.getNamedItem("zoom-appear").getTextContent());
boolean renderable = attributes.getNamedItem("renderable") == null ? true : Boolean
.parseBoolean(attributes.getNamedItem("renderable").getTextContent());
boolean forcePolygonLine = attributes.getNamedItem("force-polygon-line") == null ? false : Boolean
.parseBoolean(attributes.getNamedItem("force-polygon-line").getTextContent());
OSMTag osmTag = new OSMTag(this.wayID, key, value, zoom, renderable, forcePolygonLine);
if (this.stringToWayTag.containsKey(osmTag.tagKey())) {
LOGGER.warning("duplicate osm-tag found in tag-mapping configuration (ignoring): " + osmTag);
continue;
}
LOGGER.finest("adding way: " + osmTag);
this.stringToWayTag.put(osmTag.tagKey(), osmTag);
if (equivalentValues != null) {
for (String equivalentValue : equivalentValues) {
this.stringToWayTag.put(OSMTag.tagKey(key, equivalentValue), osmTag);
}
}
this.idToWayTag.put(Short.valueOf(this.wayID), osmTag);
// also fill optimization mapping with identity
this.optimizedWayIds.put(Short.valueOf(this.wayID), Short.valueOf(this.wayID));
// check if this tag overrides the zoom level spec of another tag
NodeList zoomOverrideNodes = ways.item(i).getChildNodes();
for (int j = 0; j < zoomOverrideNodes.getLength(); j++) {
Node overriddenNode = zoomOverrideNodes.item(j);
if (overriddenNode instanceof Element) {
String keyOverridden = overriddenNode.getAttributes().getNamedItem("key").getTextContent();
String valueOverridden = overriddenNode.getAttributes().getNamedItem("value").getTextContent();
Set<String> s = tmpWayZoomOverrides.get(Short.valueOf(this.wayID));
if (s == null) {
s = new HashSet<String>();
tmpWayZoomOverrides.put(Short.valueOf(this.wayID), s);
}
s.add(OSMTag.tagKey(keyOverridden, valueOverridden));
}
}
this.wayID++;
}
// copy temporary values from zoom-override data sets
for (Entry<Short, Set<String>> entry : tmpPoiZoomOverrides.entrySet()) {
Set<OSMTag> overriddenTags = new HashSet<OSMTag>();
for (String tagString : entry.getValue()) {
OSMTag tag = this.stringToPoiTag.get(tagString);
if (tag != null) {
overriddenTags.add(tag);
}
}
if (!overriddenTags.isEmpty()) {
this.poiZoomOverrides.put(entry.getKey(), overriddenTags);
}
}
for (Entry<Short, Set<String>> entry : tmpWayZoomOverrides.entrySet()) {
Set<OSMTag> overriddenTags = new HashSet<OSMTag>();
for (String tagString : entry.getValue()) {
OSMTag tag = this.stringToWayTag.get(tagString);
if (tag != null) {
overriddenTags.add(tag);
}
}
if (!overriddenTags.isEmpty()) {
this.wayZoomOverrides.put(entry.getKey(), overriddenTags);
}
}
// ---- Error handling ----
} catch (SAXParseException spe) {
LOGGER.severe("\n** Parsing error, line " + spe.getLineNumber() + ", uri " + spe.getSystemId());
throw new IllegalStateException(spe);
} catch (SAXException sxe) {
throw new IllegalStateException(sxe);
} catch (ParserConfigurationException pce) {
throw new IllegalStateException(pce);
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
} catch (XPathExpressionException e) {
throw new IllegalStateException(e);
}
}
/**
* @param tagSet
* the tag set
* @return the minimum zoom level of all tags in the tag set
*/
public byte getZoomAppearPOI(short[] tagSet) {
if (tagSet == null || tagSet.length == 0) {
return Byte.MAX_VALUE;
}
TShortHashSet tmp = new TShortHashSet(tagSet);
if (!this.poiZoomOverrides.isEmpty()) {
for (short s : tagSet) {
Set<OSMTag> overriddenTags = this.poiZoomOverrides.get(Short.valueOf(s));
if (overriddenTags != null) {
for (OSMTag osmTag : overriddenTags) {
tmp.remove(osmTag.getId());
}
}
}
if (tmp.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (short s : tagSet) {
sb.append(this.idToPoiTag.get(Short.valueOf(s)).tagKey() + "; ");
}
LOGGER.severe("ERROR: You have a cycle in your zoom-override definitions. Look for these tags: "
+ sb.toString());
}
}
byte zoomAppear = Byte.MAX_VALUE;
for (short s : tmp.toArray()) {
OSMTag tag = this.idToPoiTag.get(Short.valueOf(s));
if (tag.isRenderable()) {
zoomAppear = (byte) Math.min(zoomAppear, tag.getZoomAppear());
}
}
return zoomAppear;
}
/**
* @param tagSet
* the tag set
* @return the minimum zoom level of all the tags in the set
*/
public byte getZoomAppearWay(short[] tagSet) {
if (tagSet == null || tagSet.length == 0) {
return Byte.MAX_VALUE;
}
TShortHashSet tmp = new TShortHashSet(tagSet);
if (!this.wayZoomOverrides.isEmpty()) {
for (short s : tagSet) {
Set<OSMTag> overriddenTags = this.wayZoomOverrides.get(Short.valueOf(s));
if (overriddenTags != null) {
for (OSMTag osmTag : overriddenTags) {
tmp.remove(osmTag.getId());
}
}
}
if (tmp.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (short s : tagSet) {
sb.append(this.idToWayTag.get(Short.valueOf(s)).tagKey() + "; ");
}
LOGGER.severe("ERROR: You have a cycle in your zoom-override definitions. Look for these tags: "
+ sb.toString());
}
}
byte zoomAppear = Byte.MAX_VALUE;
for (short s : tmp.toArray()) {
OSMTag tag = this.idToWayTag.get(Short.valueOf(s));
if (tag.isRenderable()) {
zoomAppear = (byte) Math.min(zoomAppear, tag.getZoomAppear());
}
}
return zoomAppear;
}
/**
* @param key
* the key
* @param value
* the value
* @return the corresponding {@link OSMTag}
*/
public OSMTag getWayTag(String key, String value) {
return this.stringToWayTag.get(OSMTag.tagKey(key, value));
}
/**
* @param key
* the key
* @param value
* the value
* @return the corresponding {@link OSMTag}
*/
public OSMTag getPoiTag(String key, String value) {
return this.stringToPoiTag.get(OSMTag.tagKey(key, value));
}
/**
* @param id
* the id
* @return the corresponding {@link OSMTag}
*/
public OSMTag getWayTag(short id) {
return this.idToWayTag.get(Short.valueOf(id));
}
/**
* @param id
* the id
* @return the corresponding {@link OSMTag}
*/
public OSMTag getPoiTag(short id) {
return this.idToPoiTag.get(Short.valueOf(id));
}
// /**
// * @param tags
// * the tags
// * @return
// */
// private static short[] tagIDsFromList(List<OSMTag> tags) {
// short[] tagIDs = new short[tags.size()];
// int i = 0;
// for (OSMTag tag : tags) {
// tagIDs[i++] = tag.getId();
// }
//
// return tagIDs;
// }
/**
* @return a mapping that maps original tag ids to the optimized ones
*/
public Map<Short, Short> getOptimizedPoiIds() {
return this.optimizedPoiIds;
}
/**
* @return a mapping that maps original tag ids to the optimized ones
*/
public Map<Short, Short> getOptimizedWayIds() {
return this.optimizedWayIds;
}
/**
* @param histogram
* a histogram that represents the frequencies of tags
*/
public void optimizePoiOrdering(TShortIntHashMap histogram) {
this.optimizedPoiIds.clear();
final TreeSet<HistogramEntry> poiOrdering = new TreeSet<OSMTagMapping.HistogramEntry>();
histogram.forEachEntry(new TShortIntProcedure() {
@Override
public boolean execute(short tag, int amount) {
poiOrdering.add(new HistogramEntry(tag, amount));
return true;
}
});
short tmpPoiID = 0;
OSMTag currentTag = null;
for (HistogramEntry histogramEntry : poiOrdering.descendingSet()) {
currentTag = this.idToPoiTag.get(Short.valueOf(histogramEntry.id));
this.optimizedPoiIds.put(Short.valueOf(histogramEntry.id), Short.valueOf(tmpPoiID));
LOGGER.finer("adding poi tag: " + currentTag.tagKey() + " id:" + tmpPoiID + " amount: "
+ histogramEntry.amount);
tmpPoiID++;
}
}
/**
* @param histogram
* a histogram that represents the frequencies of tags
*/
public void optimizeWayOrdering(TShortIntHashMap histogram) {
this.optimizedWayIds.clear();
final TreeSet<HistogramEntry> wayOrdering = new TreeSet<OSMTagMapping.HistogramEntry>();
histogram.forEachEntry(new TShortIntProcedure() {
@Override
public boolean execute(short tag, int amount) {
wayOrdering.add(new HistogramEntry(tag, amount));
return true;
}
});
short tmpWayID = 0;
OSMTag currentTag = null;
for (HistogramEntry histogramEntry : wayOrdering.descendingSet()) {
currentTag = this.idToWayTag.get(Short.valueOf(histogramEntry.id));
this.optimizedWayIds.put(Short.valueOf(histogramEntry.id), Short.valueOf(tmpWayID));
LOGGER.finer("adding way tag: " + currentTag.tagKey() + " id:" + tmpWayID + " amount: "
+ histogramEntry.amount);
tmpWayID++;
}
}
private class HistogramEntry implements Comparable<HistogramEntry> {
final short id;
final int amount;
public HistogramEntry(short id, int amount) {
super();
this.id = id;
this.amount = amount;
}
/**
* First order: amount Second order: id (reversed order).
*/
@Override
public int compareTo(HistogramEntry o) {
if (this.amount > o.amount) {
return 1;
} else if (this.amount < o.amount) {
return -1;
} else {
if (this.id < o.id) {
return 1;
} else if (this.id > o.id) {
return -1;
} else {
return 0;
}
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + this.amount;
result = prime * result + this.id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
HistogramEntry other = (HistogramEntry) obj;
if (!getOuterType().equals(other.getOuterType())) {
return false;
}
if (this.amount != other.amount) {
return false;
}
if (this.id != other.id) {
return false;
}
return true;
}
private OSMTagMapping getOuterType() {
return OSMTagMapping.this;
}
}
}