// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.fixAddresses;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataSetListener;
import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* Provides a container serving streets and unresolved/incomplete addresses. It scans through a
* set of OSM primitives and checks for incomplete addresses (e. g. missing addr:... tags) or
* addresses with unknown streets ("unresolved addresses").
*
* It listens to changes within instances of {@link IOSMEntity} to notify clients on update.
*
* {@link AddressEditContainer} is the central class used within actions and UI models to show
* and alter OSM data.
*
* {@see AbstractAddressEditAction}
* {@see AddressEditTableModel}
*
* @author Oliver Wieland <oliver.wieland@online.de>
*
*/
public class AddressEditContainer implements Visitor, DataSetListener, IAddressEditContainerListener, IProblemVisitor, IAllKnowingTrashHeap {
private Collection<? extends OsmPrimitive> workingSet;
/** The street dictionary collecting all streets to a set of unique street names. */
private HashMap<String, OSMStreet> streetDict = new HashMap<>(100);
/** The unresolved (addresses without valid street name) addresses list. */
private List<OSMAddress> unresolvedAddresses = new ArrayList<>(100);
/** The incomplete addresses list. */
private List<OSMAddress> incompleteAddresses = new ArrayList<>(100);
/** The shadow copy to assemble the street dict during update. */
private HashMap<String, OSMStreet> shadowStreetDict = new HashMap<>(100);
/** The shadow copy to assemble the unresolved addresses during update. */
private List<OSMAddress> shadowUnresolvedAddresses = new ArrayList<>(100);
/** The shadow copy to assemble the incomplete addresses during update. */
private List<OSMAddress> shadowIncompleteAddresses = new ArrayList<>(100);
/** The visited nodes cache to increase iteration speed. */
private HashSet<Node> visitedNodes = new HashSet<>();
/** The visited ways cache to increase iteration speed. */
private HashSet<Way> visitedWays = new HashSet<>();
/** The tag list used within the data area. */
private HashSet<String> tags = new HashSet<>();
/** The tag list used within the data area. */
private HashMap<String, String> values = new HashMap<>();
/** The list containing the problems */
private List<IProblem> problems = new ArrayList<>();
/** The change listeners. */
private List<IAddressEditContainerListener> listeners = new ArrayList<>();
/**
* Creates an empty container.
*/
public AddressEditContainer() {
OSMEntityBase.addChangedListener(this);
}
/**
* Gets the working set used by the container. This can by either the complete or just
* a subset of the current data layer.
*
* @return the workingSet
*/
protected Collection<? extends OsmPrimitive> getWorkingSet() {
return workingSet;
}
/**
* Adds a change listener.
* @param listener change listener
*/
public void addChangedListener(IAddressEditContainerListener listener) {
listeners.add(listener);
}
/**
* Removes a change listener.
* @param listener change listener
*/
public void removeChangedListener(IAddressEditContainerListener listener) {
listeners.remove(listener);
}
/**
* Notifies clients that the address container changed.
*/
protected void fireContainerChanged() {
List<IAddressEditContainerListener> shadowListeners =
new ArrayList<>(listeners);
for (IAddressEditContainerListener listener : shadowListeners) {
listener.containerChanged(this);
}
}
/**
* Notifies clients that an entity within the address container changed.
*/
protected void fireEntityChanged(IOSMEntity entity) {
if (entity == null) throw new RuntimeException("Entity must not be null");
List<IAddressEditContainerListener> shadowListeners =
new ArrayList<>(listeners);
for (IAddressEditContainerListener listener : shadowListeners) {
listener.entityChanged(entity);
}
}
/**
* Marks an OSM node as visited.
*
* @param n the node to mark.
*/
private void markNodeAsVisited(Node n) {
visitedNodes.add(n);
}
/**
* Checks a node for been visited.
*
* @param n the n
* @return true, if node has been visited
*/
private boolean hasBeenVisited(Node n) {
return visitedNodes.contains(n);
}
/**
* Marks a way as visited.
*
* @param w the way to mark
*/
private void markWayAsVisited(Way w) {
visitedWays.add(w);
}
/**
* Checks a way for been visited.
*
* @param w the w to check
* @return true, if way has been visited
*/
private boolean hasBeenVisited(Way w) {
return visitedWays.contains(w);
}
@Override
public void visit(Node n) {
if (hasBeenVisited(n)) {
return;
}
OSMAddress aNode = null;
// Address nodes are recycled in order to keep instance variables like guessed names
aNode = OsmFactory.createNode(n);
if (aNode != null) {
addAndClassifyAddress(aNode);
aNode.visit(this, this);
}
markNodeAsVisited(n);
}
@Override
public void visit(Way w) {
// This doesn't matter, we just need the street name
//if (w.isIncomplete()) return;
if (hasBeenVisited(w)) {
return;
}
createNodeFromWay(w);
markWayAsVisited(w);
}
/**
* Adds and classify an address node according to completeness.
*
* @param aNode the address node to add and check
*/
private void addAndClassifyAddress(OSMAddress aNode) {
if (!assignAddressToStreet(aNode)) {
// Assignment failed: Street is not known (yet) -> add to 'unresolved' list
shadowUnresolvedAddresses.add(aNode);
}
if (!aNode.isComplete()) {
shadowIncompleteAddresses.add(aNode);
}
}
/**
* Creates the node from an OSM way instance.
*
* @param w the way to create the entity from
*/
private void createNodeFromWay(Way w) {
IOSMEntity ne = OsmFactory.createNodeFromWay(w);
if (!processNode(ne, w)) {
// Look also into nodes for addresses (unlikely, but at least they
// get marked as visited).
for (Node n : w.getNodes()) {
visit(n);
}
for (String key : w.keySet()) {
if (!tags.contains(key)) {
tags.add(key);
}
String v = w.get(key);
if (!values.containsKey(v)) {
values.put(v, key);
}
}
} // else: node has been processed, no need to look deeper
}
/**
* Process an entity node depending on the type. A street segment is added as a child to the
* corresponding street dictionary while an address is added to the incomplete/unresolved list
* depending of it's properties.
*
* @param ne the entity node.
* @param w the corresponding OSM way
* @return true, if node has been processed
*/
private boolean processNode(IOSMEntity ne, Way w) {
if (ne != null) {
// Node is a street (segment)
if (ne instanceof OSMStreetSegment) {
OSMStreetSegment newSegment = (OSMStreetSegment) ne;
if (newSegment != null) {
String name = newSegment.getName();
if (StringUtils.isNullOrEmpty(name)) return false;
OSMStreet sNode = null;
if (shadowStreetDict.containsKey(name)) { // street exists?
sNode = shadowStreetDict.get(name);
} else { // new street name -> add to dict
sNode = new OSMStreet(w);
shadowStreetDict.put(name, sNode);
}
if (sNode != null) {
// TODO: Check if segment really belongs to the street, even if the
// names are the same. Then the streets should be split up...
sNode.addStreetSegment(newSegment);
return true;
} else {
throw new RuntimeException("Street node is null!");
}
}
}
// Node is an address
if (ne instanceof OSMAddress) {
OSMAddress aNode = (OSMAddress) ne;
addAndClassifyAddress(aNode);
return true;
}
}
return false;
}
@Override
public void visit(Relation e) {
}
@Override
public void visit(Changeset cs) {
}
/**
* Gets the dictionary containing the collected streets.
* @return dictionary containing the collected streets
*/
public HashMap<String, OSMStreet> getStreetDict() {
return streetDict;
}
/**
* Gets the unresolved (addresses without valid street name) addresses.
*
* @return the unresolved addresses
*/
public List<OSMAddress> getUnresolvedAddresses() {
return unresolvedAddresses;
}
/**
* Gets the list with incomplete addresses.
*
* @return the incomplete addresses
*/
public List<OSMAddress> getIncompleteAddresses() {
return incompleteAddresses;
}
/**
* Gets the street list.
*
* @return the street list
*/
public List<OSMStreet> getStreetList() {
ArrayList<OSMStreet> sortedList = new ArrayList<>(streetDict.values());
Collections.sort(sortedList);
return sortedList;
}
/**
* Gets all addresses without valid street.
* @return all addresses without valid street
*/
public List<OSMAddress> getUnresolvedItems() {
return unresolvedAddresses;
}
/**
* Gets the tags used in the data layer.
* @return the tags used in the data layer
*/
public HashSet<String> getTags() {
return tags;
}
/**
* @return the values
*/
protected HashMap<String, String> getValues() {
return values;
}
/**
* Gets the number of streets in the container.
* @return the number of streets in the container
*/
public int getNumberOfStreets() {
return streetDict != null ? streetDict.size() : 0;
}
/**
* Get the number of incomplete addresses.
* @return the number of incomplete addresses
*/
public int getNumberOfIncompleteAddresses() {
return incompleteAddresses != null ? incompleteAddresses.size() : 0;
}
/**
* Gets the number of unresolved addresses.
* @return the number of unresolved addresses
*/
public int getNumberOfUnresolvedAddresses() {
return unresolvedAddresses != null ? unresolvedAddresses.size() : 0;
}
/**
* Gets the number of invalid (unresolved and/or incomplete) addresses.
*
* @return the number of invalid addresses
*/
public int getNumberOfInvalidAddresses() {
return getNumberOfIncompleteAddresses() + getNumberOfUnresolvedAddresses();
}
/**
* Gets the number of guessed tags.
* @return the number of guessed tags
*/
public int getNumberOfGuesses() {
int sum = 0;
for (OSMAddress aNode : getAllAddressesToFix()) {
if (aNode.hasGuesses()) {
sum++;
}
}
return sum;
}
/**
* Gets all (incomplete and/or unresolved) address nodes to fix.
* @return all (incomplete and/or unresolved) address nodes to fix
*/
public List<OSMAddress> getAllAddressesToFix() {
List<OSMAddress> all = new ArrayList<>(incompleteAddresses);
for (OSMAddress aNode : unresolvedAddresses) {
if (!all.contains(aNode)) {
all.add(aNode);
}
}
return all;
}
/**
* @return the problems
*/
protected List<IProblem> getProblems() {
return problems;
}
/**
* Clears the problem list.
*/
protected void clearProblems() {
problems.clear();
}
/**
* Tries to assign an address to a street.
* @param aNode address
*/
private boolean assignAddressToStreet(OSMAddress aNode) {
String streetName = aNode.getStreetName();
// street name via relation -> implicitly resolved (see TRAC #8336)
if (aNode.isPartOfRelation()) {
return true;
}
if (streetName != null && shadowStreetDict.containsKey(streetName)) {
OSMStreet sNode = shadowStreetDict.get(streetName);
sNode.addAddress(aNode);
return true;
}
return false;
}
/**
* Walks through the list of unassigned addresses and tries to assign them to streets.
*/
public void resolveAddresses() {
List<OSMAddress> resolvedAddresses = new ArrayList<>();
for (OSMAddress node : shadowUnresolvedAddresses) {
if (assignAddressToStreet(node)) {
resolvedAddresses.add(node);
}
}
/* Remove all resolves nodes from unresolved list */
for (OSMAddress resolved : resolvedAddresses) {
shadowUnresolvedAddresses.remove(resolved);
}
}
/**
* Rebuilds the street and address lists using the data set given
* in {@link AddressEditContainer#attachToDataSet(Collection)} or the
* full data set of the current data layer {@link Main#getCurrentDataSet()}.
*/
public void invalidate() {
if (workingSet != null) {
invalidate(workingSet);
} else {
if (Main.getLayerManager().getEditDataSet() != null) {
invalidate(Main.getLayerManager().getEditDataSet().allPrimitives());
}
}
}
/**
* Invalidate using the given data collection.
*
* @param osmData the collection containing the osm data to work on.
*/
public void invalidate(final Collection<? extends OsmPrimitive> osmData) {
if (osmData == null || osmData.isEmpty())
return;
synchronized (this) {
clearData();
clearProblems();
// visit data set for problems...
for (OsmPrimitive osmPrimitive : osmData) {
if (osmPrimitive.isUsable()) {
osmPrimitive.accept(this);
}
}
// match streets with addresses...
resolveAddresses();
// sort problem lists
Collections.sort(shadowIncompleteAddresses);
Collections.sort(shadowUnresolvedAddresses);
// put results from shadow copy into real lists
incompleteAddresses = new ArrayList<>(shadowIncompleteAddresses);
unresolvedAddresses = new ArrayList<>(shadowUnresolvedAddresses);
streetDict = new HashMap<>(shadowStreetDict);
// remove temp data
shadowStreetDict.clear();
shadowUnresolvedAddresses.clear();
shadowIncompleteAddresses.clear();
// update clients
fireContainerChanged();
}
}
/**
* Clears the shadowed lists data and resets the 'visited' flag for every OSM object.
*/
private void clearData() {
shadowStreetDict.clear();
shadowUnresolvedAddresses.clear();
shadowIncompleteAddresses.clear();
visitedNodes.clear();
visitedWays.clear();
}
/**
* Connects the listener to the given data set and revisits the data. This method should
* be called immediately after an edit session finished.
*
* If the given data set is not null, calls of {@link AddressEditContainer#invalidate()} will
* only consider the data given within this set. This can be useful to keep out unimportant
* areas. However, a side-effect could be that some streets are not included and leads to
* false alarms regarding unresolved addresses.
*
* Calling {@link AddressEditContainer#detachFromDataSet()} drops the explicit data set and uses
* the full data set on subsequent updates.<p>
*
* <b>Example</b><br>
* <code>
* attachToDataSet(osmDataSetToWorkOn); // osmDataSetToWorkOn = selected items<br>
* //... launch dialog or whatever<br>
* detachFromDataSet();
* </code>
*
* @param osmDataToWorkOn the data to examine
*/
public void attachToDataSet(Collection<? extends OsmPrimitive> osmDataToWorkOn) {
if (osmDataToWorkOn != null && !osmDataToWorkOn.isEmpty()) {
workingSet = new ArrayList<>(osmDataToWorkOn);
} else {
detachFromDataSet(); // drop old stuff, if present
}
invalidate(); // start our investigation...
}
/**
* Disconnects the listener from the data set. This method should
* be called immediately before an edit session started in order to
* prevent updates caused by e. g. a selection change within the map view.
*/
public void detachFromDataSet() {
//Main.main.getCurrentDataSet().removeDataSetListener(this);
if (workingSet != null) {
workingSet.clear();
workingSet = null;
}
}
@Override
public void dataChanged(DataChangedEvent event) {
}
@Override
public void nodeMoved(NodeMovedEvent event) {
}
@Override
public void otherDatasetChange(AbstractDatasetChangedEvent event) {
}
@Override
public void primitivesAdded(PrimitivesAddedEvent event) {
invalidate();
}
@Override
public void primitivesRemoved(PrimitivesRemovedEvent event) {
invalidate();
}
@Override
public void relationMembersChanged(RelationMembersChangedEvent event) {
}
@Override
public void tagsChanged(TagsChangedEvent event) {
invalidate();
}
@Override
public void wayNodesChanged(WayNodesChangedEvent event) {
}
@Override
public void containerChanged(AddressEditContainer container) {
invalidate();
}
@Override
public void entityChanged(IOSMEntity entity) {
fireEntityChanged(entity);
}
@Override
public void addProblem(IProblem problem) {
problems.add(problem);
}
@Override
public void removeProblemsOfSource(IOSMEntity entity) {
CheckParameterUtil.ensureParameterNotNull(entity, "entity");
List<IProblem> problemsToRemove = new ArrayList<>();
for (IProblem problem : problems) {
if (problem.getSource() == entity) {
problemsToRemove.add(problem);
}
}
for (IProblem iProblem : problemsToRemove) {
problems.remove(iProblem);
}
}
@Override
public String getClosestStreetName(String name) {
List<String> matches = getClosestStreetNames(name, 1);
if (matches != null && matches.size() > 0) {
return matches.get(0);
}
return null;
}
@Override
public List<String> getClosestStreetNames(String name, int maxEntries) {
CheckParameterUtil.ensureParameterNotNull(name, "name");
// ensure right number of entries
if (maxEntries < 1) maxEntries = 1;
List<StreetScore> scores = new ArrayList<>();
List<String> matches = new ArrayList<>();
// Find the longest common sub string
for (String streetName : streetDict.keySet()) {
int score = StringUtils.lcsLength(name, streetName);
if (score > 3) { // reasonable value?
StreetScore sc = new StreetScore(streetName, score);
scores.add(sc);
}
}
// sort by score
Collections.sort(scores);
// populate result list
int n = Math.min(maxEntries, scores.size());
for (int i = 0; i < n; i++) {
matches.add(scores.get(i).getName());
}
return matches;
}
@Override
public boolean isValidStreetName(String name) {
if (streetDict == null) return false;
return streetDict.containsKey(name);
}
/**
* Internal class to handle results of {@link AddressEditContainer#getClosestStreetNames(String, int)}.
*/
private class StreetScore implements Comparable<StreetScore> {
private String name;
private int score;
/**
* @param name Name of the street.
* @param score Score of the street (length of longest common substring)
*/
StreetScore(String name, int score) {
super();
this.name = name;
this.score = score;
}
/**
* @return the name of the street.
*/
protected String getName() {
return name;
}
/**
* @return the score of the street.
*/
@SuppressWarnings("unused")
// TODO: Implement properly
protected int getScore() {
return score;
}
@Override
public int compareTo(StreetScore arg0) {
if (arg0 == null) return 1;
return Integer.valueOf(score).compareTo(Integer.valueOf(arg0.score));
}
}
}