Subversion Repositories mkgmap

Rev

Rev 3972 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2017.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * 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.
 */

package uk.me.parabola.mkgmap.osmstyle;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import uk.me.parabola.imgfmt.app.mdr.Mdr7;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.scan.TokType;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;
import uk.me.parabola.util.EnhancedProperties;

/**
 * Code to add special Garmin separators 0x1b, 0x1e and 0x1f.
 * The separator 0x1e tells Garmin that the part of the name before that separator
 * should not be displayed when zooming out enough. It is displayed like a blank.
 * The separator 0x1f tells Garmin that the part of the name after that separator
 * should not be displayed when zooming out enough. It is displayed like a blank.
 * The separator 0x1b works like 0x1e, but is not displayed at all.
 * The separator 0x1c works like 0x1f, but is not displayed at all.
 * See also class {@link Mdr7}.
 *
 * @author Gerd Petermann
 *
 */

public class PrefixSuffixFilter {
        private static final Logger log = Logger.getLogger(PrefixSuffixFilter.class);

        private static final int MODE_PREFIX = 0;
        private static final int MODE_SUFFIX = 1;
       
        private boolean enabled;
        private final Set<String> languages = new LinkedHashSet<>();
        private final Map<String, List<String>> langPrefixMap = new HashMap<>();
        private final Map<String, List<String>> langSuffixMap = new HashMap<>();
        private final Map<String, List<String>> countryLanguageMap = new HashMap<>();
        private final Map<String, List<String>> countryPrefixMap = new HashMap<>();
        private final Map<String, List<String>> countrySuffixMap = new HashMap<>();

        private EnhancedProperties options = new EnhancedProperties();

        public PrefixSuffixFilter(EnhancedProperties props) {
                String cfgFile = props.getProperty("road-name-config",null);
                enabled = readConfig(cfgFile);
        }

        /**
         * Read the configuration file for this filter.
         * @param cfgFile path to file
         * @return true if filter can be used, else false.
         */

        private boolean readConfig(String cfgFile) {
                if (cfgFile == null)
                        return false;
                try (InputStreamReader reader = new InputStreamReader(new FileInputStream(cfgFile), "utf-8")) {
                        readOptionFile(reader, cfgFile);
                        return true;
                } catch (Exception e) {
                        log.error(e.getMessage());
                        log.error(this.getClass().getSimpleName() + " disabled, failed to read config file " + cfgFile);
                        return false;
                }
        }
       
        /**
         *
         * @param r
         * @param filename
         */

        private void readOptionFile(Reader r, String filename) {
                BufferedReader br = new BufferedReader(r);
                TokenScanner ts = new TokenScanner(filename, br);
                ts.setExtraWordChars(":");

                while (!ts.isEndOfFile()) {
                        Token tok = ts.nextToken();
                        if (tok.isValue("#")) {
                                ts.skipLine();
                                continue;
                        }

                        String key = tok.getValue();

                        ts.skipSpace();
                        tok = ts.peekToken();
                       
                        if (tok.getType() == TokType.SYMBOL) {

                                String punc = ts.nextValue();
                                String val;
                                if (punc.equals(":") || punc.equals("=")) {
                                        val = ts.readLine();
                                } else {
                                        ts.skipLine();
                                        continue;
                                }
                                processOption(key, val);
                        } else if (key != null){
                                throw new IllegalArgumentException("don't understand line with " + key );
                        } else {
                                ts.skipLine();
                        }
                }
                /**
                 * process lines starting with prefix1 or prefix2.
                 */

                for (String lang : languages) {
                        String prefix1 = options.getProperty("prefix1:" + lang, null);
                        if (prefix1 == null)
                                continue;
                        String prefix2 = options.getProperty("prefix2:" + lang, null);
                        List<String> p1 = prefix1 != null ? Arrays.asList(prefix1.split(",")) : Collections.emptyList();
                        List<String> p2 = prefix2 != null ? Arrays.asList(prefix2.split(",")) : Collections.emptyList();
                        langPrefixMap.put(lang, genPrefix(p1, p2));
                }
        }

        private void processOption(String key, String val) {
                String[] keysParts = key.split(":");
                String[] valParts = val.split(",");
                if (keysParts.length < 2 || val.isEmpty() || valParts.length < 1) {
                        throw new IllegalArgumentException("don't understand " + key + " = " + val);
                }
                switch (keysParts[0].trim()) {
                case "prefix1":
                case "prefix2":
                        options.put(key, val); // store for later processing
                        break;
                case "suffix":
                        List<String> suffixes = new ArrayList<>();
                        for (String s : valParts) {
                                suffixes.add(stripBlanksAndQuotes(s));
                        }
                        sortByLength(suffixes);
                        langSuffixMap.put(keysParts[1].trim(), suffixes);
                        break;
                case "lang":
                        String iso = keysParts[1].trim();
                        List<String> langs = new ArrayList<>();
                        for (String lang : valParts) {
                                langs.add(lang.trim());
                        }
                        countryLanguageMap .put(iso, langs);
                        languages.addAll(langs);
                default:
                        break;
                }
        }
       

        /** Create all combinations of items in prefix1 with items in prefix2 and finally prefix1 with an extra blank.  
         * @param prefix1 list of prefix words
         * @param prefix2 list of prepositions
         * @return all combinations
         */

        private List<String> genPrefix (List<String> prefix1, List<String> prefix2) {
                List<String> prefixes = new ArrayList<>();
                for (String p1 : prefix1) {
                        p1 = stripBlanksAndQuotes(p1);
                        for (String p2 : prefix2) {
                                p2 = stripBlanksAndQuotes(p2);
                                prefixes.add(p1 + " " + p2);
                        }
                        prefixes.add(p1 + " ");
                }
                return prefixes;
        }

        /**
         * First remove leading and trailing blanks, next check for paired quotes
         * @param s the string
         * @return the modified string
         */

        private String stripBlanksAndQuotes(String s) {
                s = s.trim();
                if (s.startsWith("'") && s.endsWith("'") || s.startsWith("\"") && s.endsWith("\"")) {
                        return s.substring(1, s.length()-1);
                }
                return s;
        }
       
       
        /**
         * Modify all labels of a road. Each label is checked against country specific lists of
         * well known prefixes (e.g. "Rue de la ", "Avenue des "  ) and suffixes (e.g. " Road").
         * If a well known prefix is found the label is modified. If the prefix ends with a blank,
         * that blank is replaced by 0x1e, else 0x1b is added after the prefix.
         * If a well known suffix is found the label is modified. If the suffix starts with a blank,
         * that blank is replaced by 0x1f, else 0x1c is added before the suffix.
         * @param road
         */

        public void filter(MapRoad road) {
                if (!enabled)
                        return;
                String country = road.getCountry();
                if (country == null)
                        return;
               
                List<String> prefixesCountry = getSearchStrings(country, MODE_PREFIX);
                List<String> suffixesCountry = getSearchStrings(country, MODE_SUFFIX);
               
                // perform brute force search, seems to be fast enough
                String[] labels = road.getLabels();
                for (int i = 0; i < labels.length; i++) {
                        String label = labels[i];
                        if (label == null || label.length() == 0)
                                continue;
                        boolean modified = false;
                        for (String prefix : prefixesCountry) {
                                if (label.charAt(0) < 7)
                                        break; // label starts with shield code
                                if (label.length() < prefix.length())
                                        continue;
                                if (prefix.equalsIgnoreCase(label.substring(0, prefix.length()))) {
                                        if (prefix.endsWith(" ")) {
                                                label = prefix.substring(0, prefix.length() - 1) + (char) 0x1e
                                                                + label.substring(prefix.length());
                                        } else {
                                                label = prefix + (char) 0x1b + label.substring(prefix.length());
                                        }
                                        modified = true;
                                        break;
                                }
                        }
                        for (String suffix : suffixesCountry) {
                                int len = label.length();
                                if (len < suffix.length())
                                        continue;
                                int pos = len - suffix.length();
                                if (suffix.equalsIgnoreCase(label.substring(pos, len))) {
                                        if (suffix.startsWith(" "))
                                                label = label.substring(0, pos) + (char) 0x1f + suffix.substring(1);
                                        else
                                                label = label.substring(0, pos) + (char) 0x1c + suffix;
                                        modified = true;
                                        break;
                                }
                        }
                        if (modified) {
                                labels[i] = label;
                                log.debug("modified",label,country,road.getRoadDef());
                        }
                }
        }
       
        /**
         * Build list of prefixes or suffixes for a given country.
         * @param country String with 3 letter ISO code
         * @param mode : signals prefix or suffix
         * @return List with prefixes or suffixes
         */

        private List<String> getSearchStrings(String country, int mode) {
                Map<String, List<String>>  cache = (mode == MODE_PREFIX) ? countryPrefixMap : countrySuffixMap;
                List<String> res = cache.get(country);
                if (res == null) {
                        // compile the list
                        List<String> languages = countryLanguageMap.get(country);
                        if (languages == null)
                                res = Collections.emptyList();
                        else  {
                                List<List<String>> all = new ArrayList<>();
                                for (String lang : languages) {
                                        List<String> prefixes = mode == MODE_PREFIX ? langPrefixMap.get(lang) : langSuffixMap.get(lang);
                                        if(prefixes != null)
                                                all.add(prefixes);
                                }
                                if(all.isEmpty())
                                        res = Collections.emptyList();
                                else if (all.size() == 1) {
                                        res = all.get(0);
                                }
                                else {
                                        Set<String> allPrefixesSet = new HashSet<>();
                                        for (List<String> prefOneLang : all)
                                                allPrefixesSet.addAll(prefOneLang);
                                        res = new ArrayList<>(allPrefixesSet);
                                        sortByLength(res);
                                       
                                }
                        }
                        // cache the result
                        cache.put(country, res);
                }
                return res;
        }

        /**
         * Sort by string length so that longest string comes first.
         * @param strings
         */

        private void sortByLength(List<String> strings) {
                strings.sort(new Comparator<String>() {
                        @Override
                        public int compare(String o1, String o2) {
                                return Integer.compare(o2.length(), o1.length());
                        }
                });
        }
}