/*
* 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 uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapPoint;
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<String, MapPoint>();
private final KdTree<MapPoint> cityFinder = new KdTree<>();
private final List<MapPoint> placesMap = new ArrayList<MapPoint>();
/** Contains the tags defined by the option name-tag-list */
private final List<String> nameTags;
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.nameTags = LocatorUtil.getNameTags(props);
this.locationAutofill = new HashSet<String>(LocatorUtil.parseAutofillOption(props));
}
public void addCityOrPlace(MapPoint p)
{
if (p.isCity() == false)
{
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, nameTags);
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;
}
}
private final static String[] PREFERRED_NAME_TAGS = {"name","name:en","int_name"};
public String getCountryISOCode(Tags countryTags) {
for (String nameTag : PREFERRED_NAME_TAGS) {
String nameValue = countryTags.get(nameTag);
String isoCode = getCountryISOCode(nameValue);
if (isoCode != null) {
return isoCode;
}
}
for (String countryStr : countryTags.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") == false) {
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(",");
//System.out.println(p.getIsIn());
// 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") == false) {
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() == false) {
if (nextCityList == null) {
nextCityList = new ArrayList<MapPoint>(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") == false && locationAutofill.contains("is_in") == false) {
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);
}
}
}