/*
* 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.nio.charset.StandardCharsets;
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 uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.Options;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LineAdder;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
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<>(
Arrays.
asList("levels",
"overview-levels",
"extra-used-tags",
"line-types-with-direction"));
// File names
private static final String FILE_VERSION =
"version";
private static final String FILE_INFO =
"info";
private static final String FILE_OPTIONS =
"options";
private static final String FILE_OVERLAYS =
"overlays";
// 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<>();
// Options from the option file that are used outside this file.
private final Map<String,
String> generalOptions =
new HashMap<>();
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
;
private Collection<String> deadEndTags =
new ArrayList<>();
/**
* 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 loc,
String name
) throws FileNotFoundException {
this(loc, 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 {
String s = props.
getProperty("dead-ends");
if (s
!=
null) {
String[] deadEndTagsAndValues = s.
split(",");
for (String deadEndTag : deadEndTagsAndValues
) {
deadEndTags.
add(deadEndTag.
split("=",
2)[0]);
}
}
location = loc
;
fileLoader = StyleFileLoader.
createStyleLoader(loc, name
);
this.
performChecks = performChecks
;
// 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
(props.
getProperty("levels"), props.
containsKey("route"));
ListIterator<StyleImpl
> listIterator = baseStyles.
listIterator(baseStyles.
size());
while (listIterator.
hasPrevious())
mergeRules
(listIterator.
previous());
}
@
Override
public String getOption
(String name
) {
return generalOptions.
get(name
);
}
@
Override
public StyleInfo getInfo
() {
return info
;
}
@
Override
public Rule getNodeRules
() {
nodes.
prepare();
return nodes
;
}
@
Override
public Rule getWayRules
() {
RuleSet r =
new RuleSet
();
r.
addAll(lines
);
r.
addAll(polygons
);
r.
prepare();
return r
;
}
@
Override
public Rule getLineRules
() {
lines.
prepare();
return lines
;
}
@
Override
public Rule getPolygonRules
() {
polygons.
prepare();
return polygons
;
}
@
Override
public Rule getRelationRules
() {
relations.
prepare();
return relations
;
}
@
Override
public LineAdder getOverlays
(final LineAdder lineAdder
) {
LineAdder adder =
null;
if (overlays
!=
null) {
adder = element -
> overlays.
addLine(element, lineAdder
);
}
return adder
;
}
@
Override
public Set<String> getUsedTagsPOI
() {
return new HashSet<>(nodes.
getUsedTags());
}
@
Override
public Set<String> getUsedTags
() {
Set<String> set =
new HashSet<>();
set.
addAll(relations.
getUsedTags());
set.
addAll(lines.
getUsedTags());
set.
addAll(polygons.
getUsedTags());
set.
addAll(nodes.
getUsedTags());
set.
addAll(deadEndTags
);
// 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.
set.
addAll(CommandArgs.
stringToList(getOption
("extra-used-tags"),
"extra-used-tags"));
// There are a lot of tags that are used within mkgmap that
try (InputStream is =
this.
getClass().
getResourceAsStream("/styles/builtin-tag-list");) {
if (is
!=
null) {
BufferedReader br =
new BufferedReader(new InputStreamReader(is, StandardCharsets.
UTF_8));
String line
;
while ((line = br.
readLine()) !=
null) {
line = line.
trim();
if (line.
startsWith("#"))
continue;
set.
add(line
);
}
}
} catch (IOException e
) {
// the file doesn't exist, this is ok but unlikely
Logger.
defaultLogger.
warn("built in tag list not found");
}
return set
;
}
private void readRules
(String levelsFromProps,
boolean routable
) {
String l = generalOptions.
get("levels");
if (l ==
null)
l = LevelInfo.
DEFAULT_LEVELS;
LevelInfo
[] levels = LevelInfo.
createFromString(l
);
if (performChecks
&& levels
[0].
getBits() <=
10) {
Logger.
defaultLogger.
warn("Resolution values <= 10 may confuse MapSource: " + l
);
}
l = generalOptions.
get("overview-levels");
if (l
!=
null){
LevelInfo
[] ovLevels = LevelInfo.
createFromString(l
);
// TODO: make sure that the combination of the two level strings makes sense
if (performChecks
){
if (ovLevels
[0].
getBits() <=
10){
Logger.
defaultLogger.
warn("Resolution values <= 10 may confuse MapSource: " + l
);
}
if (levels
[0].
getLevel() >= ovLevels
[ovLevels.
length-
1].
getLevel()){
Logger.
defaultLogger.
warn("Overview level not higher than highest normal level. " + l
);
}
}
List<LevelInfo
> tmp =
new ArrayList<>();
tmp.
addAll(Arrays.
asList(levels
));
tmp.
addAll(Arrays.
asList(ovLevels
));
levels = tmp.
toArray(new LevelInfo
[tmp.
size()]);
Arrays.
sort(levels
);
}
try {
RuleFileReader reader =
new RuleFileReader
(FeatureKind.
RELATION, levels, relations, performChecks, getOverlaidTypeMap
());
reader.
load(fileLoader,
"relations");
} 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, performChecks, getOverlaidTypeMap
());
reader.
load(fileLoader,
"points");
} 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, performChecks, getOverlaidTypeMap
());
reader.
load(fileLoader,
"lines");
if (routable
&& levelsFromProps
!=
null && !levelsFromProps.
equals(l
)) {
LevelInfo
[] pl = LevelInfo.
createFromString(levelsFromProps
);
if (levels
[levels.
length -
1].
getBits() > pl
[pl.
length -
1].
getBits()) {
RuleFileReader checker =
new RuleFileReader
(FeatureKind.
POLYLINE, pl,
new RuleSet
(),
false,
routable, getOverlaidTypeMap
());
checker.
load(fileLoader,
"lines");
}
}
} catch (FileNotFoundException e
) {
log.
debug("no lines file");
}
try {
RuleFileReader reader =
new RuleFileReader
(FeatureKind.
POLYGON, levels, polygons, performChecks, getOverlaidTypeMap
());
reader.
load(fileLoader,
"polygons");
} catch (FileNotFoundException e
) {
log.
debug("no polygons file");
}
}
/**
* 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
(opt -
> {
String key = opt.
getOption();
String val = opt.
getValue();
if ("name-tag-list".
equals(key
)) {
if (!"name".
equals(val
)) {
Logger.
defaultLogger.
warn("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
(opt -
> {
String word = opt.
getOption();
String value = opt.
getValue();
if ("summary".
equals(word
))
info.
setSummary(value
);
else if ("version".
equals(word
)) {
info.
setVersion(value
);
} else if ("base-style".
equals(word
)) {
info.
addBaseStyleName(value
);
} else if ("description".
equals(word
)) {
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
) {
Logger.
defaultLogger.
error("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
) {
Logger.
defaultLogger.
error("Error in style: " + se.
getMessage());
} catch (FileNotFoundException e1
) {
Logger.
defaultLogger.
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
) {
Logger.
defaultLogger.
warn("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){
try (StyleFileLoader 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
;
try {
style =
new StyleImpl
(loc, name, props, WITHOUT_CHECKS
);
} catch (SyntaxException e
) {
Logger.
defaultLogger.
error("Error in style: " + e.
getMessage());
throw new ExitException
("Could not open style " +
(name ==
null? "":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
;
}
@
Override
public void reportStats
() {
relations.
printStats("relations");
nodes.
printStats("points");
lines.
printStats("lines");
polygons.
printStats("polygons");
}
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));
}
}