Subversion Repositories mkgmap

Rev

Rev 4653 | 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.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));
        }
}