// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.fixAddresses; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.Collection; import java.util.HashMap; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.plugins.fixAddresses.gui.actions.AddressActions; import org.openstreetmap.josm.tools.CheckParameterUtil; /** * The class OSMAddress represents a single address node of OSM. It is a * lightweight wrapper for a OSM node in order to simplify tag handling. */ public class OSMAddress extends OSMEntityBase { public static final String MISSING_TAG = "?"; public static final String INTERPOLATION_TAG = "x..y"; /** True, if address is part of an address interpolation. */ private boolean isPartOfInterpolation; /** * True, if address has derived values from an associated street relation. */ private boolean isPartOfAssocStreetRel; /** The dictionary containing guessed values. */ private HashMap<String, String> guessedValues = new HashMap<>(); /** The dictionary containing guessed objects. */ private HashMap<String, OsmPrimitive> guessedObjects = new HashMap<>(); /** The dictionary containing indirect values. */ private HashMap<String, String> derivedValues = new HashMap<>(); public OSMAddress(OsmPrimitive osmObject) { super(osmObject); } @Override public void setOsmObject(OsmPrimitive osmObject) { super.setOsmObject(osmObject); isPartOfInterpolation = OsmUtils.getValuesFromAddressInterpolation(this); isPartOfAssocStreetRel = OsmUtils.getValuesFromRelation(this); } /** * Checks if the underlying address node has all tags usually needed to * describe an address. * * @return {@code true} if the underlying address node has all tags usually needed to describe an address */ public boolean isComplete() { boolean isComplete = hasCity() && hasHouseNumber() && hasCity() && hasStreetName(); // Check, if "addr:state" is required (US and AU) if (TagUtils.isStateRequired()) { isComplete = isComplete && hasState(); } // Check, if user checked "ignore post code" if (!FixAddressesPlugin.getPreferences().isIgnorePostCode()) { isComplete = isComplete && hasPostalCode() && PostalCodeChecker.hasValidPostalCode(this); } return isComplete; } /** * Gets the name of the street associated with this address. * * @return the name of the street associated with this address */ public String getStreetName() { return getTagValueWithGuess(TagConstants.ADDR_STREET_TAG); } /** * Gets the tag value with guess. If the object does not have the given tag, * this method looks for an appropriate guess. If both, real value and * guess, are missing, a question mark is returned. * * @param tag * the tag * @return the tag value with guess */ private String getTagValueWithGuess(String tag) { if (StringUtils.isNullOrEmpty(tag)) return MISSING_TAG; if (osmObject == null) return MISSING_TAG; if (!osmObject.hasKey(tag) || StringUtils.isNullOrEmpty(osmObject.get(tag))) { if (!hasDerivedValue(tag)) { // object does not have this tag -> check for guess if (hasGuessedValue(tag)) { return "*" + getGuessedValue(tag); } else { // give up return MISSING_TAG; } } else { // ok, use derived value known via associated relation or // way return getDerivedValue(tag); } } else { // get existing tag value return osmObject.get(tag); } } /** * Returns <tt>true</tt>, if this address node has a street name. * * @return <tt>true</tt>, if this address node has a street name */ public boolean hasStreetName() { return hasTag(TagConstants.ADDR_STREET_TAG) || isPartOfRelation(); } /** * Returns the street name guessed by the nearest-neighbor search. * * @return the guessedStreetName */ public String getGuessedStreetName() { return getGuessedValue(TagConstants.ADDR_STREET_TAG); } /** * Sets the guessed street name. * * @param guessedStreetName * the guessedStreetName to set * @param srcObj * the source object of the guess. */ public void setGuessedStreetName(String guessedStreetName, OsmPrimitive srcObj) { setGuessedValue(TagConstants.ADDR_STREET_TAG, guessedStreetName, srcObj); } /** * Checks for a guessed street name. * * @return true, if this instance has a guessed street name. */ public boolean hasGuessedStreetName() { return hasGuessedValue(TagConstants.ADDR_STREET_TAG); } /** * @return the guessedPostCode */ public String getGuessedPostalCode() { return getGuessedValue(TagConstants.ADDR_POSTCODE_TAG); } /** * Sets the guessed post code. * * @param guessedPostCode * the guessedPostCode to set * @param srcObj * srcObj the source object of the guess */ public void setGuessedPostalCode(String guessedPostCode, OsmPrimitive srcObj) { setGuessedValue(TagConstants.ADDR_POSTCODE_TAG, guessedPostCode, srcObj); } /** * Checks for a guessed post code. * * @return true, if this instance has a guessed post code. */ public boolean hasGuessedPostalCode() { return hasGuessedValue(TagConstants.ADDR_POSTCODE_TAG); } /** * @return the guessedCity */ public String getGuessedCity() { return getGuessedValue(TagConstants.ADDR_CITY_TAG); } /** * Sets the guessed city. * * @param guessedCity * the guessedCity to set * @param srcObj * the source object of the guess */ public void setGuessedCity(String guessedCity, OsmPrimitive srcObj) { setGuessedValue(TagConstants.ADDR_CITY_TAG, guessedCity, srcObj); } /** * Checks for a guessed city name. * * @return true, if this instance has a guessed city name. */ public boolean hasGuessedCity() { return hasGuessedValue(TagConstants.ADDR_CITY_TAG); } /** * Returns true, if this instance has guesses regarding address tags. * * @return true, if this instance has guesses regarding address tags */ public boolean hasGuesses() { return guessedValues.size() > 0; } /** * Applies all guessed tags for this node. */ public void applyAllGuesses() { for (String tag : guessedValues.keySet()) { applyGuessForTag(tag); } // Clear all guesses guessedValues.clear(); guessedObjects.clear(); } /** * Apply the guessed value for the given tag. * * @param tag * the tag to apply the guessed value for. */ public void applyGuessForTag(String tag) { if (guessedValues.containsKey(tag)) { String val = guessedValues.get(tag); if (!StringUtils.isNullOrEmpty(val)) { setOSMTag(tag, val); } } } /** * Gets the name of the post code associated with this address. * * @return the name of the post code associated with this address */ public String getPostalCode() { String pc = getTagValueWithGuess(TagConstants.ADDR_POSTCODE_TAG); if (!MISSING_TAG.equals(pc) && !PostalCodeChecker.hasValidPostalCode(getCountry(), pc)) { pc = "(!)" + pc; } return pc; } /** * Checks if this instance has a valid postal code. * * @return true, if successful */ public boolean hasValidPostalCode() { return PostalCodeChecker.hasValidPostalCode(this); } /** * Checks for post code tag. * * @return true, if successful */ public boolean hasPostalCode() { return hasTag(TagConstants.ADDR_POSTCODE_TAG); } /** * Gets the name of the house number associated with this address. * * @return the name of the house number associated with this address */ public String getHouseNumber() { if (!TagUtils.hasAddrHousenumberTag(osmObject)) { if (!isPartOfInterpolation) { return MISSING_TAG; } else { return INTERPOLATION_TAG; } } return TagUtils.getAddrHousenumberValue(osmObject); } /** * Checks for house number. * * @return true, if successful */ public boolean hasHouseNumber() { return TagUtils.hasAddrHousenumberTag(osmObject) || isPartOfInterpolation; } @Override public String getName() { String name = TagUtils.getNameValue(osmObject); if (!StringUtils.isNullOrEmpty(name)) { return TagUtils.getAddrHousenameValue(osmObject); } return ""; } /** * Checks if this address is part of a address interpolation. * * @return true, if is part of interpolation */ protected boolean isPartOfInterpolation() { return isPartOfInterpolation; } /** * Checks if this address is part of an 'associated street' relation. * * @return true, if is part of interpolation */ protected boolean isPartOfRelation() { return isPartOfAssocStreetRel; } /** * Gets the name of the city associated with this address. * * @return the name of the city associated with this address */ public String getCity() { return getTagValueWithGuess(TagConstants.ADDR_CITY_TAG); } /** * Checks for city tag. * * @return true, if a city tag is present or available via referrer. */ public boolean hasCity() { return hasTag(TagConstants.ADDR_CITY_TAG); } /** * Gets the name of the state associated with this address. * * @return the name of the state associated with this address */ public String getState() { return getTagValueWithGuess(TagConstants.ADDR_STATE_TAG); } /** * Checks for state tag. * * @return true, if a state tag is present or available via referrer. */ public boolean hasState() { return hasTag(TagConstants.ADDR_STATE_TAG); } /** * Gets the name of the country associated with this address. * * @return the name of the country associated with this address */ public String getCountry() { return getTagValueWithGuess(TagConstants.ADDR_COUNTRY_TAG); } /** * Checks for country tag. * * @return true, if a country tag is present or available via referrer. */ public boolean hasCountry() { return hasTag(TagConstants.ADDR_COUNTRY_TAG); } /** * Removes all address-related tags from the node or way. */ public void removeAllAddressTags() { removeOSMTag(TagConstants.ADDR_CITY_TAG); removeOSMTag(TagConstants.ADDR_COUNTRY_TAG); removeOSMTag(TagConstants.ADDR_POSTCODE_TAG); removeOSMTag(TagConstants.ADDR_HOUSENUMBER_TAG); removeOSMTag(TagConstants.ADDR_STATE_TAG); removeOSMTag(TagConstants.ADDR_STREET_TAG); } /** * Checks if the associated OSM object has the given tag or if the tag is * available via a referrer. * * @param tag * the tag to look for. * @return true, if there is a value for the given tag. */ public boolean hasTag(String tag) { if (StringUtils.isNullOrEmpty(tag)) return false; return TagUtils.hasTag(osmObject, tag) || hasDerivedValue(tag); } @Override public int compareTo(IOSMEntity o) { if (o == null || !(o instanceof OSMAddress)) { return -1; } OSMAddress other = (OSMAddress) o; if (this.equals(other)) return 0; int cc = 0; cc = this.getCountry().compareTo(other.getCountry()); if (cc == 0) { cc = this.getState().compareTo(other.getState()); if (cc == 0) { cc = this.getCity().compareTo(other.getCity()); if (cc == 0) { cc = this.getStreetName().compareTo(other.getStreetName()); if (cc == 0) { if (hasGuessedStreetName()) { if (other.hasStreetName()) { // Compare guessed name with the real name /*String gsm =*/ this.getGuessedStreetName(); cc = this.getGuessedStreetName().compareTo( other.getStreetName()); if (cc == 0) { cc = this.getHouseNumber().compareTo( other.getHouseNumber()); } } else if (other.hasGuessedStreetName()) { // Compare guessed name with the guessed name cc = this.getGuessedStreetName().compareTo( other.getGuessedStreetName()); if (cc == 0) { cc = this.getHouseNumber().compareTo( other.getHouseNumber()); } } // else: give up // No guessed name at all -> just compare the // number } else { cc = this.getHouseNumber().compareTo( other.getHouseNumber()); } } } } } return cc; } /** * Applies the street name from the specified street node. * * @param node street node */ public void assignStreet(OSMStreet node) { if (node == null || !node.hasName()) return; if (!node.getName().equals(getStreetName())) { setStreetName(node.getName()); node.addAddress(this); fireEntityChanged(this); } } /** * Gets the guessed value for the given tag. * * @param tag * The tag to get the guessed value for. * @return the guessed value */ public String getGuessedValue(String tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); if (!hasGuessedValue(tag)) { return null; } return guessedValues.get(tag); } /** * Gets the guessed object. * * @param tag * the guessed tag * @return the object which has been selected for the guess */ public OsmPrimitive getGuessedObject(String tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); if (guessedObjects.containsKey(tag)) { return guessedObjects.get(tag); } return null; } /** * Gets all guessed objects or an empty list, if no guesses have been made * yet. * * @return the guessed objects. */ public Collection<OsmPrimitive> getGuessedObjects() { if (guessedObjects == null) return null; return guessedObjects.values(); } /** * Check if this instance needs guessed values. This is the case, if the * underlying OSM node has either no street name, post code or city. * * @return true, if this instance needs at least one guessed value. */ public boolean needsGuess() { return needsGuessedValue(TagConstants.ADDR_CITY_TAG) || needsGuessedValue(TagConstants.ADDR_POSTCODE_TAG) || needsGuessedValue(TagConstants.ADDR_COUNTRY_TAG) || // needsGuessedValue(TagConstants.ADDR_STATE_TAG) || needsGuessedValue(TagConstants.ADDR_STREET_TAG); } /** * Check if this instance needs guessed value for a given tag. * * @return true, if successful */ public boolean needsGuessedValue(String tag) { return MISSING_TAG.equals(getTagValueWithGuess(tag)); } /** * Clears all guessed values. */ public void clearAllGuesses() { guessedValues.clear(); } /** * Checks if given tag has a guessed value (tag exists and has a non-empty * value). * * @param tag * the tag * @return true, if tag has a guessed value. */ private boolean hasGuessedValue(String tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); return guessedValues.containsKey(tag) && !StringUtils.isNullOrEmpty(guessedValues.get(tag)); } /** * Sets the guessed value with the given tag. * * @param tag * the tag to set the guess for * @param value * the value of the guessed tag. * @param osm * the (optional) object which was used for the guess */ public void setGuessedValue(String tag, String value, OsmPrimitive osm) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); if (value != null && osm != null) { guessedValues.put(tag, value); if (osm != null) { guessedObjects.put(tag, osm); } fireEntityChanged(this); } } /** * Checks if given tag has a derived value (value is available via a * referrer). * * @param tag * the tag * @return true, if tag has a derived value. */ private boolean hasDerivedValue(String tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); return derivedValues.containsKey(tag) && !StringUtils.isNullOrEmpty(derivedValues.get(tag)); } /** * Returns true, if this instance has derived values from any referrer. * * @return true, if this instance has derived values from any referrer */ public boolean hasDerivedValues() { return derivedValues.size() > 0; } /** * Gets the derived value for the given tag. * * @param tag * The tag to get the derived value for. * @return the derived value for the given tag */ public String getDerivedValue(String tag) { if (!hasDerivedValue(tag)) { return null; } return derivedValues.get(tag); } /** * Sets the value known indirectly via a referrer with the given tag. * * @param tag * the tag to set the derived value for * @param value * the value of the derived tag. */ public void setDerivedValue(String tag, String value) { derivedValues.put(tag, value); } /** * Sets the street name of the address node. * * @param streetName street name of the address node */ public void setStreetName(String streetName) { if (streetName != null && streetName.length() == 0) return; setOSMTag(TagConstants.ADDR_STREET_TAG, streetName); } /** * Sets the state of the address node. * * @param state state of the address node */ public void setState(String state) { if (state != null && state.length() == 0) return; setOSMTag(TagConstants.ADDR_STATE_TAG, state); } /** * Sets the country of the address node. * * @param country country of the address node */ public void setCountry(String country) { if (country != null && country.length() == 0) return; setOSMTag(TagConstants.ADDR_COUNTRY_TAG, country); } /** * Sets the post code of the address node. * * @param postCode post code of the address node */ public void setPostCode(String postCode) { if (postCode != null && postCode.length() == 0) return; setOSMTag(TagConstants.ADDR_POSTCODE_TAG, postCode); } @Override public void visit(IAllKnowingTrashHeap trashHeap, IProblemVisitor visitor) { CheckParameterUtil.ensureParameterNotNull(visitor, "visitor"); // Check for street if (!hasStreetName()) { AddressProblem p = new AddressProblem(this, tr("Address has no street")); if (hasGuessedStreetName()) { // guess exists -> add solution entry String tag = TagConstants.ADDR_STREET_TAG; addGuessValueSolution(p, tag); } addRemoveAddressTagsSolution(p); visitor.addProblem(p); // Street name exists, but is invalid -> ask the all knowing trash // heap } else if (!trashHeap.isValidStreetName(getStreetName())) { AddressProblem p = new AddressProblem(this, tr("Address has no valid street")); String match = trashHeap.getClosestStreetName(getStreetName()); if (!StringUtils.isNullOrEmpty(match)) { setGuessedStreetName(match, null); addGuessValueSolution(p, TagConstants.ADDR_STREET_TAG); } visitor.addProblem(p); } // Check for postal code if (!hasPostalCode()) { AddressProblem p = new AddressProblem(this, tr("Address has no post code")); if (hasGuessedStreetName()) { String tag = TagConstants.ADDR_POSTCODE_TAG; addGuessValueSolution(p, tag); } addRemoveAddressTagsSolution(p); visitor.addProblem(p); } // Check for city if (!hasCity()) { AddressProblem p = new AddressProblem(this, tr("Address has no city")); if (hasGuessedStreetName()) { String tag = TagConstants.ADDR_CITY_TAG; addGuessValueSolution(p, tag); } addRemoveAddressTagsSolution(p); visitor.addProblem(p); } // Check for country if (!hasCountry()) { // TODO: Add guess for country AddressProblem p = new AddressProblem(this, tr("Address has no country")); addRemoveAddressTagsSolution(p); visitor.addProblem(p); } } /** * Adds the guess value solution to a problem. * * @param p * the problem to add the solution to. * @param tag * the tag to change. */ private void addGuessValueSolution(AddressProblem p, String tag) { AddressSolution s = new AddressSolution(String.format("%s '%s'", tr("Assign to"), getGuessedValue(tag)), AddressActions.getApplyGuessesAction(), SolutionType.Change); p.addSolution(s); } /** * Adds the remove address tags solution entry to a problem. * * @param problem * the problem */ private void addRemoveAddressTagsSolution(IProblem problem) { CheckParameterUtil.ensureParameterNotNull(problem, "problem"); AddressSolution s = new AddressSolution(tr("Remove all address tags"), AddressActions.getRemoveTagsAction(), SolutionType.Remove); problem.addSolution(s); } @Override public String toString() { return OSMAddress.getFormatString(this); } /** * Gets the formatted string representation of the given node. * * @param node * the node * @return the format string */ public static String getFormatString(OSMAddress node) { // TODO: Add further countries here // DE String guessed = node.getGuessedStreetName(); String sName = node.getStreetName(); if (!StringUtils.isNullOrEmpty(guessed) && MISSING_TAG.equals(sName)) { sName = String.format("(%s)", guessed); } return String.format("%s %s, %s-%s %s (%s) ", sName, node.getHouseNumber(), node.getCountry(), node.getPostalCode(), node.getCity(), node.getState()); } }