
Hi Guys, first of all I should introduce my self since I forgot to do so when I joined this list some weeks ago. Shame on me ... My name is Berni I'm 34 and live in southern Germany. In my daytime life I develop software for Automatic Test Systems in C++. So please excuse my poor Java coding .. I joined the OSM project about a year ago. Since then my gps logger is always in my pocket. Last summer I got by "accident" a Nuvi and I was really happy when I found out that I can use my OSM maps on this device. But now to the real stuff ... 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 If no country / region / address info is given information is take from nearest city. I have added a class that uses tiles for fast nearest point search. In countries that have OpenGeoDB data imported into OSM this works pretty good. In Bavaria and Austria it works pretty good. For other countries this doesn't work so well. I have also tested it near Cambridge UK and I it was also not so bad. Unfortunately the is_in tag I have to us is very fuzzy. I needed some lookup tables right now. This are hardcoded in Locator.java right now this should be in a config file. It might help to pass your country with the --country-name option for better County detection. If you use the .osm file as direct input for mkgmap you should be fine. If you use polish map you have to patch osm2mp because I needed some more fields. Let me know If this works for you guys or if you have further questions ... Thanks Berni. Index: src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java (revision 162) +++ 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 char cityIndex = 0; + private char zipIndex = 0; - private char cityIndex ; - private char zipIndex ; - private String phoneNumber; public void setLabel(Label label) { @@ -60,6 +152,27 @@ 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; @@ -82,6 +195,15 @@ 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()); @@ -100,44 +222,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 (simpleStreetNumber.isUsed() || streetNumberName != null) + b |= HAS_STREET_NUM; if (cityIndex > 0) 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,8 +288,16 @@ 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; + size += 3; if (cityIndex > 0) { /* @@ -160,9 +306,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 162) +++ src/uk/me/parabola/imgfmt/app/lbl/PlacesFile.java (working copy) @@ -34,6 +34,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 List<City> cityList = new ArrayList<City>(); private final Map<String, Zip> postalCodes = new LinkedHashMap<String, Zip>(); private final List<POIRecord> pois = new ArrayList<POIRecord>(); @@ -62,7 +63,7 @@ r.write(writer); placeHeader.endRegions(writer.position()); - for (City c : cities.values()) + for (City c : cityList) c.write(writer); placeHeader.endCity(writer.position()); @@ -79,56 +80,99 @@ } 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, cityList.size()+1); - cities.put(name, c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + cityList.add(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, cityList.size()+1); - cities.put(name, c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + cityList.add(c); + cities.put(uniqueCityName, c); + } + return c; } 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; } Index: src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java (revision 162) +++ 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 162) +++ 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 162) +++ 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 0) @@ -0,0 +1,384 @@ +/* + * 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; + +public class Locator { + + private final MapPointFastFindMap cityMap = new MapPointFastFindMap(); + private final MapPointFastFindMap fuzzyCityMap = new MapPointFastFindMap(); + private final MapPointFastFindMap placesMap = new MapPointFastFindMap(); + + private String defaultCountryName = null; + private String defaultCountryAbbr = null; + + 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.put(p.getName(),p); + } + } + + public void setDefaultCountry(String country, String abbr) + { + defaultCountryName = country.toUpperCase(); + defaultCountryAbbr = abbr; + } + + public String fixCountryString(String country) + { + String cStr = country.toUpperCase(); + + if(cStr.equals("BUNDESREPUBLIK DEUTSCHLAND")) return("DEUTSCHLAND"); + if(cStr.equals("UK")) return("United Kingdom"); + + return(cStr); + } + + private String isCountry(String country) + { + String cStr = fixCountryString(country); + + if(cStr.equals("DEUTSCHLAND")) return(cStr); + if(cStr.equals("ÖSTERREICH")) return(cStr); + if(cStr.equals("SCHWEIZ")) return(cStr); + if(cStr.equals("UNITED KINGDOM")) return(cStr); + if(cStr.equals(defaultCountryName)) return(defaultCountryName); + return null; + } + + public String getCountryCode(String country) + { + String cStr = country.toUpperCase(); + + if(cStr.equals("DEUTSCHLAND")) return("DEU"); + if(cStr.equals("ÖSTERREICH")) return("AUT"); + if(cStr.equals("SCHWEIZ")) return("CHE"); + if(cStr.equals("UNITED KINGDOM")) return("GBR"); + if(cStr.equals(defaultCountryName)) return(defaultCountryAbbr); + + return null; + } + + private boolean isOpenGeoDBCountry(String country) + { + // Countries that have open geo db data in osm + // Right now this are only germany, austria and swizerland + + String cStr = country.toUpperCase(); + if(cStr.equals("DEUTSCHLAND")) return(true); + if(cStr.equals("ÖSTERREICH")) return(true); + if(cStr.equals("SCHWEIZ")) return(true); + + return false; + } + + private boolean isContinent(String continent) + { + String s = continent.toUpperCase(); + + if(s.equals("EUROPE")) return(true); + + return false; + } + + + /** + * 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 + if(cityList.length > 1) + p.setCountry(fixCountryString(cityList[cityList.length-2].trim())); + + // If this exist this could be the county in german Landkreis + if(cityList.length > 2) + p.setRegion(cityList[cityList.length-3].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 + if(cityList.length > 1) + p.setCountry(fixCountryString(cityList[1].trim())); + + // If this exist this could be the county in german Landkreis + if(cityList.length > 2) + p.setRegion(cityList[3].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); + + if(cityList.length > 2) + p.setRegion(cityList[cityList.length-3].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 162) +++ 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,10 @@ 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 SortedMap<String, Object> sortedCities = new TreeMap<String, Object>(); private boolean doRoads; + private Locator locator = new Locator(); private Country country; private Region region; @@ -123,6 +126,7 @@ if(regionName != null) region = lblFile.createRegion(country, regionName, regionAbbr); + processCities(map, src); processPOIs(map, src); //preProcessRoads(map, src); processOverviews(map, src); @@ -157,43 +161,168 @@ } /** - * 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); + { + sortedCities.put(p.getName() + "@" + p, p); + locator.addLocation(p); // Put the city info the map for missing info resolution + } } + locator.resolve(); // Try to fill missing information that include search of next city + // create the city records in alphabetic order + // now loop through the sorted keys and retrieve + // the MapPoint associated with the key - now we + // can create the City object and remember it for later 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); + MapPoint p = (MapPoint)sortedCities.get(s); + String cityName = p.getName(); + + 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); + sortedCities.put(s, 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(); + + 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(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.setCityIndex(city.getIndex()); + + } + + 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); + } + } + lbl.allPOIsDone(); + } /** @@ -449,7 +578,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)sortedCities.get(name + "@" + 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 0) @@ -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 162) +++ 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 162) +++ 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: 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"; } }