Rev 2583 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* Copyright (C) 2007 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License 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.
*
*
* Author: Steve Ratcliffe
* Create date: Feb 17, 2008
*/
package uk.me.parabola.mkgmap.osmstyle;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.Option;
import uk.me.parabola.mkgmap.OptionProcessor;
import uk.me.parabola.mkgmap.Options;
import uk.me.parabola.mkgmap.build.LocatorUtil;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LineAdder;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.osmstyle.actions.Action;
import uk.me.parabola.mkgmap.osmstyle.actions.NameAction;
import uk.me.parabola.mkgmap.osmstyle.eval.EqualsOp;
import uk.me.parabola.mkgmap.osmstyle.eval.ExistsOp;
import uk.me.parabola.mkgmap.osmstyle.eval.Op;
import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.reader.osm.Style;
import uk.me.parabola.mkgmap.reader.osm.StyleInfo;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.TokenScanner;
import uk.me.parabola.util.EnhancedProperties;
/**
* A style is a collection of files that describe the mapping between the OSM
* features and the garmin features. This file reads in those files and
* provides methods for using the information.
*
* The files are either contained in a directory, in a package or in a zip'ed
* file.
*
* @author Steve Ratcliffe
*/
public class StyleImpl implements Style {
private static final Logger log = Logger.getLogger(StyleImpl.class);
public static final boolean WITH_CHECKS = true;
public static final boolean WITHOUT_CHECKS = false;
// This is max the version that we understand
private static final int VERSION = 1;
// General options just have a value and don't need any special processing.
private static final Collection<String> OPTION_LIST = new ArrayList<String>(
Arrays.asList("levels", "extra-used-tags"));
// File names
private static final String FILE_VERSION = "version";
private static final String FILE_INFO = "info";
private static final String FILE_FEATURES = "map-features.csv";
private static final String FILE_OPTIONS = "options";
private static final String FILE_OVERLAYS = "overlays";
// Patterns
private static final Pattern COMMA_OR_SPACE_PATTERN = Pattern.compile("[,\\s]+");
private static final Pattern EQUAL_PATTERN = Pattern.compile("=");
// A handle on the style directory or file.
private final StyleFileLoader fileLoader;
private final String location;
// The general information in the 'info' file.
private StyleInfo info = new StyleInfo();
// Set if this style is based on another one.
private final List<StyleImpl> baseStyles = new ArrayList<StyleImpl>();
// A list of tag names to be used as the element name
private List<String> nameTagList;
// Options from the option file that are used outside this file.
private final Map<String, String> generalOptions = new HashMap<String, String>();
private final RuleSet lines = new RuleSet();
private final RuleSet polygons = new RuleSet();
private final RuleSet nodes = new RuleSet();
private final RuleSet relations = new RuleSet();
private OverlayReader overlays;
private final boolean performChecks;
/**
* Create a style from the given location and name.
* @param loc The location of the style. Can be null to mean just check
* the classpath.
* @param name The name. Can be null if the location isn't. If it is
* null then we just check for the first version file that can be found.
* @throws FileNotFoundException If the file doesn't exist. This can
* include the version file being missing.
*/
public StyleImpl(String styleFile, String name) throws FileNotFoundException {
this(styleFile, name, new EnhancedProperties(), WITHOUT_CHECKS);
}
/**
* Create a style from the given location and name.
* @param loc The location of the style. Can be null to mean just check
* the classpath.
* @param name The name. Can be null if the location isn't. If it is
* null then we just check for the first version file that can be found.
* @param props optional program properties (may be null)
* @throws FileNotFoundException If the file doesn't exist. This can
* include the version file being missing.
*/
public StyleImpl(String loc, String name, EnhancedProperties props, boolean performChecks) throws FileNotFoundException {
location = loc;
fileLoader = StyleFileLoader.createStyleLoader(loc, name);
this.performChecks = performChecks;
nameTagList = LocatorUtil.getNameTags(props);
// There must be a version file, if not then we don't create the style.
checkVersion();
readInfo();
for (String baseName : info.baseStyles())
readBaseStyle(baseName, props);
for (StyleImpl baseStyle : baseStyles)
mergeOptions(baseStyle);
readOptions();
// read overlays before the style rules to be able to ignore overlaid "wrong" types.
readOverlays();
readRules();
readMapFeatures();
ListIterator<StyleImpl> listIterator = baseStyles.listIterator(baseStyles.size());
while (listIterator.hasPrevious())
mergeRules(listIterator.previous());
// OR: other way
//for (StyleImpl s : baseStyles)
// mergeRules(s);
}
public String getOption(String name) {
return generalOptions.get(name);
}
public StyleInfo getInfo() {
return info;
}
public Rule getNodeRules() {
nodes.prepare();
return nodes;
}
public Rule getWayRules() {
RuleSet r = new RuleSet();
r.addAll(lines);
r.addAll(polygons);
r.prepare();
return r;
}
public Rule getLineRules() {
lines.prepare();
return lines;
}
public Rule getPolygonRules() {
polygons.prepare();
return polygons;
}
public Rule getRelationRules() {
relations.prepare();
return relations;
}
public LineAdder getOverlays(final LineAdder lineAdder) {
LineAdder adder = null;
if (overlays != null) {
adder = new LineAdder() {
public void add(MapLine element) {
overlays.addLine(element, lineAdder);
}
};
}
return adder;
}
public Set<String> getUsedTags() {
Set<String> set = new HashSet<String>();
set.addAll(relations.getUsedTags());
set.addAll(lines.getUsedTags());
set.addAll(polygons.getUsedTags());
set.addAll(nodes.getUsedTags());
// this is to allow style authors to say that tags are really used even
// if they are not found in the style file. This is mostly to work
// around situations that we haven't thought of - the style is expected
// to get it right for itself.
String s = getOption("extra-used-tags");
if (s != null)
set.addAll(Arrays.asList(COMMA_OR_SPACE_PATTERN.split(s)));
// These tags are passed on the command line and so must be added
if (nameTagList != null)
set.addAll(nameTagList);
// There are a lot of tags that are used within mkgmap that
InputStream is = getClass().getResourceAsStream("/styles/builtin-tag-list");
try {
if (is != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//System.out.println("Got built in list");
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.startsWith("#"))
continue;
//System.out.println("adding " + line);
set.add(line);
}
}
} catch (IOException e) {
// the file doesn't exist, this is ok but unlikely
System.err.println("warning: built in tag list not found");
} finally {
Utils.closeFile(is);
}
return set;
}
private void readRules() {
String l = generalOptions.get("levels");
if (l == null)
l = LevelInfo.DEFAULT_LEVELS;
LevelInfo[] levels = LevelInfo.createFromString(l);
try {
RuleFileReader reader = new RuleFileReader(FeatureKind.RELATION, levels, relations);
reader.load(fileLoader, "relations", performChecks, getOverlaidTypeMap());
} catch (FileNotFoundException e) {
// it is ok for this file to not exist.
log.debug("no relations file");
}
try {
RuleFileReader reader = new RuleFileReader(FeatureKind.POINT, levels, nodes);
reader.load(fileLoader, "points", performChecks, getOverlaidTypeMap());
} catch (FileNotFoundException e) {
// it is ok for this file to not exist.
log.debug("no points file");
}
try {
RuleFileReader reader = new RuleFileReader(FeatureKind.POLYLINE, levels, lines);
reader.load(fileLoader, "lines", performChecks, getOverlaidTypeMap());
} catch (FileNotFoundException e) {
log.debug("no lines file");
}
try {
RuleFileReader reader = new RuleFileReader(FeatureKind.POLYGON, levels, polygons);
reader.load(fileLoader, "polygons", performChecks, getOverlaidTypeMap());
} catch (FileNotFoundException e) {
log.debug("no polygons file");
}
}
/**
* Read the map-features file. This is the old format of the mapping
* between osm and garmin types.
*/
private void readMapFeatures() {
try {
Reader r = fileLoader.open(FILE_FEATURES);
MapFeatureReader mfr = new MapFeatureReader();
String l = generalOptions.get("levels");
if (l == null)
l = LevelInfo.DEFAULT_LEVELS;
mfr.setLevels(LevelInfo.createFromString(l));
mfr.readFeatures(new BufferedReader(r));
initFromMapFeatures(mfr);
} catch (FileNotFoundException e) {
// optional file
log.debug("no map-features file");
} catch (IOException e) {
log.error("could not read map features file");
}
}
/**
* Take the output of the map-features file and create rules for
* each line and add to this style. All rules in map-features are
* unconditional, in other words the osm 'amenity=cinema' always
* maps to the same garmin type.
*
* @param mfr The map feature file reader.
*/
private void initFromMapFeatures(MapFeatureReader mfr) {
addBackwardCompatibleRules();
for (Entry<String, GType> me : mfr.getLineFeatures().entrySet())
lines.add(me.getKey(), createRule(me.getKey(), me.getValue()), Collections.<String>emptySet());
for (Entry<String, GType> me : mfr.getShapeFeatures().entrySet())
polygons.add(me.getKey(), createRule(me.getKey(), me.getValue()), Collections.<String>emptySet());
for (Entry<String, GType> me : mfr.getPointFeatures().entrySet())
nodes.add(me.getKey(), createRule(me.getKey(), me.getValue()), Collections.<String>emptySet());
}
/**
* For backward compatibility, when we find a map-features file we add
* rules for actions that were previously hard coded in the conversion.
* These are added even if there was also a lines, points, etc file.
*/
private void addBackwardCompatibleRules() {
// Name rule for highways
List<Action> l = new ArrayList<Action>();
NameAction action = new NameAction();
action.add("${name} (${ref})");
action.add("${ref}");
action.add("${name}");
l.add(action);
Op expr = new ExistsOp();
expr.setFirst(new ValueOp("highway"));
Rule rule = new ActionRule(expr, l);
lines.add("highway=*", rule, Collections.<String>emptySet());
// Name rule for contour lines
l = new ArrayList<Action>();
action = new NameAction();
action.add("${ele|conv:m=>ft}");
l.add(action);
EqualsOp expr2 = new EqualsOp();
expr2.setFirst(new ValueOp("contour"));
expr2.setSecond(new ValueOp("elevation"));
rule = new ActionRule(expr2, l);
lines.add("contour=elevation", rule, Collections.<String>emptySet()); // "contour=elevation"
expr2 = new EqualsOp();
expr2.setFirst(new ValueOp("contour_ext"));
expr2.setSecond(new ValueOp("elevation"));
rule = new ActionRule(expr2, l);
lines.add("contour_ext=elevation", rule, Collections.<String>emptySet()); // "contour_ext=elevation"
}
/**
* Create a rule from a raw gtype. You get raw gtypes when you
* have read the types from a map-features file.
*
* @return A rule that is conditional on the key string given.
*/
private Rule createRule(String key, GType gt) {
if (gt.getDefaultName() != null)
log.debug("set default name of", gt.getDefaultName(), "for", key);
String[] tagval = EQUAL_PATTERN.split(key);
EqualsOp op = new EqualsOp();
op.setFirst(new ValueOp(tagval[0]));
op.setSecond(new ValueOp(tagval[1]));
return new ExpressionRule(op, gt);
}
/**
* If there is an options file, then read it and keep options that
* we are interested in.
*
* Only specific options can be set.
*/
private void readOptions() {
try {
Reader r = fileLoader.open(FILE_OPTIONS);
Options opts = new Options(new OptionProcessor() {
public void processOption(Option opt) {
String key = opt.getOption();
String val = opt.getValue();
if (key.equals("name-tag-list")) {
if ("name".equals(val) == false){
System.err.println("Warning: option name-tag-list used in the style options is ignored. "
+ "Please use only the command line option to specify this value." );
}
} else if (OPTION_LIST.contains(key)) {
// Simple options that have string value. Perhaps we should allow
// anything here?
generalOptions.put(key, val);
}
}
});
opts.readOptionFile(r, FILE_OPTIONS);
} catch (FileNotFoundException e) {
// the file is optional, so ignore if not present, or causes error
log.debug("no options file");
}
}
/**
* Read the info file. This is just information about the style.
*/
private void readInfo() {
try {
Reader br = new BufferedReader(fileLoader.open(FILE_INFO));
info = new StyleInfo();
Options opts = new Options(new OptionProcessor() {
public void processOption(Option opt) {
String word = opt.getOption();
String value = opt.getValue();
if (word.equals("summary"))
info.setSummary(value);
else if (word.equals("version")) {
info.setVersion(value);
} else if (word.equals("base-style")) {
info.addBaseStyleName(value);
} else if (word.equals("description")) {
info.setLongDescription(value);
}
}
});
opts.readOptionFile(br, FILE_INFO);
} catch (FileNotFoundException e) {
// optional file..
log.debug("no info file");
}
}
private void readOverlays() {
try {
Reader r = fileLoader.open(FILE_OVERLAYS);
overlays = new OverlayReader(r, FILE_OVERLAYS);
overlays.readOverlays();
} catch (FileNotFoundException e) {
// this is perfectly normal
log.debug("no overlay file");
}
}
/**
* If this style is based upon another one then read it in now. The rules
* for merging styles are that it is as-if the style was read just after
* the current styles 'info' section and any option or rule specified
* in the current style will override any corresponding item in the
* base style.
* @param name The name of the base style
* @param props program properties
*/
private void readBaseStyle(String name, EnhancedProperties props) {
if (name == null)
return;
try {
baseStyles.add(new StyleImpl(location, name, props, performChecks));
} catch (SyntaxException e) {
System.err.println("Error in style: " + e.getMessage());
} catch (FileNotFoundException e) {
// not found, try on the classpath. This is the common
// case where you have an external style, but want to
// base it on a built in one.
log.debug("could not open base style file", e);
try {
baseStyles.add(new StyleImpl(null, name, props, performChecks));
} catch (SyntaxException se) {
System.err.println("Error in style: " + se.getMessage());
} catch (FileNotFoundException e1) {
log.error("Could not find base style", e);
}
}
}
/**
* Merge another style's options into this one. The style will have a lower
* priority, in other words any option set in 'other' and this style will
* take the value given in this style.
*
* This is used to base styles on other ones, without having to repeat
* everything.
*
* @see #mergeRules(StyleImpl)
*/
private void mergeOptions(StyleImpl other) {
for (Entry<String, String> ent : other.generalOptions.entrySet()) {
String opt = ent.getKey();
String val = ent.getValue();
if (OPTION_LIST.contains(opt)) {
// Simple options that have string value. Perhaps we should allow
// anything here?
generalOptions.put(opt, val);
}
}
}
/**
* Merge rules from the base style. This has to called after this
* style's rules are read.
*
* The other rules have a lower priority than the rules in this file; it is as if they
* were appended to the rule files of this style.
*
* @see #mergeOptions(StyleImpl)
*/
private void mergeRules(StyleImpl other) {
lines.merge(other.lines);
polygons.merge(other.polygons);
nodes.merge(other.nodes);
relations.merge(other.relations);
}
private void checkVersion() throws FileNotFoundException {
Reader r = fileLoader.open(FILE_VERSION);
TokenScanner scan = new TokenScanner(FILE_VERSION, r);
int version;
try {
version = scan.nextInt();
log.debug("Got version", version);
} catch (NumberFormatException e) {
// default to 0 if no version can be found.
version = 0;
}
if (version > VERSION) {
System.err.println("Warning: unrecognised style version " + version +
", but only versions up to " + VERSION + " are understood");
}
}
/**
* Writes out this file to the given writer in the single file format.
* This produces a valid style file, although it is mostly used
* for testing.
*/
void dumpToFile(Writer out) {
StylePrinter stylePrinter = new StylePrinter(this);
stylePrinter.setGeneralOptions(generalOptions);
stylePrinter.setRelations(relations);
stylePrinter.setLines(lines);
stylePrinter.setNodes(nodes);
stylePrinter.setPolygons(polygons);
stylePrinter.dumpToFile(out);
}
/**
*
* @return null or the map that was read from the overlays file
*/
private Map<Integer, List<Integer>> getOverlaidTypeMap() {
if (overlays != null)
return overlays.getOverlays();
return Collections.emptyMap();
}
/**
* Evaluate the style options and try to read the style.
*
* The option --style-file give the location of an alternate file or
* directory containing styles rather than the default built in ones.
*
* The option --style gives the name of a style, either one of the
* built in ones or selects one from the given style-file.
*
* If there is no name given, but there is a file then the file should
* just contain one style.
*
* @param props the program properties
* @return A style instance or null in case of error.
*/
public static Style readStyle(EnhancedProperties props) {
String loc = props.getProperty("style-file");
if (loc == null)
loc = props.getProperty("map-features");
String name = props.getProperty("style");
if (loc == null && name == null)
name = "default";
if (name == null){
StyleFileLoader loader;
try {
loader = StyleFileLoader.createStyleLoader(loc, null);
int numEntries = loader.list().length;
if (numEntries > 1)
throw new ExitException("Style file " + loc + " contains multiple styles, use option --style to select one.");
} catch (FileNotFoundException e) {
throw new ExitException("Could not open style file " + loc);
}
}
Style style = null;
try {
style = new StyleImpl(loc, name, props, WITHOUT_CHECKS);
} catch (SyntaxException e) {
System.err.println("Error in style: " + e.getMessage());
throw new ExitException("Could not open style " + name);
} catch (FileNotFoundException e) {
String msg = "Could not open style ";
if (name != null){
msg += name;
if (loc != null)
msg += " in " + loc;
}
else
msg += loc + " . Make sure that it points to a style or add the --style option.";
throw new ExitException(msg);
}
return style;
}
public static void main(String[] args) throws FileNotFoundException {
String file = args[0];
String name = null;
if (args.length > 1)
name = args[1];
StyleImpl style = new StyleImpl(file, name, new EnhancedProperties(), WITH_CHECKS);
style.dumpToFile(new OutputStreamWriter(System.out));
}
}