// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.czechaddress.addressdatabase;
import java.util.List;
import java.util.Objects;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.plugins.czechaddress.PrimUtils;
import org.openstreetmap.josm.plugins.czechaddress.StringUtils;
import org.openstreetmap.josm.plugins.czechaddress.intelligence.Reasoner;
import org.openstreetmap.josm.plugins.czechaddress.proposal.Proposal;
/**
* The most general element of the houses and streets database.
*
* <p>Every element must have a <i>name</i> and may have a <i>parent</i>.</p>
*
* @author Radomír Černoch radomir.cernoch@gmail.com
*/
public abstract class AddressElement implements Comparable<AddressElement> {
protected String name;
protected AddressElement parent = null;
/**
* Constructor setting the name of this element.
*/
public AddressElement(String name) {
if (name == null)
throw new NullPointerException("You must specify the name of this AddressElement");
this.name = name;
}
/**
* Sets the new name of this element.
*
* <p><b>NOTE:</b> Unlike in the default constructor, the name is <b>not</b>
* capitalized.</p>
*/
public void setName(String newName) {
name = newName;
}
/**
* Returns the name of this element.
*
* <p>The name is returned "as it was entered" in the constructor.</p>
*/
public String getName() {
return name;
}
public static String getName(Object o) {
if (o instanceof OsmPrimitive)
return getName((OsmPrimitive) o);
if (o instanceof AddressElement)
return ((AddressElement) o).getName();
return null;
}
public static String getName(OsmPrimitive prim) {
// CHECKSTYLE.OFF: SingleSpaceSeparator
String cp = prim.get(PrimUtils.KEY_ADDR_CP);
String co = prim.get(PrimUtils.KEY_ADDR_CO);
String street = prim.get(PrimUtils.KEY_ADDR_STREET);
String city = prim.get(PrimUtils.KEY_ADDR_CITY);
String name = prim.get(PrimUtils.KEY_NAME);
// CHECKSTYLE.ON: SingleSpaceSeparator
String result = "";
// Firstly we try to construct the name from name + place.
if (name != null)
result += name;
// We prefer to display street. If not present, try city.
if (street != null) {
if (result.length() > 0) result += ", ";
result += street;
} else
if (city != null) {
if (result.length() > 0) result += ", ";
result += city;
}
// If we can display CP, do it!
if (cp != null) {
if (result.length() > 0) result += " ";
result += cp;
}
// If we can display CO, do it!
if (co != null) {
if (co != null) result += "/";
result += co;
}
if (prim instanceof Node) {
Node node = (Node) prim;
result += " " + StringUtils.latLonToString(node.getCoor());
} else if (prim instanceof Way) {
Way way = (Way) prim;
if (way.getNodesCount() > 0)
result += " " + StringUtils.latLonToString(way.firstNode().getCoor());
else
result += " empty way";
}
if (prim.isDeleted())
result += " DEL";
return result;
}
/**
* Sets the new parent of this element.
*
* <p><b>NOTE:</b> Parent is NOT notified that this element has been
* assigned to him. This can be useful for some "ugly hacks".</p>
*/
public void setParent(AddressElement parent) {
this.parent = parent;
}
/**
* Returns the parent of this element.
*
* @return the parent element or {@code null} if no parent is present.
*/
public AddressElement getParent() {
return parent;
}
/**
* Compares 2 elements.
*
* <p>Basic criterion to comparing elements is their <i>name</i> and the
* <i>parent</i>. Notice that this behaviour might be changed
* in subclasses.</p>
*
* @return {@code false} if <i>name</i> does not match, or <i>parent</i>
* does not match or {@code null} is given as a parameter.
* {@code true} otherwise.
*/
@Override
public boolean equals(Object elem) {
return this == elem || (elem instanceof AddressElement
&& name.equals(((AddressElement) elem).name)
&& parent == ((AddressElement) elem).parent);
}
@Override
public int hashCode() {
return Objects.hash(name, parent);
}
/**
* Returns the string representation of this element.
*
* <p>If the <i>parent</i> of the element is {@code null}, then this equals
* to the element's <i>name</i>. Otherwise the result is created recursively
* as <tt>"name, parent.name"</tt>.</p>
*/
@Override
public String toString() {
return (parent == null) ? getName() : getName() + ", " + parent.toString();
}
/**
* Compute differences between two strings.
*
* <p>Typical usage of this function is during {@code OsmPrimitive} to
* {@code AddressElement} matching. This function helps to whether the given
* field does match, does not or causes a conflict.</p>
*
* <p>This method returns {@code 1} if both fields do match,
* {@code 0} if they can be matched and {@code -1} if they do not match.</p>
*
* <ul>
* <li>If the {@code elemValue} is {@code null}, the result is {@code 0}.
* The reason for this is that the database is considered untrustworthy
* by default (compared to the map) and therefore a missing field
* in the database can be matched with an element from the map.</li>
*
* <li>If the {@code primValue} is {@code null}, but {@code elemValue} is
* not, the map should be changeded. If the database already contains
* a value, we trust it's right. Therefore {@code -1} is returned.</li>
*
* <li>When both fields are not {@code null}, they are compared by their
* upper-case trimmed value. {@code 1} is equals, {@code -1}
* otherwise.</li>
* </ul>
*
* @param elemValue value of the {@link AddressElement}'s field
* @param primValue value of the {@link OsmPrimitive}'s field
*
* @return {@code -1}, {@code 0} or {@code 1} (see above)
*/
public static int matchField(String elemValue, String primValue) {
if (elemValue == null) return 0;
if (primValue == null) return -1;
return (primValue.trim().toUpperCase().equals(
elemValue.trim().toUpperCase())) ? 1 : -1;
}
/**
* Does the same as {@code matchField()}, but allows the string to contain
* abbreviations.
*
* <p>For detailed description see {@code matchField()}.</p>
*/
public static int matchFieldAbbrev(String elemValue, String primValue) {
if (elemValue == null) return 0;
if (primValue == null) return -1;
return StringUtils.matchAbbrev(primValue, elemValue) ? 1 : -1;
}
protected int[] getFieldMatchList(OsmPrimitive primitive) {
int[] result = {0};
return result;
}
protected int[] getAdditionalFieldMatchList(OsmPrimitive primitive) {
int[] result = {0};
return result;
}
/**
* Makes the given primitive have the same key-values as the current element.
*/
public List<Proposal> getDiff(OsmPrimitive prim) {
return null;
}
/**
* Returns the "quality" of a element-primitive match.
*
* <p>Returns a value from {@link Reasoner}{@code .MATCH_*}, which
* describes how well does the given primitive match to this element.</p>
*/
public int getQ(OsmPrimitive primitive) {
// Firstly get integers representing a match of every matchable field.
int[] fieldMatches = getFieldMatchList(primitive);
assert fieldMatches.length > 0;
// Now find the max and min of this array.
int minVal = fieldMatches[0];
int maxVal = fieldMatches[0];
for (int i = 1; i < fieldMatches.length; i++) {
if (minVal > fieldMatches[i])
minVal = fieldMatches[i];
if (maxVal < fieldMatches[i])
maxVal = fieldMatches[i];
}
// Check valid results
assert Math.abs(minVal) <= 1;
assert Math.abs(maxVal) <= 1;
// If the best among all fields is 'neutral' match, the given primitive
// has nothing to do with our field.
if (maxVal <= 0)
return Reasoner.MATCH_NOMATCH;
// If all fields are 1 --> ROCKSOLID MATCH
// If some are 1, none -1 --> PARTIAL MATCH
// If some are 1, some -1 --> CONFLICT
switch (minVal * maxVal) {
case -1 : return Reasoner.MATCH_CONFLICT;
case 0 : return Reasoner.MATCH_PARTIAL;
case +1 : return Reasoner.MATCH_ROCKSOLID;
}
return 0; // <-- just to make compilers happy. We cannot get here.
}
@Override
public int compareTo(AddressElement elem) {
ParentResolver r1 = new ParentResolver(this);
ParentResolver r2 = new ParentResolver(elem);
int retVal = r1.compareTo(r2);
if (retVal != 0) return retVal;
return toString().compareTo(elem.toString());
}
}