Subversion Repositories mkgmap

Rev

Rev 4537 | 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

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), StandardCharsets.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) {

                                switch (ts.nextValue()) {
                                case ":":
                                case "=":
                                        processOption(key, ts.readLine());
                                        break;
                                default:
                                        ts.skipLine();
                                }

                        } 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 = Arrays.asList(prefix1.split(","));
                        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);
                        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 static 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 + " ");
                }
                sortByLength(prefixes);
                return prefixes;
        }

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

        private static 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;
               
                final List<String> prefixesCountry = getSearchStrings(country, MODE_PREFIX);
                final List<String> suffixesCountry = getSearchStrings(country, MODE_SUFFIX);
               
                String[] labels = road.getLabels();
                for (int i = 0; i < labels.length; i++) {
                        String label = labels[i];
                        if (label == null || label.isEmpty())
                                continue;
                        label = applyPrefixes(label, prefixesCountry);
                        label = applySuffixes(label, suffixesCountry);
                        if (!label.equals(labels[i])) {
                                labels[i] = label;
                                log.debug("modified", label, country, road.getRoadDef());
                        }
                }
        }
       
        static String applyPrefixes(String label, List<String> prefixesCountry) {
                if (label.charAt(0) < 7)
                        return label; // label starts with shield code
                // perform brute force search, seems to be fast enough
                for (String prefix : prefixesCountry) {
                        if (label.length() >= prefix.length() && prefix.equalsIgnoreCase(label.substring(0, prefix.length()))) {
                                if (prefix.endsWith(" ")) {
                                        return prefix.substring(0, prefix.length() - 1) + (char) 0x1e + label.substring(prefix.length());
                                }
                                return prefix + (char) 0x1b + label.substring(prefix.length());
                        }
                }
                return label;
        }

        private static String applySuffixes(String label, List<String> suffixesCountry) {
                // perform brute force search, seems to be fast enough
                for (String suffix : suffixesCountry) {
                        int len = label.length();
                        int pos = len - suffix.length();
                        if (pos >= 0 && suffix.equalsIgnoreCase(label.substring(pos, len))) {
                                if (suffix.startsWith(" ")) {
                                        return label.substring(0, pos) + (char) 0x1f + suffix.substring(1);
                                }
                                return label.substring(0, pos) + (char) 0x1c + suffix;
                        }
                }
                return label;
        }

        /**
         * 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;
                return cache.computeIfAbsent(country, k-> {
                        // compile the list
                        List<String> languageList = countryLanguageMap.get(country);
                        if (languageList == null)
                                return Collections.emptyList();
                        final Map<String, List<String>> map = mode == MODE_PREFIX ? langPrefixMap : langSuffixMap;
                        List<String> res = languageList.stream()
                                        .map(lang -> map.getOrDefault(lang, Collections.emptyList()))
                                        .flatMap(List::stream)
                                        .distinct()
                                        .collect(Collectors.toList());
                        if (res.isEmpty())
                                return Collections.emptyList();
                        sortByLength(res);
                        return res;
                });
        }

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

        private static void sortByLength(List<String> strings) {
                strings.sort((o1, o2) -> Integer.compare(o2.length(), o1.length()));
        }
}