package de.blau.android.propertyeditor; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Set; import java.util.TreeMap; import android.content.Context; import android.util.Log; import de.blau.android.App; import de.blau.android.Logic; import de.blau.android.exception.OsmException; import de.blau.android.osm.BoundingBox; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.Relation; import de.blau.android.osm.StorageDelegator; import de.blau.android.osm.Tags; import de.blau.android.osm.Way; import de.blau.android.prefs.Preferences; import de.blau.android.util.ElementSearch; import de.blau.android.util.GeoMath; import de.blau.android.util.SavingHelper; import de.blau.android.util.StreetTagValueAdapter; import de.blau.android.util.Util; /** * Store coordinates and address information for use in address prediction * @author simon * */ public class Address implements Serializable { private static final long serialVersionUID = 5L; private static final String DEBUG_TAG = Address.class.getSimpleName(); public static final int NO_HYSTERESIS = 0; public static final int DEFAULT_HYSTERESIS = 2; private static final String ADDRESS_TAGS_FILE = "addresstags.dat"; private static final int MAX_SAVED_ADDRESSES = 100; private static SavingHelper<LinkedList<Address>> savingHelperAddress = new SavingHelper<LinkedList<Address>>(); public enum Side { LEFT, RIGHT, UNKNOWN } private Side side = Side.UNKNOWN; private float lat; private float lon; private LinkedHashMap<String, ArrayList<String>> tags; private static LinkedList<Address> lastAddresses = null; /** * Create a copy of Address a * @param a */ private Address(Address a) { side = a.side; lat = a.lat; lon = a.lon; tags = new LinkedHashMap<String, ArrayList<String>>(a.tags); } /** * Create empty address object */ private Address() { tags = new LinkedHashMap<String, ArrayList<String>>(); } /** * Create an address object from an OSM element * @param type type of element * @param id its ID * @param tags the relevant address tags */ private Address(String type, long id, LinkedHashMap<String, ArrayList<String>> tags) { OsmElement e = App.getDelegator().getOsmElement(type, id); if (e == null) { Log.e(DEBUG_TAG,type + " " + id + " doesn't exist in storage "); //FIXME is might make sense to create a crash dump here return; } init(e,tags); } /** * Create an address object from an OSM element * @param e the OSM element * @param tags the relevant address tags */ private Address(OsmElement e, LinkedHashMap<String, ArrayList<String>> tags) { init(e,tags); } /** * Initialize an address object from an OSM element * @param e the OSM element * @param tags the relevant address tags */ private void init(OsmElement e, LinkedHashMap<String, ArrayList<String>> tags) { switch (e.getType()) { case NODE: lat = ((Node)e).getLat()/1E7F; lon = ((Node)e).getLon()/1E7F; break; case WAY: case CLOSEDWAY: case AREA: if (Way.NAME.equals(e.getName())) { double[] center = Logic.centroidLonLat((Way)e); if (center != null) { lat = (float) center[1]; lon = (float) center[0]; } } else { // MP and maybe one day an area type } break; case RELATION: // doing nothing is probably best for now default: break; } this.tags = new LinkedHashMap<String, ArrayList<String>>(tags); } /** * Set which side of the road this address is on * @param wayId * @return */ private void setSide(long wayId) { side = Side.UNKNOWN; Way w = (Way)App.getDelegator().getOsmElement(Way.NAME,wayId); if (w == null) { return; } double distance = Double.MAX_VALUE; // to avoid rounding errors we translate the bb to 0,0 BoundingBox bb = w.getBounds(); double latOffset = GeoMath.latE7ToMercatorE7(bb.getBottom()); double lonOffset = bb.getLeft(); double ny = GeoMath.latToMercator(lat)-latOffset/1E7D; double nx = lon - lonOffset/1E7D; ArrayList<Node> nodes = new ArrayList<Node>(w.getNodes()); for (int i = 0;i <= nodes.size()-2;i++) { double bx = (nodes.get(i).getLon()-lonOffset)/1E7D; double by = (GeoMath.latE7ToMercatorE7(nodes.get(i).getLat())-latOffset )/1E7D; double ax = (nodes.get(i+1).getLon()-lonOffset)/1E7D; double ay = (GeoMath.latE7ToMercatorE7(nodes.get(i+1).getLat())-latOffset)/1E7D; float[] closest = GeoMath.closestPoint((float)nx, (float)ny, (float)bx, (float)by, (float)ax, (float)ay); double newDistance = GeoMath.haversineDistance(nx, ny, closest[0], closest[1]); if (newDistance < distance) { distance = newDistance; double determinant = (bx-ax)*(ny-ay) - (by-ay)*(nx-ax); if (determinant < 0) { side = Side.LEFT; } else if (determinant > 0) { side = Side.RIGHT; } } } // Log.d(DEBUG_TAG,"set side to " + side); } private Side getSide() { return side; } /** * Predict address tags * This uses a file to cache/save the address information over invocations of the TagEditor, if the cache doesn't have entries for a specific street/place * an attempt to extract the information from the downloaded data is made * * @param elementType * @param elementOsmId * @param es * @param current * @param maxRank determines how far away from the nearest street the last address street can be, 0 will always use the nearest, higher numbers will provide some hysteresis * @return */ public synchronized static LinkedHashMap<String,ArrayList<String>> predictAddressTags(Context context, final String elementType, final long elementOsmId, final ElementSearch es, final LinkedHashMap<String, ArrayList<String>> current, int maxRank) { Address newAddress = null; loadLastAddresses(context); if (lastAddresses != null && lastAddresses.size() > 0) { newAddress = new Address(elementType, elementOsmId,lastAddresses.get(0).tags); // last address we added if (newAddress.tags.containsKey(Tags.KEY_ADDR_HOUSENUMBER)) { newAddress.tags.put(Tags.KEY_ADDR_HOUSENUMBER, Util.getArrayList("")); } Log.d("Address","seeding with last addresses"); } if (newAddress == null) { // make sure we have the address object newAddress = new Address(elementType, elementOsmId, new LinkedHashMap<String, ArrayList<String>>()); Log.d("Address","nothing to seed with, creating new"); } // merge in any existing tags for (String k: current.keySet()) { Log.d("Address","Adding in existing tag " + k); newAddress.tags.put(k, current.get(k)); } boolean hasPlace = newAddress.tags.containsKey(Tags.KEY_ADDR_PLACE); boolean hasNumber = current.containsKey(Tags.KEY_ADDR_HOUSENUMBER); // if the object already had a number don't overwrite it StorageDelegator storageDelegator = App.getDelegator(); if (es != null /* || hasPlace */) { // the arrays should now be calculated, retrieve street names if any ArrayList<String> streetNames = new ArrayList<String>(Arrays.asList(es.getStreetNames())); // ArrayList<String> placeNames = new ArrayList<String>(Arrays.asList(placeAdapter.getNames())); if ((streetNames != null && streetNames.size() > 0) || hasPlace) { LinkedHashMap<String, ArrayList<String>> tags = newAddress.tags; Log.d(DEBUG_TAG,"tags.get(Tags.KEY_ADDR_STREET)) " + tags.get(Tags.KEY_ADDR_STREET)); // Log.d("TagEditor","Rank of " + tags.get(Tags.KEY_ADDR_STREET) + " " + streetNames.indexOf(tags.get(Tags.KEY_ADDR_STREET))); String street; if (!hasPlace) { ArrayList<String> addrStreetValues = tags.get(Tags.KEY_ADDR_STREET); int rank = -1; boolean hasAddrStreet = addrStreetValues != null && addrStreetValues.size() > 0 && !addrStreetValues.get(0).equals(""); if (hasAddrStreet) { rank = streetNames.indexOf(addrStreetValues.get(0)); // FIXME this and the following could consider other values in multi select } Log.d(DEBUG_TAG, (hasAddrStreet ? "rank " + rank + " for " + addrStreetValues.get(0) : "no addrStreet tag")); if (!hasAddrStreet || rank > maxRank || rank < 0) { // check if has street and still in the top 3 nearest // Log.d("TagEditor","names.indexOf(tags.get(Tags.KEY_ADDR_STREET)) " + streetNames.indexOf(tags.get(Tags.KEY_ADDR_STREET))); // nope -> zap tags.put(Tags.KEY_ADDR_STREET, Util.getArrayList(streetNames.get(0))); } addrStreetValues = tags.get(Tags.KEY_ADDR_STREET); if (addrStreetValues != null && addrStreetValues.size() > 0) { street = tags.get(Tags.KEY_ADDR_STREET).get(0); // should now have the final suggestion for a street } else { street = ""; // FIXME } try { newAddress.setSide(es.getStreetId(street)); } catch (OsmException e) { // street not in adapter newAddress.side = Side.UNKNOWN; } } else { // ADDR_PLACE minimal support, don't overwrite with street ArrayList<String> addrPlaceValues = tags.get(Tags.KEY_ADDR_PLACE); if (addrPlaceValues != null && addrPlaceValues.size() > 0) { street = tags.get(Tags.KEY_ADDR_PLACE).get(0); } else { street = ""; // FIXME } newAddress.side = Side.UNKNOWN; } Log.d(DEBUG_TAG,"side " + newAddress.getSide()); Side side = newAddress.getSide(); // find the addresses corresponding to the current street if (!hasNumber && street != null && lastAddresses != null) { TreeMap<Integer,Address> list = getHouseNumbers(street, side, lastAddresses); if (list.size() == 0) { // try to seed lastAddresses from OSM data try { Log.d(DEBUG_TAG,"street " + street); long streetId = -1; if (!hasPlace) { streetId = es.getStreetId(street); } // nodes for (Node n: storageDelegator.getCurrentStorage().getNodes()) { seedAddressList(context, street,streetId, n,lastAddresses); } // ways for (Way w: storageDelegator.getCurrentStorage().getWays()) { seedAddressList(context, street,streetId, w,lastAddresses); } // and try again list = getHouseNumbers(street, side, lastAddresses); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // try to predict the next number // // - get all existing numbers for the side of the street we are on // - determine if the increment per number is 1 or 2 (for now everything else is ignored) // - determine the nearest address node // - if it is the last or first node and we are at one side use that and add or subtract the increment // - if the nearest node is somewhere in the middle determine on which side of it we are, // - inc/dec in that direction // If everything works out correctly even if a prediction is wrong, entering the correct number should improve the next prediction // TODO the above assumes that the road is not doubling back or similar, aka that the addresses are more or less in a straight line, // use the length along the way defined by the addresses instead // if (list.size() >= 2) { try { int firstNumber = list.firstKey(); int lastNumber = list.lastKey(); // // determine increment // int inc = 1; float incTotal = 0; float incCount = 0; ArrayList<Integer> numbers = new ArrayList<Integer>(list.keySet()); for (int i=0;i<numbers.size()-1;i++) { int diff = numbers.get(i+1).intValue()-numbers.get(i).intValue(); if (diff > 0 && diff <= 2) { incTotal = incTotal + diff; incCount++; } } inc = Math.round(incTotal/incCount); // // find the most appropriate next address // int nearest = -1; int prev = -1; int post = -1; double distanceFirst = 0; double distanceLast = 0; double distance = Double.MAX_VALUE; for (int i=0;i<numbers.size();i++) { // determine the nearest existing address // FIXME there is an obvious better criteria int number = Integer.valueOf(numbers.get(i)); Address a = list.get(number); double newDistance = GeoMath.haversineDistance(newAddress.lon, newAddress.lat, a.lon, a.lat); if (newDistance <= distance) { // if distance is the same replace with values for the // current number which will be larger distance = newDistance; nearest = number; prev = numbers.get(Math.max(0, i-1)); post = numbers.get(Math.min(numbers.size()-1, i+1)); } if (i==0) { distanceFirst = newDistance; } else if (i==numbers.size()-1) { distanceLast = newDistance; } } // double distanceTotal = GeoMath.haversineDistance(list.get(firstNumber).lon, list.get(firstNumber).lat, list.get(lastNumber).lon, list.get(lastNumber).lat); if (nearest == firstNumber) { if (distanceLast > distanceTotal) { inc = -inc; } } else if (nearest == lastNumber) { if (distanceFirst < distanceTotal) { inc = -inc; } } else { double distanceNearestFirst = GeoMath.haversineDistance(list.get(firstNumber).lon, list.get(firstNumber).lat, list.get(nearest).lon, list.get(nearest).lat); if (distanceFirst < distanceNearestFirst) { inc = -inc; } // else already correct } // Toast.makeText(this, "First " + firstNumber + " last " + lastNumber + " nearest " + nearest + "inc " + inc + " prev " + prev + " post " + post + " side " + side, Toast.LENGTH_LONG).show(); Log.d("TagEditor","First " + firstNumber + " last " + lastNumber + " nearest " + nearest + " inc " + inc + " prev " + prev + " post " + post + " side " + side); // first apply tags from nearest address if they don't already exist for (String key:list.get(nearest).tags.keySet()) { if (!tags.containsKey(key)) { tags.put(key,list.get(nearest).tags.get(key)); } } int newNumber = Math.max(1, nearest+inc); if (numbers.contains(Integer.valueOf(newNumber))) { // try one inc more and one less, if they both fail use the original number if (!numbers.contains(Integer.valueOf(Math.max(1,newNumber+inc)))) { newNumber = Math.max(1,newNumber+inc); } else if (!numbers.contains(Integer.valueOf(Math.max(1,newNumber-inc)))) { newNumber = Math.max(1,newNumber-inc); } } tags.put(Tags.KEY_ADDR_HOUSENUMBER, Util.getArrayList("" + newNumber)); } catch (NumberFormatException nfe){ tags.put(Tags.KEY_ADDR_HOUSENUMBER, Util.getArrayList("")); } } else if (list.size() == 1) { // can't do prediction with only one value // apply tags from sole existing address if they don't already exist for (String key:list.get(list.firstKey()).tags.keySet()) { if (!tags.containsKey(key)) { tags.put(key,list.get(list.firstKey()).tags.get(key)); } } } else if (list.size() == 0) { tags.put(Tags.KEY_ADDR_HOUSENUMBER, Util.getArrayList("")); // NOTE this could be the first address on this side of the road and we could // potentially use the house numbers from the opposite side for prediction } } } else { // last ditch attemot // fill with Karlsruher schema Preferences prefs = new Preferences(context); Set<String> addressTags = prefs.addressTags(); for (String key:addressTags) { newAddress.tags.put(key, Util.getArrayList("")); } } } // is this a node on a building outline, if yes add entrance=yes if it doesn't already exist if (elementType.equals(Node.NAME)) { boolean isOnBuilding = false; // we can't call wayForNodes here because Logic may not be around for (Way w: storageDelegator.getCurrentStorage().getWays((Node) storageDelegator.getOsmElement(Node.NAME, elementOsmId))) { if (w.hasTagKey(Tags.KEY_BUILDING)) { isOnBuilding = true; } else if (w.getParentRelations() != null) { // need to check relations too for (Relation r:w.getParentRelations()) { if (r.hasTagKey(Tags.KEY_BUILDING) || r.hasTag(Tags.KEY_TYPE, Tags.VALUE_BUILDING)) { isOnBuilding = true; break; } } } if (isOnBuilding) { break; } } if (isOnBuilding && !newAddress.tags.containsKey(Tags.KEY_ENTRANCE)) { newAddress.tags.put(Tags.KEY_ENTRANCE, Util.getArrayList("yes")); } } return newAddress.tags; } private static int getNumber(String hn) throws NumberFormatException { StringBuffer sb = new StringBuffer(); for (Character c:hn.toCharArray()) { if (Character.isDigit(c)) { sb.append(c); } } if (sb.toString().equals("")) { return 0; } else { return Integer.parseInt(sb.toString()); } } /** * Return a sorted list of house numbers and the associated address objects * @param street * @param side * @param lastAddresses * @return */ private synchronized static TreeMap<Integer,Address> getHouseNumbers(String street, Address.Side side, LinkedList<Address> lastAddresses ) { TreeMap<Integer,Address> result = new TreeMap<Integer,Address>(); //list sorted by house numbers for (Address a:lastAddresses) { if (a != null && a.tags != null) { ArrayList<String> addrStreetValues = a.tags.get(Tags.KEY_ADDR_STREET); ArrayList<String> addrPlaceValues = a.tags.get(Tags.KEY_ADDR_PLACE); if ( ((addrStreetValues != null && addrStreetValues.size() > 0 && addrStreetValues.get(0).equals(street)) // FIXME || (addrPlaceValues != null && addrPlaceValues.size() > 0 && addrPlaceValues.get(0).equals(street))) && a.tags.containsKey(Tags.KEY_ADDR_HOUSENUMBER) && a.getSide() == side) { Log.d(DEBUG_TAG,"Number " + a.tags.get(Tags.KEY_ADDR_HOUSENUMBER)); ArrayList<String> addrHousenumberValues = a.tags.get(Tags.KEY_ADDR_HOUSENUMBER); if ( addrHousenumberValues != null && addrHousenumberValues.size()>0) { String[] numbers = addrHousenumberValues.get(0).split("[\\,;\\-]"); for (String n:numbers) { Log.d(DEBUG_TAG,"add number " + n); //noinspection EmptyCatchBlock try { result.put(Integer.valueOf(getNumber(n)),a); } catch (NumberFormatException nfe){ } } } } } } return result; } /** * Add an address from OSM data to the address cache * @param street * @param streetId * @param e * @param addresses */ private static void seedAddressList(Context context, String street,long streetId, OsmElement e,LinkedList<Address> addresses) { if (e.hasTag(Tags.KEY_ADDR_STREET, street) && e.hasTagKey(Tags.KEY_ADDR_HOUSENUMBER)) { Address seed = new Address(e,getAddressTags(context, new LinkedHashMap<String,ArrayList<String>>(Util.getArrayListMap(e.getTags())))); if (streetId > 0) { seed.setSide(streetId); } if (addresses.size() >= MAX_SAVED_ADDRESSES) { //arbitrary limit for now addresses.removeLast(); } addresses.addFirst(seed); Log.d("TagEditor","seedAddressList added " + seed.tags.toString()); } } private static LinkedHashMap<String,ArrayList<String>> getAddressTags(Context context, LinkedHashMap<String, ArrayList<String>> sortedMap) { LinkedHashMap<String,ArrayList<String>> result = new LinkedHashMap<String,ArrayList<String>>(); Preferences prefs = new Preferences(context); Set<String> addressTags = prefs.addressTags(); for (String key:sortedMap.keySet()) { // include everything except interpolation related tags if (addressTags.contains(key)) { result.put(key, sortedMap.get(key)); } } return result; } synchronized static void resetLastAddresses(Context context) { savingHelperAddress.save(context, ADDRESS_TAGS_FILE, new LinkedList<Address>(), false); lastAddresses = null; } synchronized static void updateLastAddresses(TagEditorFragment caller, LinkedHashMap<String, ArrayList<String>> tags) { // save any address tags for "last address tags" LinkedHashMap<String,ArrayList<String>> addressTags = getAddressTags(caller.getContext(),tags); // this needs to be done after the edit again in case the street name of what ever has changed if (addressTags.size() > 0) { if (lastAddresses == null) { lastAddresses = new LinkedList<Address>(); } if (lastAddresses.size() >= MAX_SAVED_ADDRESSES) { //arbitrary limit for now lastAddresses.removeLast(); } Address current = new Address(caller.getType(), caller.getOsmId(), addressTags); StreetTagValueAdapter streetAdapter = (StreetTagValueAdapter)((NameAdapters)caller.getActivity()).getStreetNameAdapter(null); if (streetAdapter!= null) { ArrayList<String> values = tags.get(Tags.KEY_ADDR_STREET); if (values != null && values.size() > 0) { String streetName = values.get(0); // FIXME can't remember what this is supposed to do.... if (streetName != null) { try { current.setSide(streetAdapter.getId(streetName)); } catch (OsmException e) { current.side = Side.UNKNOWN; } } } } lastAddresses.addFirst(current); savingHelperAddress.save(caller.getActivity(),ADDRESS_TAGS_FILE, lastAddresses, false); } } synchronized static void saveLastAddresses(Context context) { if (lastAddresses != null) { savingHelperAddress.save(context, ADDRESS_TAGS_FILE, lastAddresses, false); } } synchronized static void loadLastAddresses(Context context) { if (lastAddresses == null) { try { lastAddresses = savingHelperAddress.load(context, ADDRESS_TAGS_FILE, false); Log.d("TagEditor","onResume read " + lastAddresses.size() + " addresses"); } catch (Exception e) { //TODO be more specific } } } }