
Hello, here is v5 of my patch. Changes since last release are: - Code cleanup - Add option --pois-addresses to switch on the filling of POI address information By default the address information is no longer added to the pois. Also the filling is very conservative now and is not doing to much guessing. I think in most countries this default is useful to minimize the risk of wrong information. All POIs that have incomplete address info are marked with "Fix me address" as street name. In countries with good is_in info coverage I would suggest to use this options: --pois-addresses --location-autofill=1 I think I'm pretty done for now. What do you guys think about integration of my stuff ? Thanks Berni.
here comes version 4 of my patch. I have added a option to control the new feature. By default only reliable address info is used:
--location-autofill=''number'' Controls how country region info is gathered for cities / streets and pois
0 (Default) Info of nearest city is taken. The country region info is gathered by analysis of the cities is_in tags. If no country region info is present the given default country region is used. 1 Additional analysis of partial is_in info to get relations between hamlets and cities 2 Brute force search for nearest city with info if all methods before failed. WARNING: Cities my end up in the wrong country/region. 3 Enables debug output about suspicious relations that might cause wrong country region info
Thanks Berni.
- Reactivated map.setPoiDisplayFlags method to control format of address for POIs. Flag is read from xml config file per country - Road Name POI: Replaced linear search for next city by faster tile based search I developed for Country/Region Autofill
I have tested this stuff so far for the following locations: Paris, Switzerland, Souther Bavaria - Western Austria, North Italy and Cambridge Area UK. Sometimes some cities might end up in the wrong region / country because of missing or wrong is_in tagging.
I think I might be able to improve this by geometrically analyzing the borderlines in the maps but I have to take a closer look into this.
Resources/LocatorConfig.xml contains only information for a bunch of countries. You might want to add a entry to specify variants of country names used in the osm file.
Berni.
- 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 ...
------------------------------------------------------------------------
_______________________________________________ mkgmap-dev mailing list mkgmap-dev@lists.mkgmap.org.uk http://www.mkgmap.org.uk/mailman/listinfo/mkgmap-dev
------------------------------------------------------------------------
_______________________________________________ mkgmap-dev mailing list mkgmap-dev@lists.mkgmap.org.uk http://www.mkgmap.org.uk/mailman/listinfo/mkgmap-dev
Index: src/uk/me/parabola/imgfmt/app/net/RouteNode.java =================================================================== --- src/uk/me/parabola/imgfmt/app/net/RouteNode.java (revision 180) +++ src/uk/me/parabola/imgfmt/app/net/RouteNode.java (working copy) @@ -149,7 +149,7 @@ writer.put((byte) flags); if (haveLargeOffsets()) { - writer.putInt((lonOff << 16) | (latOff & 0xffff)); + writer.putInt((latOff << 16) | (lonOff & 0xffff)); } else { writer.put3((latOff << 12) | (lonOff & 0xfff)); } @@ -204,11 +204,13 @@ private void setLatOff(int latOff) { log.debug("lat off", Integer.toHexString(latOff)); this.latOff = (char) latOff; + checkOffSize(latOff); } private void setLonOff(int lonOff) { log.debug("long off", Integer.toHexString(lonOff)); this.lonOff = (char) lonOff; + checkOffSize(lonOff); } /** Index: src/uk/me/parabola/imgfmt/app/net/RouteArc.java =================================================================== --- src/uk/me/parabola/imgfmt/app/net/RouteArc.java (revision 180) +++ src/uk/me/parabola/imgfmt/app/net/RouteArc.java (working copy) @@ -97,7 +97,11 @@ * Provide an upper bound for the written size in bytes. */ public int boundSize() { - return 6; // XXX: this could be reduced, and may increase + // XXX: this could be reduced, and may increase + // currently: 1 (flagA) + 1-2 (offset) + 1 (indexA) + // + 2 (length) + 1 (initialHeading) + // needs updating when curve data is written + return 7; } /** @@ -136,7 +140,7 @@ * Set this arc's index into Table B. Applies to external arcs only. */ public void setIndexB(byte indexB) { - assert !isInternal() : "Trying to set index on external arc."; + assert !isInternal() : "Trying to set index on internal arc."; this.indexB = indexB; } @@ -199,7 +203,7 @@ writer.put(flagA); if (isInternal()) { - // space for 12 bit node offset, written in writeSecond. + // space for 14 bit node offset, written in writeSecond. writer.put(flagB); writer.put((byte) 0); } else { @@ -234,7 +238,7 @@ char val = (char) (flagB << 8); int diff = dest.getOffsetNod1() - source.getOffsetNod1(); assert diff < 0x2000 && diff >= -0x2000 - : "relative pointer too large for 12 bits"; + : "relative pointer too large for 14 bits"; val |= diff & 0x3fff; // We write this big endian Index: src/uk/me/parabola/imgfmt/app/net/TableB.java =================================================================== --- src/uk/me/parabola/imgfmt/app/net/TableB.java (revision 180) +++ src/uk/me/parabola/imgfmt/app/net/TableB.java (working copy) @@ -35,7 +35,7 @@ /** * Retrieve the size of the Table as an int. * - * While Table A is limited in size (0x40 entries), + * While Table B is limited in size (0x40 entries), * we temporarily build larger tables while subdividing * the network. */ Index: src/uk/me/parabola/imgfmt/app/trergn/TREHeader.java =================================================================== --- src/uk/me/parabola/imgfmt/app/trergn/TREHeader.java (revision 180) +++ src/uk/me/parabola/imgfmt/app/trergn/TREHeader.java (working copy) @@ -236,6 +236,10 @@ public void setMapId(int id) { mapId = id; } + + public void setPoiDisplayFlags(byte poiDisplayFlags) { + this.poiDisplayFlags = poiDisplayFlags; + } public int getMapInfoSize() { return mapInfoSize; Index: src/uk/me/parabola/imgfmt/app/trergn/TREFile.java =================================================================== --- src/uk/me/parabola/imgfmt/app/trergn/TREFile.java (revision 180) +++ src/uk/me/parabola/imgfmt/app/trergn/TREFile.java (working copy) @@ -315,6 +315,10 @@ header.setBounds(area); } + public void setPoiDisplayFlags(byte b) { + header.setPoiDisplayFlags(b); + } + public String[] getCopyrights() { if (!isReadable()) throw new IllegalStateException("not open for reading"); Index: src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java =================================================================== --- src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java (revision 180) +++ 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 180) +++ 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; @@ -33,7 +35,8 @@ public class PlacesFile { private final Map<String, Country> countries = new LinkedHashMap<String, Country>(); private final Map<String, Region> regions = new LinkedHashMap<String, Region>(); - private final List<City> cities = new ArrayList<City>(); + 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) + for (String s : cityList.keySet()) + { + City c = cityList.get(s); c.write(writer); + } + placeHeader.endCity(writer.position()); int poistart = writer.position(); @@ -79,56 +86,122 @@ } 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.add(c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + /* + Adding 0 in between is important to get right sort order !!! + We have to make sure that "Kirchdorf" gets sorted before "Kirchdorf am Inn" + If this order is not correct nuvi would not find right city + */ + + 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.add(c); + Label l = lblFile.newLabel(name); + c.setLabel(l); + + /* + Adding 0 in between is important to get right sort order !!! + We have to make sure that "Kirchdorf" gets sorted before "Kirchdorf am Inn" + If this order is not correct nuvi would not find right city + */ + + 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 +219,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 180) +++ 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 180) +++ 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 180) +++ 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 180) +++ src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java (working copy) @@ -225,9 +225,54 @@ } 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(country != null) + country = element.getTag("addr:country"); + + 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/main/MapMaker.java =================================================================== --- src/uk/me/parabola/mkgmap/main/MapMaker.java (revision 180) +++ src/uk/me/parabola/mkgmap/main/MapMaker.java (working copy) @@ -278,26 +278,10 @@ } String name = road.getName(); - MapPoint nearestCity = null; - if(cities != null) { - double shortestDistance = 10000000; - for(MapPoint mp : cities) { - double distance = coord.distance(mp.getLocation()); - if(distance < shortestDistance) { - shortestDistance = distance; - nearestCity = mp; - } - } - } - MapPoint rnp = new MapPoint(); - if(nearestCity != null && nearestCity.getName() != null) { - //rnp.setNearestCityPoint(nearestCity); - name += "/" + nearestCity.getName(); - } - rnp.setName(name); + rnp.setRoadNamePOI(true); rnp.setType(type); rnp.setLocation(coord); return rnp; Index: src/uk/me/parabola/mkgmap/build/MapPointMultiMap.java =================================================================== --- src/uk/me/parabola/mkgmap/build/MapPointMultiMap.java (revision 0) +++ src/uk/me/parabola/mkgmap/build/MapPointMultiMap.java (revision 0) @@ -0,0 +1,67 @@ +/* + * 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.Vector; +import java.util.HashMap; +import java.util.Collection; +import uk.me.parabola.mkgmap.general.MapPoint; + +class MapPointMultiMap{ + + private final java.util.Map<String,Vector<MapPoint>> map = new HashMap<String,Vector<MapPoint>>(); + + 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); + + 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); + } +} \ No newline at end of file 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,433 @@ +/* + * 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 + * + * The algorithm works like this: + * + * 1. Step: Go through all cities an check if they have useful country region info + * The best case is if the tags is_in:country and is_in:county are present thats easy. + * Some cities have is_in information that can be used. We check for three different + * formats: + * + * County, State, Country, Continent + * County, State, Country + * Continent, Country, State, County, ... + * + * In "openGeoDb countries" this info is pretty reliable since it was imported from a + * external db into osm. Other countries have very sparse is_in info. + * + * All this cities that have such info will end up in "city" list. All which lack + * such information in "location" list. + * + * 2. Step: Go through the "location" list and check if the is_in info has some relations + * to the cities we have info about. + * + * Especially hamlets often have no full is_in information. They only have one entry in + * is_in that points to the city they belong to. I will check if I can find the name + * of this city in the "City" list. If there are more with the same name I use the + * closest one. If we can't find the exact name I use fuzzy name search. Thats a + * workaround for german umlaute since sometimes there are used in the tags and + * sometimes there are written as ue ae oe. + * + * 3. Step: Do the same like in step 2 once again. This is used to support at least + * one level of recursion in is_in relations. + * + * If there is still no info found I use brute force and use the information from the + * next city. Has to be used for countries with poor is_in tagging. + * + * 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 MapPointMultiMap fuzzyCityMap = new MapPointMultiMap(); + private final java.util.Vector<MapPoint> placesMap = new Vector<MapPoint>(); + + private LocatorConfig locConfig = new LocatorConfig(); + + static double totalTime = 0; + static long totalFinds = 0; + private int autoFillLevel = 0; + + public void addLocation(MapPoint p) + { + resolveIsInInfo(p); // Preprocess the is_in field + + if(autoFillLevel > 0) + { + 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); + + if(p.getName() != null && p.getCity().equals(p.getName()) == false) // Name variants ? + fuzzyCityMap.put(fuzzyDecode(p.getName()),p); + } + else + { + // All other places which do not seam to be a real city has to resolved later + placesMap.add(p); + } + } + else + { + if(p.getCity() == null) // Without autofill city name is the name of the tag + p.setCity(p.getName()); + + cityMap.put(p.getCity(),p); + } + } + + + + public void setAutoFillLevel(int level) + { + autoFillLevel = level; + } + + 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); + } + + public int getPOIDispFlag(String country) + { + return locConfig.getPoiDispFlag(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; + + 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 (autoFillLevel > 3) // Some debug output to find suspicios relations + { + + if(near != null && minDist > 30000) + { + System.out.println("Locator: " + place.getName() + " is far away from " + + near.getName() + " " + (minDist/1000.0) + " km is_in" + place.getIsIn()); + if(nextCityList != null) + System.out.println("Number of cities with this name: " + nextCityList.size()); + } + + //if(near != null && fuzzy) + //{ + // System.out.println("Locator: " + place.getName() + " may belong to " + + // near.getName() + " is_in" + place.getIsIn()); + //} + } + } + + return near; + } + + public void resolve() { + + if(autoFillLevel < 0) + return; // Nothing to do if autofill is fulli disabled + + if(autoFillLevel > 2) + { + 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(autoFillLevel > 3 && near == null && (runCount + 1) == maxRuns) + { + if(place.getIsIn() != null) + System.out.println("Locator: CAN't locate " + place.getName() + " is_in " + place.getIsIn()); + } + + + if(near != null) + { + place.setCity(near.getCity()); + place.setZip(near.getZip()); + } + else if (autoFillLevel > 1 && (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(near == null) + unresCount++; + + } + } + + for (int i = 0; i < placesMap.size(); i++) + { + MapPoint place = placesMap.get(i); + + if (place != null) + { + if( place.getCity() != null) + { + cityMap.put(place.getName(),place); + fuzzyCityMap.put(fuzzyDecode(place.getName()),place); + placesMap.set(i, null); + } + else if(autoFillLevel < 2 && (runCount + 1) == maxRuns) + { + place.setCity(place.getName()); + cityMap.put(place.getName(),place); + } + } + } + + runCount++; + + if(autoFillLevel > 2) + 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"); + + //if(decodeString.equals(stringToDecode) == false) + // System.out.println(stringToDecode + " -> " + decodeString); + + return (decodeString); + } + +} + Index: src/uk/me/parabola/mkgmap/build/MapBuilder.java =================================================================== --- src/uk/me/parabola/mkgmap/build/MapBuilder.java (revision 180) +++ 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,26 +85,55 @@ 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, Object> sortedCities = new TreeMap<String, Object>(); + 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; - private String countryName = "UNITED KINGDOM"; - private String countryAbbr = "GBR"; + private String countryName = "COUNTRY"; + private String countryAbbr = "ABC"; private String regionName; private String regionAbbr; + private int locationAutofillLevel = 0; + private boolean poiAddresses = false; + private int poiDisplayFlags = 0; public MapBuilder() { regionName = null; } public void config(EnhancedProperties props) { + + String autoFillPar; + countryName = props.getProperty("country-name", countryName); countryAbbr = props.getProperty("country-abbr", countryAbbr); regionName = props.getProperty("region-name", null); regionAbbr = props.getProperty("region-abbr", null); + + if(props.getProperty("pois-addresses", null) != null) + poiAddresses = true; + + autoFillPar = props.getProperty("location-autofill", null); + + if(autoFillPar != null) + { + try + { + locationAutofillLevel = Integer.parseInt(autoFillPar); + } + catch (Exception e) + { + locationAutofillLevel = 1; + } + } + + locator.setAutoFillLevel(locationAutofillLevel); + + } /** @@ -123,6 +154,7 @@ if(regionName != null) region = lblFile.createRegion(country, regionName, regionAbbr); + processCities(map, src); processPOIs(map, src); //preProcessRoads(map, src); processOverviews(map, src); @@ -157,52 +189,194 @@ } /** - * 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(); - // gpsmapedit doesn't sort the city names so to be - // friendly we generate the city objects in alphabetic - // order - to do that we first build a map from city - // name to the associated MapPoint - we don't want to - // be fooled by duplicate names so suffix the name - // with the object to make it unique + 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() + "@" + p, p); + locator.addLocation(p); // Put the city info the map for missing info } - // 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()) { - MapPoint p = (MapPoint)sortedCities.get(s); - City c; - if(region != null) - c = lbl.createCity(region, p.getName()); - else - c = lbl.createCity(country, p.getName()); - sortedCities.put(s, c); + if(locationAutofillLevel > 0) + 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); + } } - // if point has a nearest city, create a POIRecord to - // reference it + } + + private void processPOIs(Map map, MapDataSource src) { + + LBLFile lbl = map.getLblFile(); + long poiAddrCountr = 0; + boolean checkedForPoiDispFlag = false; + for (MapPoint p : src.getPoints()) { - MapPoint nearestCityPoint = p.getNearestCityPoint(); - if(nearestCityPoint != null && p.getName() != null) { - POIRecord r = lbl.createPOI(p.getName()); - City nearestCity = (City)sortedCities.get(nearestCityPoint.getName() + "@" + nearestCityPoint); - r.setCityIndex(nearestCity.getIndex()); + + if(p.isCity() == false && + (p.isRoadNamePOI() || poiAddresses)) + { + + 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(CountryStr != null && checkedForPoiDispFlag == false) + { + // Different countries require different address notation + + poiDisplayFlags = locator.getPOIDispFlag(CountryStr); + checkedForPoiDispFlag = true; + } + + + if(CityStr != null && p.isRoadNamePOI()) + { + // If it is road POI add city name and street name into address info + p.setStreet(p.getName()); + p.setName(p.getName() + "/" + CityStr); + } + + POIRecord r = lbl.createPOI(p.getName()); + + 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("FIX MY 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(); + } /** @@ -369,6 +543,9 @@ // The bounds of the map. map.setBounds(src.getBounds()); + if(poiDisplayFlags != 0) // POI requested alterate address notation + map.setPoiDisplayFlags(poiDisplayFlags); + // You can add anything here. // But there has to be something, otherwise the map does not show up. // @@ -461,7 +638,8 @@ // retrieve the City created earlier for // this point and store the point info // in it - City c = (City)sortedCities.get(name + "@" + point); + 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"); } else { 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/build/LocatorConfig.java =================================================================== --- src/uk/me/parabola/mkgmap/build/LocatorConfig.java (revision 0) +++ src/uk/me/parabola/mkgmap/build/LocatorConfig.java (revision 0) @@ -0,0 +1,290 @@ +/* + * 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 org.w3c.dom.*; +import org.xml.sax.*; +import javax.xml.parsers.*; +import java.io.*; + +import java.util.HashMap; + +public class LocatorConfig { + + private final java.util.Map<String,String> variantMap = new HashMap<String,String>(); + private final java.util.Map<String,String> abrMap = new HashMap<String,String>(); + private final java.util.Map<String,Boolean> geoDbMap = new HashMap<String,Boolean>(); + private final java.util.Map<String,Integer> regOffsetMap = new HashMap<String,Integer>(); + private final java.util.Map<String,Integer> poiDispFlagMap = new HashMap<String,Integer>(); + private final java.util.Map<String,Boolean> continentMap = new HashMap<String,Boolean>(); + + + public LocatorConfig() + { + loadConfig("resources/LocatorConfig.xml"); + } + + private void loadConfig(String fileName) + { + try + { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + + Document document = builder.parse(fileName); + + Node rootNode = document.getDocumentElement(); + + if(rootNode.getNodeName().equals("locator")) + { + Node cNode = rootNode.getFirstChild(); + + while(cNode != null) + { + if(cNode.getNodeName().equals("continent")) + { + NamedNodeMap attr = cNode.getAttributes(); + Node nameTag = null; + + if(attr != null) + { + nameTag = attr.getNamedItem("name"); + if(nameTag != null) + addContinent(nameTag.getNodeValue()); + } + + } + + if(cNode.getNodeName().equals("country")) + { + NamedNodeMap attr = cNode.getAttributes(); + Node nameTag = null; + + if(attr != null) + { + nameTag = attr.getNamedItem("name"); + + Node abrTag = attr.getNamedItem("abr"); + + if(abrTag != null && nameTag != null) + addAbr(nameTag.getNodeValue(),abrTag.getNodeValue()); + + if(abrTag == null && nameTag != null) + addAbr(nameTag.getNodeValue(),""); + + Node geoTag = attr.getNamedItem("geodb"); + + if(nameTag != null && geoTag != null) + { + if(geoTag.getNodeValue().equals("1")) + addOpenGeoDb(nameTag.getNodeValue()); + } + + Node regionOffsetTag = attr.getNamedItem("regionOffset"); + + if(regionOffsetTag != null && nameTag != null) + { + addRegionOffset(nameTag.getNodeValue(),Integer.parseInt(regionOffsetTag.getNodeValue())); + } + + Node poiDispTag = attr.getNamedItem("poiDispFlag"); + + if(poiDispTag != null && nameTag != null) + { + addPoiDispTag(nameTag.getNodeValue(),Integer.decode(poiDispTag.getNodeValue())); + } + } + + Node cEntryNode = cNode.getFirstChild(); + while(cEntryNode != null) + { + if(cEntryNode.getNodeName().equals("variant")) + { + Node nodeText = cEntryNode.getFirstChild(); + + if(nodeText != null && nameTag != null) + addVariant(nameTag.getNodeValue(), nodeText.getNodeValue()); + + } + cEntryNode = cEntryNode.getNextSibling(); + } + } + + cNode = cNode.getNextSibling(); + } + } + else + { + System.out.println(fileName + "contains invalid root tag " + rootNode.getNodeName()); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + //System.out.println("Something is wrong here"); + } + return; + } + + private void addVariant(String country, String variant) + { + String cStr = country.toUpperCase().trim(); + String vStr = variant.toUpperCase().trim(); + + //System.out.println(vStr + " -> " + cStr); + + variantMap.put(vStr,cStr); + } + + private void addAbr(String country, String abr) + { + String cStr = country.toUpperCase().trim(); + String aStr = abr.toUpperCase().trim(); + + //System.out.println(cStr + " -> " + aStr); + + abrMap.put(cStr,aStr); + } + + private void addRegionOffset(String country, Integer offset) + { + String cStr = country.toUpperCase().trim(); + + //System.out.println(cStr + " -> " + offset); + + regOffsetMap.put(cStr,offset); + } + + private void addPoiDispTag(String country, Integer flag) + { + String cStr = country.toUpperCase().trim(); + + //System.out.println(cStr + " -> " + flag); + + poiDispFlagMap.put(cStr,flag); + } + + private void addOpenGeoDb(String country) + { + String cStr = country.toUpperCase().trim(); + + //System.out.println(cStr + " openGeoDb"); + + geoDbMap.put(cStr,true); + + } + + private void addContinent(String continent) + { + String cStr = continent.toUpperCase().trim(); + + //System.out.println(cStr + " continent"); + + continentMap.put(cStr,true); + + } + + + public void setDefaultCountry(String country, String abbr) + { + addAbr(country, abbr); + } + + public String fixCountryString(String country) + { + String cStr = country.toUpperCase().trim(); + + String fixedString = variantMap.get(cStr); + + if(fixedString != null) + return fixedString; + else + return(cStr); + } + + public String isCountry(String country) + { + String cStr = fixCountryString(country); + + if(getCountryCode(cStr) != null) + return cStr; + else + return null; + + } + + public String getCountryCode(String country) + { + String cStr = country.toUpperCase().trim(); + return abrMap.get(cStr); + } + + public int getRegionOffset(String country) + { + String cStr = country.toUpperCase().trim(); + + Integer regOffset = regOffsetMap.get(cStr); + + if(regOffset != null) + return regOffset; + else + return 1; // Default is 1 the next string after before country + } + + public int getPoiDispFlag(String country) + { + String cStr = country.toUpperCase().trim(); + + Integer flag = poiDispFlagMap.get(cStr); + + if(flag != null) + return flag; + else + return 0; // Default is 1 the next string after before country + } + + public 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().trim(); + + if(geoDbMap.get(cStr) != null) + return true; + + return false; + } + + public boolean isContinent(String continent) + { + String s = continent.toUpperCase().trim(); + + if(continentMap.get(s) != null) + return(true); + + return false; + } + + + + +} + Index: src/uk/me/parabola/mkgmap/reader/polish/PolishMapDataSource.java =================================================================== --- src/uk/me/parabola/mkgmap/reader/polish/PolishMapDataSource.java (revision 180) +++ 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 180) +++ 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: src/uk/me/parabola/mkgmap/general/MapPoint.java =================================================================== --- src/uk/me/parabola/mkgmap/general/MapPoint.java (revision 180) +++ src/uk/me/parabola/mkgmap/general/MapPoint.java (working copy) @@ -27,6 +27,7 @@ public class MapPoint extends MapElement { private Coord location; private MapPoint nearestCityPoint; + private boolean isRoadNamePoi = false; public MapPoint() { } @@ -63,11 +64,11 @@ return type >= 0x0100 && type <= 0x1100; } - public void setNearestCityPoint(MapPoint nearestCityPoint) { - this.nearestCityPoint = nearestCityPoint; + public void setRoadNamePOI(boolean isRoadNamePoi) { + this.isRoadNamePoi = isRoadNamePoi; } - public MapPoint getNearestCityPoint() { - return nearestCityPoint; + public boolean isRoadNamePOI() { + return this.isRoadNamePoi; } } Index: resources/help/en/options =================================================================== --- resources/help/en/options (revision 180) +++ resources/help/en/options (working copy) @@ -113,6 +113,11 @@ --ignore-osm-bounds When reading OSM files, ignore any "bounds" elements. +--road-name-pois[=GarminCode] + Generate a POI for each named road. By default, the POIs' + Garmin type code is 0x640a. If desired, a different type code + can be specified with this option. + --tdbfile Write a .tdb file. @@ -126,6 +131,22 @@ same area, you can see through this map and see the lower map too. Useful for contour line maps among other things. +--pois-addresses + Add address / phone information to POIs. Address info is read according to + the "Karlsruhe" tagging schema. If no address info is given name of next city + is used. Automatic filling could be extended using the "location-autofill" option. + +--location-autofill=''number'' + Controls how country region info is gathered for cities / streets and pois + + 0 (Default) Info of nearest city is taken. If no country region info is present + for next city the default passed default country region is used. The country region + info is gathered by analysis of the cities is_in tags. + 1 Additional analysis of partial is_in info to get relations between hamlets and cities + 2 Brute force search for nearest city with info if all methods before failed. Warning + cities my end up in the wrong country/region. + 3 Enables debug output about suspicious relations that might cause wrong country region info + --version Output program version. Index: resources/LocatorConfig.xml =================================================================== --- resources/LocatorConfig.xml (revision 0) +++ resources/LocatorConfig.xml (revision 0) @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<locator> + <country name="Deutschland" abr="DEU" geodb="1" regionOffset="3" poiDispFlag="0xc"> + <variant>Bundesrepublik Deutschland</variant> + <variant>Germany</variant> + <variant>DE</variant> + </country> + <country name="Österreich" abr="AUT" geodb="1" poiDispFlag="0xc"> + <variant>Austria</variant> + <variant>AT</variant> + </country> + <country name="Schweiz" abr="CHE" geodb="1" poiDispFlag="0xc"> + <variant>Switzerland</variant> + <variant>CH</variant> + </country> + <country name="United Kingdom" abr="GBR"> + <variant>UK</variant> + <variant>GB</variant> + </country> + <country name="Italia" abr="ITA" regionOffset="2"> + <variant>Italy</variant> + <variant>IT</variant> + </country> + <country name="France" abr="FRA"> + </country> + <continent name="Europe"> + </continent> + <continent name="Africa"> + </continent> + <continent name="Asia"> + </continent> + <continent name="North America"> + </continent> + <continent name="South America"> + </continent> + <continent name="Oceania"> + </continent> + +</locator> \ No newline at end of file Index: osm2mp.pl =================================================================== --- osm2mp.pl (revision 180) +++ osm2mp.pl (working copy) @@ -228,7 +228,8 @@ my $id; my $latlon; -my ($poi, $poiname); +my ($poi, $poiname, $ZipCode, $CityName, $StreetDesc, $HouseNumber, $isIn, $phone); +my ($CountryName, $RegionName); my $nameprio = 99; while (<IN>) { @@ -242,18 +243,41 @@ $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"); + $CountryName = convert_string($2) if ( "$1" eq "addr: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: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; } @@ -270,6 +294,14 @@ 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"; } }