Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2006, 2011.
 *
 * 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.build;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.osmstyle.NameFinder;
import uk.me.parabola.mkgmap.reader.osm.TagDict;
import uk.me.parabola.mkgmap.reader.osm.Tags;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.KdTree;
import uk.me.parabola.util.MultiHashMap;

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

    /** hash map to collect equally named MapPoints*/
        private final MultiHashMap<String, MapPoint> cityMap = new MultiHashMap<>();
       
        private final KdTree<MapPoint> cityFinder = new KdTree<>();
        private final List<MapPoint> placesMap  =  new ArrayList<>();

        private final NameFinder nameFinder;

        private final LocatorConfig locConfig = LocatorConfig.get();

        private final Set<String> locationAutofill;
       
        private static final double MAX_CITY_DIST = 30000;

        public Locator() {
                this(new EnhancedProperties());
        }
       
        public Locator(EnhancedProperties props) {
                this.nameFinder = new NameFinder(props);
                this.locationAutofill = new HashSet<>(LocatorUtil.parseAutofillOption(props));
        }
       
        public void addCityOrPlace(MapPoint p)
        {
                if (!p.isCity()) {
                        log.warn("MapPoint has no city type id: 0x" + Integer.toHexString(p.getType()));
                        return;
                }
               
                if (log.isDebugEnabled())
                        log.debug("S City 0x"+Integer.toHexString(p.getType()), p.getName(), "|", p.getCity(), "|", p.getRegion(), "|", p.getCountry());

                // correct the country name
                // usually this is the translation from 3letter ISO code to country name
                if(p.getCountry() != null)
                        p.setCountry(normalizeCountry(p.getCountry()));

                resolveIsInInfo(p); // Pre-process the is_in field

                if(p.getCity() != null)
                {
                        if (log.isDebugEnabled())
                                log.debug(p.getCity(),p.getRegion(),p.getCountry());
                        // Must use p.getName() here because p.getCity() contains the city name of the preprocessed cities
                        addCity(p.getName(), p);
                }
                else
                {
                        // All other places which do not seam to be a real city has to resolved later
                        placesMap.add(p);              
                }
               
                if (log.isDebugEnabled())
                        log.debug("E City 0x"+Integer.toHexString(p.getType()), p.getName(), "|", p.getCity(), "|", p.getRegion(), "|", p.getCountry());
        }

        public void setDefaultCountry(String country, String abbr)
        {
                locConfig.setDefaultCountry(country, abbr);
        }
       
        public String normalizeCountry(String country)
        {
                if (country == null) {
                        return null;
                }
                String iso = locConfig.getCountryISOCode(country);
                if (iso != null) {
                        String normedCountryName = locConfig.getCountryName(iso, nameFinder);
                        if (normedCountryName != null) {
                                log.debug("Country:",country,"ISO:",iso,"Norm:",normedCountryName);
                                return normedCountryName;
                        }
                }
               
                // cannot find the country in our config => return the country itself
                log.debug("Country:",country,"ISO:",iso,"Norm:",country);
                return country;
        }

        /**
         * Checks if the country given by attached tags is already known, adds or completes
         * the Locator information about this country and return the three letter ISO code
         * (in case the country is known in the LocatorConfig.xml) or the country name.
         *
         * @param tags the countries tags
         * @return the three letter ISO code or <code>null</code> if ISO code is unknown
         */

        public String addCountry(Tags tags) {
                synchronized (locConfig) {
                        String iso = getCountryISOCode(tags);
                        if (iso == null) {
                                log.warn("Cannot find iso code for country with tags", tags);
                        } else {
                                locConfig.addCountryWithTags(iso, tags);
                        }
                        return iso;
                }
        }
       
        public static final ShortArrayList PREFERRED_NAME_TAG_KEYS = TagDict.compileTags("name","name:en","int_name");
       
        public String getCountryISOCode(Tags tags) {
                for (short nameTagKey : PREFERRED_NAME_TAG_KEYS) {
                        String isoCode = getCountryISOCode(tags.get(nameTagKey));
                        if (isoCode != null) {
                                return isoCode;
                        }
                }

                for (String countryStr : tags.getTagsWithPrefix("name:", false).values()) {
                        String isoCode = getCountryISOCode(countryStr);
                        if (isoCode != null) {
                                return isoCode;
                        }
                }
                return null;
        }
       
        public String getCountryISOCode(String country)
        {
                return locConfig.getCountryISOCode(country);
        }

        public int getPOIDispFlag(String country)
        {
                return locConfig.getPoiDispFlag(getCountryISOCode(country));
        }

        private boolean isContinent(String continent)
        {
                return locConfig.isContinent(continent);
        }


        /**
         * resolveIsInInfo tries to get country and region info out of the is_in field
         * @param p     Point to process
         */

        private void resolveIsInInfo(MapPoint p)
        {
                if (!locationAutofill.contains("is_in")) {
                        return;
                }
               
                if(p.getCountry() != null && p.getRegion() != null && p.getCity() == null)
                {              
                        p.setCity(p.getName());
                        return;
                }

                if(p.getIsIn() != null)
                {      
                        String[] cityList = p.getIsIn().split(",");
                       
                        // is_in content is not well defined so we try our best to get some info out of it
                        // Format 1 popular in Germany: "County,State,Country,Continent"

                        if(cityList.length > 1 &&
                                isContinent(cityList[cityList.length-1]))       // Is last a continent ?
                        {
                                if (p.getCountry() == null) {
                                        // The one before continent should be the country
                                        p.setCountry(normalizeCountry(cityList[cityList.length-2].trim()));
                                }
                               
                                // aks the config which info to use for region info                            
                                int offset = locConfig.getRegionOffset(getCountryISOCode(p.getCountry())) + 1;

                                if(cityList.length > offset && p.getRegion() == null)
                                        p.setRegion(cityList[cityList.length-(offset+1)].trim());

                        } else

                        // Format 2 other way round: "Continent,Country,State,County"  

                        if(cityList.length > 1 && isContinent(cityList[0]))     // Is first a continent ?
                        {
                                if (p.getCountry() == null) {
                                        // The one before continent should be the country
                                        p.setCountry(normalizeCountry(cityList[1].trim()));
                                }
                               
                                int offset = locConfig.getRegionOffset(getCountryISOCode(p.getCountry())) + 1;

                                if(cityList.length > offset && p.getRegion() == null)
                                        p.setRegion(cityList[offset].trim());
                        } else

                        // Format like this "County,State,Country"

                        if(p.getCountry() == null && cityList.length > 0)
                        {
                                // I don't like to check for a list of countries but I don't want other stuff in country field
                                String isoCode = locConfig.getCountryISOCode(cityList[cityList.length-1]);
                                if (isoCode != null)
                                {      
                                        p.setCountry(normalizeCountry(isoCode));

                                        int offset = locConfig.getRegionOffset(isoCode) + 1;

                                        if(cityList.length > offset && p.getRegion() == null)
                                                p.setRegion(cityList[cityList.length-(offset+1)].trim());      
                                }
                        }
                }

                if(p.getCountry() != null && p.getRegion() != null && p.getCity() == null)
                {      
                        p.setCity(p.getName());
                }
        }
       
        public MapPoint findNextPoint(MapPoint p)
        {
                return cityFinder.findNextPoint(p);
        }
       
        public MapPoint findNearbyCityByName(MapPoint p) {

                if (p.getCity() == null)
                        return null;

                Collection<MapPoint> nextCityList = cityMap.get(p.getCity());
                if (nextCityList.isEmpty()) {
                        return null;
                }

                MapPoint near = null;
                double minDist = Double.MAX_VALUE;
                for (MapPoint nextCity : nextCityList) {
                        double dist = p.getLocation().distance(nextCity.getLocation());

                        if (dist < minDist) {
                                minDist = dist;
                                near = nextCity;
                        }
                }

                if (minDist <= MAX_CITY_DIST) // Wrong hit more the 30 km away ?
                        return near;
                else
                        return null;
        }
       
        private MapPoint findCityByIsIn(MapPoint place) {
               
                if (!locationAutofill.contains("is_in")) {
                        return null;
                }
               
                String isIn = place.getIsIn();

                if (isIn == null) {
                        return null;
                }

                String[] cityList = isIn.split(",");

                // Go through the isIn string and check if we find a city with this name
                // Lets hope we find the next bigger city

                double minDist = Double.MAX_VALUE;
                Collection<MapPoint> nextCityList = null;
                for (String cityCandidate : cityList) {
                        cityCandidate = cityCandidate.trim();

                        Collection<MapPoint> candidateCityList = cityMap.get(cityCandidate);
                        if (!candidateCityList.isEmpty()) {
                                if (nextCityList == null) {
                                        nextCityList = new ArrayList<>(candidateCityList.size());
                                }
                                nextCityList.addAll(candidateCityList);
                        }
                }

                if (nextCityList == null) {
                        // no city name found in the is_in tag
                        return null;
                }

                MapPoint nearbyCity = null;
                for (MapPoint nextCity : nextCityList) {
                        double dist = place.getLocation().distance(nextCity.getLocation());

                        if (dist < minDist) {
                                minDist = dist;
                                nearbyCity = nextCity;
                        }
                }

                // Check if the city is closer than MAX_CITY_DIST
                // otherwise don't use it but issue a warning
                if (minDist > MAX_CITY_DIST) {
                        log.warn("is_in of", place.getName(), "is far away from",
                                        nearbyCity.getName(), (minDist / 1000.0), "km is_in",
                                        place.getIsIn());
                        log.warn("Number of cities with this name:", nextCityList.size());
                }

                return nearbyCity;
        }

        public void autofillCities() {
                if (!locationAutofill.contains("nearest") && !locationAutofill.contains("is_in")) {
                        return;
                }
               
                log.info("Locator City   Map contains", cityMap.size(), "entries");
                log.info("Locator Places Map contains", placesMap.size(), "entries");
                log.info("Locator Finder KdTree contains", cityFinder.size(), "entries");

                int runCount = 0;
                int maxRuns = 2;
                int unresCount;

                do {
                        unresCount = 0;

                        for (MapPoint place : placesMap) {
                                if (place != null) {

                                        // first lets try exact name

                                        MapPoint near = findCityByIsIn(place);

                                        // if this didn't worked try to workaround german umlaut

                                        if (near == null) {
                                                // TODO perform a soundslike search
                                        }

                                        if (near != null) {
                                                if (place.getCity() == null)
                                                        place.setCity(near.getCity());
                                                if (place.getZip() == null)
                                                        place.setZip(near.getZip());
                                        } else if (locationAutofill.contains("nearest") && (runCount + 1) == maxRuns) {
                                                // In the last resolve run just take info from the next
                                                // known city
                                                near = cityFinder.findNextPoint(place);
                                                if (near != null && near.getCountry() != null) {
                                                        if (place.getCity() == null)
                                                                place.setCity(place.getName());
                                                }
                                        }

                                        if (near != null) {
                                                if (place.getRegion() == null)
                                                        place.setRegion(near.getRegion());

                                                if (place.getCountry() == null)
                                                        place.setCountry(near.getCountry());

                                        }

                                        if (near == null)
                                                unresCount++;
                                }
                        }

                        for (int i = 0; i < placesMap.size(); i++) {
                                MapPoint place = placesMap.get(i);

                                if (place != null) {
                                        if (place.getCity() != null) {
                                                addCity(place.getName(), place);
                                                placesMap.set(i, null);
                                        } else if ((runCount + 1) == maxRuns) {
                                                place.setCity(place.getName());
                                                addCity(place.getName(), place);
                                        }
                                }
                        }

                        runCount++;

                        log.info("Locator City   Map contains", cityMap.size(),
                                         "entries after resolver run", runCount,
                                         "Still unresolved", unresCount);

                } while (unresCount > 0 && runCount < maxRuns);

        }
       
        /**
         * Add MapPoint to cityMap and cityFinder
         *  
         * @param name Name that is used to find the city
         * @param p the MapPoint
         */

        private void addCity(String name, MapPoint p){
                if(name != null)
                {
                        cityMap.add(name, p);
                       
                        // add point to the kd-tree
                        cityFinder.add(p);
                }
               
        }
}