
Steve Ratcliffe schrieb:
Here is a improved version of my "POI Address Support Patch" I have submitted some time. It add the following stuff to mkgmap:
- Country Region Autofill for Marks recently added find-by-name function - POI simple and complex house number and phone number support - POI address info is taken from "Karlsruhe address" info if available - Phone support is pretty cool if you have a garmin that support cell phone control
I appolgise for not having looked at this patch properly yet. I am very keen to have support for phone numbers and addresses on POI's so I would like to merge in at least parts of this patch very soon.
No problem Steve. I used the time and have done some small improvements to my patch. I have added LocatorConfig.xml to the resources directory. So people could add support for there country and can chose which administrative level they want to use as region. I think I also finally fixed the sorting of the cities. I looks like the Nuvi is doing a binary search on the citiy list. If it is not properly ordered some cities can not be found. I just post the new version it is still based on R857. I hope I could update my local branch to the latest trunk in the evening ... Thanks Berni. Index: src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java (revision 152) +++ src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java (working copy) @@ -16,6 +16,7 @@ */ package uk.me.parabola.imgfmt.app.lbl; +import java.util.Vector; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; @@ -23,6 +24,94 @@ * @author Steve Ratcliffe */ public class POIRecord { + + class SimpleStreetPhoneNumber // Helper Class to encode Street Phone Numbers + { + /** + Street and Phone numbers can be stored in two different ways in the poi record + Simple Number that only contain digits are coded in base 11 coding. + This helper class tries to code the given number. If the number contains other + chars like in 4a the coding fails and the caller has to use a Label instead + */ + + private byte encodedNumber[] = null; + private int encodedSize = 0; + + public boolean set(String number) + { + int i = 0; + int j = 0; + int val = 0; + + // remove sourounding whitespaces to increase chance for simple encoding + + number = number.trim(); + + encodedNumber = new byte[(number.length()/2)+2]; + + while(i < number.length()) + { + int c1 = 0; + int c2 = 0; + + c1 = decodeChar(number.charAt(i)); + i++; + + if(i < number.length()) + { + c2 = decodeChar(number.charAt(i)); + i++; + } + else + c2 = 10; + + if(c1 < 0 || c1 > 10 || c2 < 0 || c2 > 10) // Only 0-9 and - allowed + { + return false; + } + + val = c1 * 11 + c2; // Encode as base 11 + + if(i < 3 || i >= number.length()) // first byte needs special marking with 0x80 + val = val | 0x80; // If this is not set would be treated as label pointer + + encodedNumber[j++] = (byte)val; + } + + if((val & 0x80) == 0 || i < 3) // terminate string with 0x80 if not done + { + val = 0xF8; + encodedNumber[j++] = (byte)val; + } + + encodedSize = j; + + return true; + } + + public void write(ImgFileWriter writer) + { + for(int i = 0; i < encodedSize; i++) + writer.put(encodedNumber[i]); + } + + public boolean isUsed() + { + return (encodedSize > 0); + } + + public int getSize() + { + return encodedSize; + } + + private int decodeChar(char ch) + { + return (ch - '0'); + } + + } + public static final byte HAS_STREET_NUM = 0x01; public static final byte HAS_STREET = 0x02; public static final byte HAS_CITY = 0x04; @@ -43,13 +132,16 @@ private int offset = -1; private Label poiName; - private int streetNumber; + private SimpleStreetPhoneNumber simpleStreetNumber = new SimpleStreetPhoneNumber(); + private SimpleStreetPhoneNumber simplePhoneNumber = new SimpleStreetPhoneNumber(); + private Label streetName; private Label streetNumberName; // Used for numbers such as 221b + private Label complexPhoneNumber; // Used for numbers such as 221b + + private City city = null; + private char zipIndex = 0; - private char cityIndex ; - private char zipIndex ; - private String phoneNumber; public void setLabel(Label label) { @@ -60,14 +152,35 @@ this.streetName = label; } + public boolean setSimpleStreetNumber(String streetNumber) + { + return simpleStreetNumber.set(streetNumber); + } + + public void setComplexStreetNumber(Label label) + { + streetNumberName = label; + } + + public boolean setSimplePhoneNumber(String phone) + { + return simplePhoneNumber.set(phone); + } + + public void setComplexPhoneNumber(Label label) + { + complexPhoneNumber = label; + } + + public void setZipIndex(int zipIndex) { this.zipIndex = (char) zipIndex; } - public void setCityIndex(int cityIndex) + public void setCity(City city) { - this.cityIndex = (char) cityIndex; + this.city = city; } void write(ImgFileWriter writer, byte POIGlobalFlags, int realofs, @@ -82,11 +195,21 @@ if (POIGlobalFlags != getPOIFlags()) writer.put(getWrittenPOIFlags(POIGlobalFlags)); + if (streetNumberName != null) + { + int labOff = streetNumberName.getOffset(); + writer.put((byte)((labOff & 0x7F0000) >> 16)); + writer.putChar((char)(labOff & 0xFFFF)); + } + else if (simpleStreetNumber.isUsed()) + simpleStreetNumber.write(writer); + if (streetName != null) writer.put3(streetName.getOffset()); - if (cityIndex > 0) + if (city != null) { + char cityIndex = (char) city.getIndex(); if(numCities > 255) writer.putChar(cityIndex); else @@ -100,44 +223,60 @@ else writer.put((byte)zipIndex); } + + if (complexPhoneNumber != null) + { + int labOff = complexPhoneNumber.getOffset(); + writer.put((byte)((labOff & 0x7F0000) >> 16)); + writer.putChar((char)(labOff & 0xFFFF)); + } + else if (simplePhoneNumber.isUsed()) + simplePhoneNumber.write(writer); } byte getPOIFlags() { byte b = 0; if (streetName != null) b |= HAS_STREET; - if (cityIndex > 0) + if (simpleStreetNumber.isUsed() || streetNumberName != null) + b |= HAS_STREET_NUM; + if (city != null) b |= HAS_CITY; if (zipIndex > 0) - b |= HAS_ZIP; + b |= HAS_ZIP; + if (simplePhoneNumber.isUsed() || complexPhoneNumber != null) + b |= HAS_PHONE; return b; } byte getWrittenPOIFlags(byte POIGlobalFlags) { - int mask; - int flag = 0; - int j = 0; + int mask; + int flag = 0; + int j = 0; - int usedFields = getPOIFlags(); + int usedFields = getPOIFlags(); - /* the local POI flag is really tricky - if a bit is not set in the global mask - we have to skip this bit in the local mask. - In other words the meaning of the local bits - change influenced by the global bits */ + /* the local POI flag is really tricky if a bit is not set in the global mask + we have to skip this bit in the local mask. In other words the meaning of the local bits + change influenced by the global bits */ + + for(byte i = 0; i < 6; i++) + { + mask = 1 << i; - for (byte i = 0; i < 6; i++) { - mask = 1 << i; - - if ((mask & POIGlobalFlags) == mask) { - if ((mask & usedFields) == mask) - flag |= (1 << j); - j++; + if((mask & POIGlobalFlags) == mask) + { + if((mask & usedFields) == mask) + flag = flag | (1 << j); + j++; + } + } - } - return (byte) flag; + flag = flag | 0x80; // gpsmapedit asserts for this bit set + + return (byte) flag; } /** @@ -150,9 +289,17 @@ int size = 3; if (POIGlobalFlags != getPOIFlags()) size += 1; + if (simpleStreetNumber.isUsed()) + size += simpleStreetNumber.getSize(); + if (streetNumberName != null) + size += 3; + if (simplePhoneNumber.isUsed()) + size += simplePhoneNumber.getSize(); + if (complexPhoneNumber != null) + size += 3; if (streetName != null) - size += 3; - if (cityIndex > 0) + size += 3; + if (city != null) { /* depending on how many cities are in the LBL block we have @@ -160,9 +307,9 @@ */ if(numCities > 255) - size += 2; + size += 2; else - size += 1; + size += 1; } if (zipIndex > 0) { Index: src/uk/me/parabola/imgfmt/app/lbl/PlacesFile.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/PlacesFile.java (revision 152) +++ src/uk/me/parabola/imgfmt/app/lbl/PlacesFile.java (working copy) @@ -20,6 +20,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; @@ -34,6 +36,7 @@ private final Map<String, Country> countries = new LinkedHashMap<String, Country>(); private final Map<String, Region> regions = new LinkedHashMap<String, Region>(); private final Map<String, City> cities = new LinkedHashMap<String, City>(); + private final SortedMap<String, City> cityList = new TreeMap<String, City>(); private final Map<String, Zip> postalCodes = new LinkedHashMap<String, Zip>(); private final List<POIRecord> pois = new ArrayList<POIRecord>(); @@ -62,8 +65,12 @@ r.write(writer); placeHeader.endRegions(writer.position()); - for (City c : cities.values()) + for (String s : cityList.keySet()) + { + City c = cityList.get(s); c.write(writer); + } + placeHeader.endCity(writer.position()); int poistart = writer.position(); @@ -79,56 +86,110 @@ } Country createCountry(String name, String abbr) { - Country c = new Country(countries.size()+1); - + String s = abbr != null ? name + (char)0x1d + abbr : name; + + Country c = countries.get(s); + + if(c == null) + { + c = new Country(countries.size()+1); - Label l = lblFile.newLabel(s); - c.setLabel(l); - - countries.put(name, c); + Label l = lblFile.newLabel(s); + c.setLabel(l); + countries.put(s, c); + } return c; } Region createRegion(Country country, String name, String abbr) { - Region r = new Region(country, regions.size()+1); - + String s = abbr != null ? name + (char)0x1d + abbr : name; - Label l = lblFile.newLabel(s); - r.setLabel(l); - - regions.put(name, r); + String uniqueRegionName = s.toUpperCase().concat(Long.toString(country.getIndex())); + + Region r = regions.get(uniqueRegionName); + + if(r == null) + { + r = new Region(country, regions.size()+1); + Label l = lblFile.newLabel(s); + r.setLabel(l); + regions.put(uniqueRegionName, r); + } return r; } - City createCity(Country country, String name) { - City c = new City(country, cities.size()+1); + City createCity(Country country, String name, boolean unique) { + + String uniqueCityName = name.toUpperCase().concat("_C").concat(Long.toString(country.getIndex())); + + City c = null; - Label l = lblFile.newLabel(name); - c.setLabel(l); // label may be ignored if pointref is set + if(!unique) + c = cities.get(uniqueCityName); + + if(c == null) + { + c = new City(country); - cities.put(name, c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + cityList.put(name + " 0" + c, c); + cities.put(uniqueCityName, c); + } + return c; - } + } - City createCity(Region region, String name) { - City c = new City(region, cities.size()+1); + City createCity(Region region, String name, boolean unique) { + + String uniqueCityName = name.toUpperCase().concat("_R").concat(Long.toString(region.getIndex())); + + City c = null; - Label l = lblFile.newLabel(name); - c.setLabel(l); // label may be ignored if pointref is set + if(!unique) + c = cities.get(uniqueCityName); + + if(c == null) + { + c = new City(region); - cities.put(name, c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + cityList.put(name + " 0" + c, c); + cities.put(uniqueCityName, c); + } + return c; } + private void sortCities() + { + int index = 1; + + for (String s : cityList.keySet()) + { + City c = cityList.get(s); + c.setIndex(index++); + } + } + Zip createZip(String code) { - Zip z = new Zip(postalCodes.size()+1); + + Zip z = postalCodes.get(code); + + if(z == null) + { + z = new Zip(postalCodes.size()+1); - Label l = lblFile.newLabel(code); - z.setLabel(l); + Label l = lblFile.newLabel(code); + z.setLabel(l); - postalCodes.put(code, z); + postalCodes.put(code, z); + } return z; } @@ -146,6 +207,9 @@ } void allPOIsDone() { + + sortCities(); + poisClosed = true; byte poiFlags = 0; Index: src/uk/me/parabola/imgfmt/app/lbl/City.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/City.java (revision 152) +++ src/uk/me/parabola/imgfmt/app/lbl/City.java (working copy) @@ -30,7 +30,7 @@ private static final int POINT_REF = 0x8000; private static final int REGION_IS_COUNTRY = 0x4000; - private final int index; + private int index = -1; private final Region region; private final Country country; @@ -49,13 +49,13 @@ // null if the location is being specified. private Label label; - public City(Region region, int index) { + public City(Region region) { this.region = region; this.country = null; this.index = index; } - public City(Country country, int index) { + public City(Country country) { this.country = country; this.region = null; this.index = index; @@ -83,9 +83,15 @@ } public int getIndex() { + if (index == -1) + throw new IllegalStateException("Offset not known yet."); return index; } + public void setIndex(int index) { + this.index = index; + } + public void setLabel(Label label) { pointRef = false; this.label = label; Index: src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java (revision 152) +++ src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java (working copy) @@ -147,14 +147,18 @@ return places.createRegion(country, region, abbr); } - public City createCity(Region region, String city) { - return places.createCity(region, city); + public City createCity(Region region, String city, boolean unique) { + return places.createCity(region, city, unique); } - public City createCity(Country country, String city) { - return places.createCity(country, city); + public City createCity(Country country, String city, boolean unique) { + return places.createCity(country, city, unique); } + public Zip createZip(String code) { + return places.createZip(code); + } + public void allPOIsDone() { places.allPOIsDone(); } Index: src/uk/me/parabola/imgfmt/app/map/Map.java =================================================================== --- src/uk/me/parabola/imgfmt/app/map/Map.java (revision 152) +++ src/uk/me/parabola/imgfmt/app/map/Map.java (working copy) @@ -225,6 +225,14 @@ treFile.addPolygonOverview(ov); } + /** + * Set the point of interest flags. + * @param flags The POI flags. + */ + public void setPoiDisplayFlags(int flags) { + treFile.setPoiDisplayFlags((byte) flags); + } + public void addMapObject(MapObject item) { rgnFile.addMapObject(item); } Index: src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java =================================================================== --- src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java (revision 152) +++ src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java (working copy) @@ -225,9 +225,51 @@ } private void elementSetup(MapElement ms, GType gt, Element element) { + ms.setName(element.getName()); ms.setType(gt.getType()); ms.setMinResolution(gt.getMinResolution()); ms.setMaxResolution(gt.getMaxResolution()); + + // Now try to get some address info for POIs + + String city = element.getTag("addr:city"); + String zip = element.getTag("addr:postcode"); + String street = element.getTag("addr:street"); + String houseNumber = element.getTag("addr:housenumber"); + String phone = element.getTag("phone"); + String isIn = element.getTag("is_in"); + String country = element.getTag("is_in:country"); + String region = element.getTag("is_in:county"); + + if(zip == null) + zip = element.getTag("openGeoDB:postal_codes"); + + if(city == null) + city = element.getTag("openGeoDB:sort_name"); + + if(city != null) + ms.setCity(city); + + if(zip != null) + ms.setZip(zip); + + if(street != null) + ms.setStreet(street); + + if(houseNumber != null) + ms.setHouseNumber(houseNumber); + + if(isIn != null) + ms.setIsIn(isIn); + + if(phone != null) + ms.setPhone(phone); + + if(country != null) + ms.setCountry(country); + + if(region != null) + ms.setRegion(region); } } Index: src/uk/me/parabola/mkgmap/build/Locator.java =================================================================== --- src/uk/me/parabola/mkgmap/build/Locator.java (revision 0) +++ src/uk/me/parabola/mkgmap/build/Locator.java (revision 175) @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2009 Bernhard Heibler + * + * 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. + * + * The Locator tries to fill missing country, region, postal coude information + * + * To do so we analyse the is_in information and if this doesn't helps us we + * try to get info from the next known city + * + * Author: Bernhard Heibler + * Create date: 02-Jan-2009 + */ +package uk.me.parabola.mkgmap.build; + +import java.util.Collection; +import uk.me.parabola.mkgmap.general.MapPoint; +import uk.me.parabola.imgfmt.app.Coord; +import java.util.Vector; + +public class Locator { + + private final MapPointFastFindMap cityMap = new MapPointFastFindMap(); + private final MapPointFastFindMap fuzzyCityMap = new MapPointFastFindMap(); + private final java.util.Vector<MapPoint> placesMap = new Vector<MapPoint>(); + + private LocatorConfig locConfig = new LocatorConfig(); + + static double totalTime = 0; + static long totalFinds = 0; + + public void addLocation(MapPoint p) + { + resolveIsInInfo(p); // Preprocess the is_in field + + if(p.getCity() != null) + { + // All cities I have more detailed info about. + // In Germany, Austria and Swizerland this are real independet communites Gemeinden + + cityMap.put(p.getCity(),p); + fuzzyCityMap.put(fuzzyDecode(p.getCity()),p); + } + else + { + // All other places which do not seam to be a real city has to resolved later + placesMap.add(p); + } + } + + public void setDefaultCountry(String country, String abbr) + { + locConfig.setDefaultCountry(country, abbr); + } + + public String fixCountryString(String country) + { + return locConfig.fixCountryString(country); + } + + private String isCountry(String country) + { + return locConfig.isCountry(country); + } + + public String getCountryCode(String country) + { + return locConfig.getCountryCode(country); + } + + private boolean isOpenGeoDBCountry(String country) + { + // Countries that have open geo db data in osm + // Right now this are only germany, austria and swizerland + return locConfig.isOpenGeoDBCountry(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(p.getCountry() != null) + p.setCountry(fixCountryString(p.getCountry())); + + 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 ? + { + // The one before contient should be the country + p.setCountry(fixCountryString(cityList[cityList.length-2].trim())); + + // aks the config which info to use for region info + int offset = locConfig.getRegionOffset(p.getCountry()) + 1; + + if(cityList.length > offset) + p.setRegion(cityList[cityList.length-(offset+1)].trim()); + + } + + // Format 2 other way round: "Continent,Country,State,County" + + if(cityList.length > 1 && isContinent(cityList[0])) // Is first a continent ? + { + // The one before contient should be the country + p.setCountry(fixCountryString(cityList[1].trim())); + + int offset = locConfig.getRegionOffset(p.getCountry()) + 1; + + if(cityList.length > offset) + p.setRegion(cityList[offset].trim()); + } + + // 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 countryStr = isCountry(cityList[cityList.length-1]); + + if(countryStr != null) + { + p.setCountry(countryStr); + + int offset = locConfig.getRegionOffset(countryStr) + 1; + + if(cityList.length > offset) + p.setRegion(cityList[cityList.length-(offset+1)].trim()); + } + } + } + + if(p.getCountry() != null && p.getRegion() != null && p.getCity() == null) + { + // In OpenGeoDB Countries I don't want to mess up the info which city is a real independent + // Community in all other countries I just have to do it + + if(isOpenGeoDBCountry(p.getCountry()) == false) + p.setCity(p.getName()); + } + } + + public MapPoint findNextPoint(MapPoint p) + { + long startTime = System.nanoTime(); + + MapPoint nextPoint = null; + + nextPoint = cityMap.findNextPoint(p); + + totalFinds++; + totalTime = totalTime + ((System.nanoTime() - startTime)/1e9); + return nextPoint; + } + + private MapPoint findCity(MapPoint place, boolean fuzzy) + { + MapPoint near = null; + Double minDist = Double.MAX_VALUE; + Collection <MapPoint> nextCityList = null; + boolean checker = false; + + String isIn = place.getIsIn(); + + if(isIn != 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 + + for(int i = 0; i < cityList.length; i++) + { + String biggerCityName=cityList[i].trim(); + + + if(fuzzy == false) + nextCityList = cityMap.getList(biggerCityName); + else + nextCityList = fuzzyCityMap.getList(fuzzyDecode(biggerCityName)); + + if(nextCityList != null) + { + for (MapPoint nextCity: nextCityList) + { + Double dist = place.getLocation().distance(nextCity.getLocation()); + + if(dist < minDist) + { + minDist = dist; + near = nextCity; + } + } + } + } + + if (checker) // Some debug output to find suspicios relations + { + + if(near != null && minDist > 30000) + { + System.out.println(place.getName() + " -> " + near.getName() + + " " + (minDist/1000.0) + " km " + place.getIsIn()); + if(nextCityList != null) + System.out.println("Number of cities with this name: " + nextCityList.size()); + } + + if(near == null) + { + System.out.println("CAN't locate " + place.getName() + " " + place.getIsIn()); + } + + if(near != null && fuzzy) + { + System.out.println(place.getName() + " may belong to " + near.getName()); + } + } + } + + return near; + } + + public void resolve() { + + System.out.println("\nLocator City Map contains " + cityMap.size() + " entries"); + System.out.println("Locator Places Map contains " + placesMap.size() + " entries"); + + int runCount = 0; + int maxRuns = 2; + int unresCount; + + do + { + unresCount=0; + + for (int i = 0; i < placesMap.size(); i++) + { + MapPoint place = placesMap.get(i); + + if(place != null) + { + + // first lets try exact name + + MapPoint near = findCity(place, false); + + + // if this didn't worked try to workaround german umlaute + + if(near == null) + near = findCity(place, true); + + + if(near != null) + { + place.setCity(near.getCity()); + place.setZip(near.getZip()); + } + else if ((runCount + 1) == maxRuns) + { + // In the last resolve run just take info from the next known city + near = cityMap.findNextPoint(place); + } + + + if(near != null) + { + if(place.getRegion() == null) + place.setRegion(near.getRegion()); + + if(place.getCountry() == null) + place.setCountry(near.getCountry()); + + if((runCount + 1) == maxRuns) + place.setCity(place.getName()); + } + + if(near == null) + unresCount++; + + } + } + + for (int i = 0; i < placesMap.size(); i++) + { + MapPoint place = placesMap.get(i); + + if (place != null && place.getCity() != null) + { + cityMap.put(place.getName(),place); + fuzzyCityMap.put(fuzzyDecode(place.getName()),place); + placesMap.set(i, null); + } + } + + runCount++; + + System.out.println("Locator City Map contains " + cityMap.size() + " entries after resolver run " + runCount + " Still unresolved " + unresCount); + + } + while(unresCount > 0 && runCount < maxRuns); + + } + + public void printStat() + { + System.out.println("Locator Find called: " + totalFinds + " time"); + System.out.println("Locator Find time: " + totalTime + " s"); + + cityMap.printStat(); + } + + private String fuzzyDecode(String stringToDecode) + { + + if(stringToDecode == null) + return stringToDecode; + + String decodeString = stringToDecode.toUpperCase().trim(); + + // German umlaut resolution + decodeString = decodeString.replaceAll("Ä","AE").replaceAll("Ü","UE").replaceAll("Ö","OE"); + + return (decodeString); + } + +} + Index: src/uk/me/parabola/mkgmap/build/MapBuilder.java =================================================================== --- src/uk/me/parabola/mkgmap/build/MapBuilder.java (revision 152) +++ src/uk/me/parabola/mkgmap/build/MapBuilder.java (working copy) @@ -27,6 +27,8 @@ import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.lbl.Country; +import uk.me.parabola.imgfmt.app.lbl.Zip; +import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.LBLFile; import uk.me.parabola.imgfmt.app.lbl.POIRecord; import uk.me.parabola.imgfmt.app.lbl.Region; @@ -83,9 +85,11 @@ private static final int CLEAR_TOP_BITS = (32 - 15); private final java.util.Map<MapPoint,POIRecord> poimap = new HashMap<MapPoint,POIRecord>(); - private final SortedMap<String, City> sortedCities = new TreeMap<String, City>(); + private final java.util.Map<MapPoint,City> cityMap = new HashMap<MapPoint,City>(); + private boolean doRoads; + private Locator locator = new Locator(); private Country country; private Region region; @@ -123,6 +127,7 @@ if(regionName != null) region = lblFile.createRegion(country, regionName, regionAbbr); + processCities(map, src); processPOIs(map, src); //preProcessRoads(map, src); processOverviews(map, src); @@ -157,43 +162,173 @@ } /** - * First stage of handling POIs + * Processing of Cities * - * POIs need to be handled first, because we need the offsets - * in the LBL file. + * Fills the city list in lbl block that is required for find by name + * It also builds up information that is required to get address info + * for the POIs * * @param map The map. * @param src The map data. */ - private void processPOIs(Map map, MapDataSource src) { + private void processCities(Map map, MapDataSource src) { LBLFile lbl = map.getLblFile(); + locator.setDefaultCountry(countryName, countryAbbr); + // collect the names of the cities for (MapPoint p : src.getPoints()) { if(p.isCity() && p.getName() != null) - sortedCities.put(p.getName(), null); + locator.addLocation(p); // Put the city info the map for missing info } - // create the city records in alphabetic order - for (String s : sortedCities.keySet()) { - City c; - if(region != null) - c = lbl.createCity(region, s); - else - c = lbl.createCity(country, s); - sortedCities.put(s, c); + locator.resolve(); // Try to fill missing information that include search of next city + + for (MapPoint p : src.getPoints()) + { + if(p.isCity() && p.getName() != null) + { + + String cityName = p.getName(); + + //System.out.println(s); + + Country thisCountry; + Region thisRegion; + City thisCity; + + String CountryStr = p.getCountry(); + String RegionStr = p.getRegion(); + + if(CountryStr != null) + thisCountry = lbl.createCountry(CountryStr, locator.getCountryCode(CountryStr)); + else + thisCountry = country; + + if(RegionStr != null) + { + thisRegion = lbl.createRegion(thisCountry,RegionStr, null); + } + else + thisRegion = region; + + if(thisRegion != null) + thisCity = lbl.createCity(thisRegion, p.getName(), true); + else + thisCity = lbl.createCity(thisCountry, p.getName(), true); + + cityMap.put(p, thisCity); + } } - // TODO: this is temporarily removed, but should be put back - // in once there are some addresses etc available here. - //for (MapPoint p : src.getPoints()) { - //POIRecord r = null; - // if(p.hasCityName()) { - // r = lbl.createPOI(p.getName()); - // r.setCityIndex(sortedCities.get(p.getCityName()).getIndex()); - //} - //poimap.put(p, r); - //} + } + + private void processPOIs(Map map, MapDataSource src) { + + LBLFile lbl = map.getLblFile(); + long poiAddrCountr = 0; + + for (MapPoint p : src.getPoints()) { + + if(p.isCity() == false) + { + POIRecord r = lbl.createPOI(p.getName()); + String CountryStr = p.getCountry(); + String RegionStr = p.getRegion(); + String ZipStr = p.getZip(); + String CityStr = p.getCity(); + boolean guessed = false; + + if(CityStr != null || ZipStr != null ||RegionStr != null || CountryStr != null) + poiAddrCountr++; + + if(CountryStr != null) + CountryStr = locator.fixCountryString(CountryStr); + + if(CountryStr == null || RegionStr == null || (ZipStr == null && CityStr == null)) + { + MapPoint nextCity = locator.findNextPoint(p); + + if(nextCity != null) + { + guessed = true; + + if (CountryStr == null) CountryStr = nextCity.getCountry(); + if (RegionStr == null) RegionStr = nextCity.getRegion(); + + if(ZipStr == null) ZipStr = nextCity.getZip(); + if(CityStr == null) CityStr = nextCity.getCity(); + + } + } + + if(CityStr != null) + { + Country thisCountry; + Region thisRegion; + City city; + + if(CountryStr != null) + thisCountry = lbl.createCountry(CountryStr, locator.getCountryCode(CountryStr)); + else + thisCountry = country; + + if(RegionStr != null) + thisRegion = lbl.createRegion(thisCountry,RegionStr, null); + else + thisRegion = region; + + if(thisRegion != null) + city = lbl.createCity(thisRegion, CityStr, false); + else + city = lbl.createCity(thisCountry, CityStr, false); + + r.setCity(city); + + } + + if (ZipStr != null) + { + Zip zip = lbl.createZip(ZipStr); + r.setZipIndex(zip.getIndex()); + } + + if(p.getStreet() != null) + { + Label streetName = lbl.newLabel(p.getStreet()); + r.setStreetName(streetName); + } + else if (guessed == true) + { + Label streetName = lbl.newLabel("FIXME ADDRESS"); + r.setStreetName(streetName); + } + + if(p.getHouseNumber() != null) + { + if(r.setSimpleStreetNumber(p.getHouseNumber()) == false) + { + Label streetNumber = lbl.newLabel(p.getHouseNumber()); + r.setComplexStreetNumber(streetNumber); + } + } + + if(p.getPhone() != null) + { + if(r.setSimplePhoneNumber(p.getPhone()) == false) + { + Label phoneNumber = lbl.newLabel(p.getPhone()); + r.setComplexPhoneNumber(phoneNumber); + } + } + + poimap.put(p, r); + } + } + + System.out.println(poiAddrCountr + " POIs have address info"); + lbl.allPOIsDone(); + } /** @@ -449,7 +584,10 @@ p.setLongitude(coord.getLongitude()); if(div.getZoom().getLevel() == 0 && name != null) { - City c = sortedCities.get(name); + // retrieve the City created earlier for + // this point and store the point info + // in it + City c = (City)cityMap.get(point); if(pointIndex > 255) { System.err.println("Can't set city point index for " + name + " (too many indexed points in division)\n"); Index: src/uk/me/parabola/mkgmap/build/MapPointFastFindMap.java =================================================================== --- src/uk/me/parabola/mkgmap/build/MapPointFastFindMap.java (revision 0) +++ src/uk/me/parabola/mkgmap/build/MapPointFastFindMap.java (revision 163) @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2009 Bernhard Heibler + * + * 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. + * + * This is multimap to store city information for the Address Locator + * It provides also a fast tile based nearest point search function + * + * + * Author: Bernhard Heibler + * Create date: 02-Jan-2009 + */ + +package uk.me.parabola.mkgmap.build; + + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Vector; +import uk.me.parabola.mkgmap.general.MapPoint; +import uk.me.parabola.imgfmt.app.Coord; + +class MapPointFastFindMap{ + + private final java.util.Map<String,Vector<MapPoint>> map = new HashMap<String,Vector<MapPoint>>(); + private final java.util.Map<Long,Vector<MapPoint>> posMap = new HashMap<Long,Vector<MapPoint>>(); + private final java.util.Vector<MapPoint> points = new Vector<MapPoint>(); + + public static final long POS_HASH_DIV = 8000; // the smaller -> more tiles + public static final long POS_HASH_MUL = 10000; // multiplicator for latitude to create hash + + public MapPoint put(String name, MapPoint p) + { + Vector<MapPoint> list; + + list = map.get(name); + + if(list == null){ + + list = new Vector<MapPoint>(); + list.add(p); + map.put(name, list); + } + else + list.add(p); + + points.add(p); + + long posHash = getPosHashVal(p.getLocation().getLatitude(), p.getLocation().getLongitude()); + + //System.out.println("PosHash " + posHash); + + list = posMap.get(posHash); + + if(list == null) + { + list = new Vector<MapPoint>(); + list.add(p); + posMap.put(posHash, list); + } + else + list.add(p); + + return p; + } + + public MapPoint get(String name) + { + Vector<MapPoint> list; + + list = map.get(name); + + if(list != null) + return list.elementAt(0); + else + return null; + } + + public Collection<MapPoint> getList(String name) + { + return map.get(name); + } + + public long size() + { + return points.size(); + } + + public Collection<MapPoint> values() + { + return points; + } + + public MapPoint get(int index) + { + return points.get(index); + } + + public MapPoint set(int index, MapPoint p) + { + return points.set(index, p); + } + + public boolean remove(MapPoint p) + { + return points.remove(p); + } + + public MapPoint findNextPoint(MapPoint p) + { + /* tile based search + + to prevent expensive linear search over all points we put the points + into tiles. We just search the tiles the point is in linear and the + sourounding tiles. If we don't find a point we have to search further + arround the central tile + + */ + + Vector<MapPoint> list; + double minDist = Double.MAX_VALUE; + MapPoint nextPoint = null; + + if(points.size() < 1) // No point in list + return nextPoint; + + long centLatitIdx = p.getLocation().getLatitude() / POS_HASH_DIV ; + long centLongiIdx = p.getLocation().getLongitude() / POS_HASH_DIV ; + long delta = 1; + + long latitIdx; + long longiIdx; + long posHash; + + do + { + // in the first step we only check our tile and the tiles sourinding us + + for(latitIdx = centLatitIdx - delta; latitIdx <= centLatitIdx + delta; latitIdx++) + for(longiIdx = centLongiIdx - delta; longiIdx <= centLongiIdx + delta; longiIdx++) + { + if(delta < 2 + || latitIdx == centLatitIdx - delta + || latitIdx == centLatitIdx + delta + || longiIdx == centLongiIdx - delta + || longiIdx == centLongiIdx + delta) + { + + posHash = latitIdx * POS_HASH_MUL + longiIdx; + + list = posMap.get(posHash); + + if(list != null) + { + + for (MapPoint actPoint: list) + { + double distance = actPoint.getLocation().distance(p.getLocation()); + + if(distance < minDist) + { + nextPoint = actPoint; + minDist = distance; + } + } + } + } + } + delta ++; // We have to look in tiles farer away + } + while(nextPoint == null); + + return nextPoint; + } + + private long getPosHashVal(long lat, long lon) + { + long latitIdx = lat / POS_HASH_DIV ; + long longiIdx = lon / POS_HASH_DIV ; + + //System.out.println("LatIdx " + latitIdx + " LonIdx " + longiIdx); + + return latitIdx * POS_HASH_MUL + longiIdx; + } + + public void printStat() + { + System.out.println("Locator PosHashmap contains " + posMap.size() + " tiles"); + } +} \ No newline at end of file Index: src/uk/me/parabola/mkgmap/reader/polish/PolishMapDataSource.java =================================================================== --- src/uk/me/parabola/mkgmap/reader/polish/PolishMapDataSource.java (revision 152) +++ src/uk/me/parabola/mkgmap/reader/polish/PolishMapDataSource.java (working copy) @@ -362,6 +362,23 @@ } catch (NumberFormatException e) { endLevel = 0; } + } else if (name.equals("ZipCode")) { + elem.setZip(recode(value)); + } else if (name.equals("CityName")) { + elem.setCity(recode(value)); + } else if (name.equals("StreetDesc")) { + elem.setStreet(recode(value)); + } else if (name.equals("HouseNumber")) { + elem.setHouseNumber(recode(value)); + } else if (name.equals("is_in")) { + elem.setIsIn(recode(value)); + } else if (name.equals("Phone")) { + elem.setPhone(recode(value)); + } else if (name.equals("CountryName")) { + elem.setCountry(recode(value)); + } else if (name.equals("RegionName")) { + //System.out.println("RegionName " + value); + elem.setRegion(recode(value)); } else { return false; } Index: src/uk/me/parabola/mkgmap/general/MapElement.java =================================================================== --- src/uk/me/parabola/mkgmap/general/MapElement.java (revision 152) +++ src/uk/me/parabola/mkgmap/general/MapElement.java (working copy) @@ -15,6 +15,9 @@ */ package uk.me.parabola.mkgmap.general; +import java.util.Map; +import java.util.HashMap; + import uk.me.parabola.imgfmt.app.Coord; /** @@ -29,6 +32,13 @@ private int minResolution = 24; private int maxResolution = 24; + + private String zipCode; + private String city; + private String region; + private String country; + + private final Map<String, String> attributes = new HashMap<String, String>(); protected MapElement() { } @@ -53,9 +63,84 @@ } public void setName(String name) { - this.name = name; + if(name != null) + this.name = name.toUpperCase(); } + public String getCity() { + return city; + } + + public void setCity(String city) { + if(city != null) + this.city = city.toUpperCase(); + } + + public String getZip() { + return zipCode; + } + + public void setZip(String zip) { + this.zipCode = zip; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + if(country != null) + this.country = country.toUpperCase(); + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + if(region != null) + this.region = region.toUpperCase(); + } + + public String getStreet() { + return attributes.get("street"); + } + + public void setStreet(String street) { + attributes.put("street", street); + } + + public String getPhone() { + return attributes.get("phone"); + } + + public void setPhone(String phone) { + + if(phone.startsWith("00")) + { + phone = phone.replaceFirst("00","+"); + } + attributes.put("phone", phone); + } + + public String getHouseNumber() { + return attributes.get("houseNumber"); + } + + public void setHouseNumber(String houseNumber) { + attributes.put("houseNumber", houseNumber); + } + + public String getIsIn() { + return attributes.get("isIn"); + } + + public void setIsIn(String isIn) { + if(isIn != null) + attributes.put("isIn", isIn.toUpperCase()); + } + + /** * This is the type code that goes in the .img file so that the GPS device * knows what to display. Index: resources/LocatorConfig.xml =================================================================== --- resources/LocatorConfig.xml (revision 0) +++ resources/LocatorConfig.xml (revision 175) @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<locator> + <country name="Deutschland" abr="DEU" geodb="1" regionOffset="3"> + <variant>Bundesrepublik Deutschland</variant> + <variant>Germany</variant> + </country> + <country name="Österreich" abr="AUT" geodb="1"> + <variant>Austria</variant> + </country> + <country name="Schweiz" abr="CHE" geodb="1"> + <variant>Switzerland</variant> + <variant>CH</variant> + </country> + <country name="United Kingdom" abr="GBR"> + <variant>UK</variant> + </country> + <country name="Italia" abr="ITA" regionOffset="2"> + <variant>Italy</variant> + </country> + <country name="France" abr="FRA"> + </country> + +</locator> \ No newline at end of file Index: osm2mp.pl =================================================================== --- osm2mp.pl (revision 160) +++ osm2mp.pl (working copy) @@ -228,8 +228,7 @@ my $id; my $latlon; -my ($poi, $poiname, $ZipCode, $CityName, $StreetDesc, $HouseNumber, $isIn, $phone); -my ($CountryName, $RegionName); +my ($poi, $poiname); my $nameprio = 99; while (<IN>) { @@ -243,41 +242,18 @@ $poi = ""; $poiname = ""; - $ZipCode = ""; - $CityName = ""; - $StreetDesc = ""; - $HouseNumber = ""; - $isIn = ""; - $phone = ""; $nameprio = 99; - $CountryName = ""; - $RegionName = ""; next; } if ( /\<tag/ ) { - /^.*k=["'](.*)["'].*v=["'](.*)["'].*$/; + /^.*k=["'](.*)["'].*v=["'](.*)["'].*$/; $poi = "$1=$2" if ($poitype{"$1=$2"}); my $tagprio = indexof(\@nametagarray, $1); if ($tagprio>=0 && $tagprio<$nameprio) { $poiname = convert_string ($2); $nameprio = $tagprio; } - - #printf STDERR "$1 $2\n" if ( "$1" ne "created_by"); - - $CountryName = convert_string($2) if ( "$1" eq "is_in:country"); - $RegionName = convert_string($2) if ( "$1" eq "is_in:county"); - $isIn = convert_string($2) if ( "$1" eq "is_in"); - $ZipCode = convert_string($2) if ( "$1" eq "openGeoDB:postal_codes"); - $ZipCode = convert_string($2) if ( "$1" eq "addr:postcode" ); - $CityName = convert_string($2) if ( "$1" eq "addr:city" ); - #$CityName = convert_string($2) if ( "$1" eq "openGeoDB:name" ); - $CityName = convert_string($2) if ( "$1" eq "openGeoDB:sort_name" ); - $StreetDesc = convert_string($2) if ( "$1" eq "addr:street" ); - $HouseNumber = convert_string($2) if ( "$1" eq "addr:housenumber" ); - $phone = convert_string($2) if ( "$1" eq "phone" ); - next; } @@ -294,14 +270,6 @@ printf "EndLevel=%d\n", $type[2] if ($type[2] > $type[1]); printf "City=Y\n", if ($type[3]); print "Label=$poiname\n" if ($poiname); - printf "ZipCode=$ZipCode\n", if ($ZipCode); - printf "CityName=$CityName\n", if ($CityName); - printf "StreetDesc=$StreetDesc\n", if ($StreetDesc); - printf "HouseNumber=$HouseNumber\n", if ($HouseNumber); - printf "is_in=$isIn\n", if ($isIn); - printf "Phone=$phone\n", if ($phone); - printf "CountryName=$CountryName\n", if ($CountryName); - printf "RegionName=$RegionName\n", if ($RegionName); print "[END]\n\n"; } }