// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.czechaddress.intelligence;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.plugins.czechaddress.addressdatabase.AddressElement;
import org.openstreetmap.josm.plugins.czechaddress.proposal.ProposalContainer;
import org.openstreetmap.josm.plugins.czechaddress.proposal.ProposalDatabase;
/**
* Intended to concentrate all intelligence of
* {@link AddressElement}-{@link OsmPrimitive} matching.
*
* <p>Reasoner holds the relations between AddressElement and OsmPrimitive
* and also tries to keep it consistent with the state of the map.</p>
*
* <p>You can imagine data model as a big matrix, whose rows consist of
* {@code AddressElement}s and columns are {@code OsmPrimitive}s. The cell
* of this matrix is a so-called "quality", which says how well the primitive
* and element fit together (see {@code MATCH_*} for details).
* Through the documentation we will use <tt><b>Q(prim, elem)</b></tt> notation
* for this matrix. The reasoner is memory-efficient iff most of the Q values
* are equal to {@code MATCH_NOMATCH}.</p>
*
* <p><b>NOTE:</b> Currently there is no known way of adding a hook into JOSM
* to detect changed or deleted elements. Therefore there is a
* {@link SelectionMonitor}, which passes every selected primitive to
* the reasoner.</p>
*
* @author Radomír Černoch radomir.cernoch@gmail.com
*/
public final class Reasoner {
// CHECKSTYLE.OFF: SingleSpaceSeparator
public static final int MATCH_OVERWRITE = 4;
public static final int MATCH_ROCKSOLID = 3;
public static final int MATCH_PARTIAL = 2;
public static final int MATCH_CONFLICT = 1;
public static final int MATCH_NOMATCH = 0;
private Map<OsmPrimitive, AddressElement> primBestIndex = new HashMap<>();
private Map<AddressElement, OsmPrimitive> elemBestIndex = new HashMap<>();
private Map<OsmPrimitive, Map<AddressElement, Integer>> primMatchIndex = new HashMap<>();
private Map<AddressElement, Map<OsmPrimitive, Integer>> elemMatchIndex = new HashMap<>();
private Set<OsmPrimitive> primToUpdate = new HashSet<>();
private Set<AddressElement> elemToUpdate = new HashSet<>();
// CHECKSTYLE.ON: SingleSpaceSeparator
public static Logger logger = Logger.getLogger(Reasoner.class.getName());
private Reasoner() {}
private static Reasoner singleton = null;
public static Reasoner getInstance() {
if (singleton == null)
singleton = new Reasoner();
return singleton;
}
//==============================================================================
// INPUT METHODS
//==============================================================================
/**
* Brings the reasoner to the initial state
*/
public void reset() {
primToUpdate.clear();
elemToUpdate.clear();
primMatchIndex.clear();
elemMatchIndex.clear();
primBestIndex.clear();
primBestIndex.clear();
transactionOpened = false;
for (ReasonerListener listener : listeners) {
listener.resonerReseted();
}
}
/**
* Indicates whether there is currently an open transaction
*/
private boolean transactionOpened = false;
/**
* Prepares reasoner to modify its data.
*
* <p>This method must be called before <u>any</u> method, which might
* modify the data in the reasoner.
* The only exception is {@code reset()}.</p>
*
* <p>When there's an open transaction, the result of most output methods
* undefined. Exceptions to this rules are indicated.</p>
*
* <p><b>Transactions:</b> This method requires a closed transaction.</p>
*/
public void openTransaction() {
assert primToUpdate.size() == 0;
assert elemToUpdate.size() == 0;
assert !transactionOpened;
primToUpdate.clear();
elemToUpdate.clear();
transactionOpened = true;
}
/**
* Turns the reasoner back into consistent state.
*
* <p>Recreates {@code *BestIndex} indexes, sends notification to
* all listeners about changed elements/primitives and closes
* the transaction.</p>
*
* <p><b>Transactions:</b> This method requires an open transaction.</p>
*/
public void closeTransaction() {
assert transactionOpened;
Set<AddressElement> elemChanges = new HashSet<>();
Set<OsmPrimitive> primChanges = new HashSet<>();
for (OsmPrimitive prim : primToUpdate) {
AddressElement bestMatch = getStrictlyBest(prim);
if (primBestIndex.get(prim) != bestMatch) {
if (bestMatch == null) {
logger.log(Level.FINE, "primitive has no longer best match",
AddressElement.getName(prim));
primBestIndex.remove(prim);
} else {
logger.log(Level.FINE, "primitive has a new best match",
"prim=„" + AddressElement.getName(prim) + "“ → " +
"elem=„" + bestMatch + "“");
elemChanges.add(primBestIndex.get(prim));
primBestIndex.put(prim, bestMatch);
}
}
}
for (AddressElement elem : elemToUpdate) {
OsmPrimitive bestMatch = getStrictlyBest(elem);
if (elemBestIndex.get(elem) != bestMatch) {
if (bestMatch == null) {
logger.log(Level.FINE, "element has no longer best match", elem);
elemBestIndex.remove(elem);
} else {
logger.log(Level.FINE, "element has a new best match",
"elem=„" + elem + "“ → " +
"prim=„" + AddressElement.getName(bestMatch) + "“");
primChanges.add(elemBestIndex.get(elem));
elemBestIndex.put(elem, bestMatch);
}
}
}
elemToUpdate.addAll(elemChanges);
primToUpdate.addAll(primChanges);
transactionOpened = false;
for (ReasonerListener listener : listeners) {
for (AddressElement elem : elemToUpdate) {
if (elem != null)
listener.elementChanged(elem);
}
for (OsmPrimitive prim : primToUpdate) {
if (prim != null)
listener.primitiveChanged(prim);
}
}
primToUpdate.clear();
elemToUpdate.clear();
}
/**
* Update all relations of the given primitive.
*
* <p>If the primitive is unknown to the reasoner, it's added.
* Then it updates all cells in the Q matrix's column, which corresponds
* to the provided primitive. In the Q-matrix analogy is roughly equivalent
* to doing an update
* <center>∀ elem. Q(elem, prim) ← elem.getQ(prim).</center>
* Hence its time complexity is linear.</p>
*
* <p><b>Transactions:</b> This method requires an open transaction.</p>
*/
public void update(OsmPrimitive prim) {
logger.log(Level.FINER, "considering primitive", AddressElement.getName(prim));
assert transactionOpened;
Map<AddressElement, Integer> matches = primMatchIndex.get(prim);
if (matches == null) {
logger.log(Level.FINE, "new primitive detected", AddressElement.getName(prim));
matches = new HashMap<>();
primMatchIndex.put(prim, matches);
primToUpdate.add(prim);
}
for (AddressElement elem : elemMatchIndex.keySet()) {
reconsider(prim, elem);
}
}
/**
* Update all relations of the given element
*
* <p>If the primitive is unknown to the reasoner, it's added.
* Then it updates all cells in the Q matrix's row, which corresponds
* to the provided element.In the Q-matrix analogy is roughly equivalent
* to doing an update
* <center>∀ prim. Q(elem, prim) ← elem.getQ(prim).</center>
* Hence its time complexity is linear.</p>
*
* <p><b>Transactions:</b> This method requires an open transaction.</p>
*/
public void update(AddressElement elem) {
logger.log(Level.FINER, "considering element", elem);
assert transactionOpened;
Map<OsmPrimitive, Integer> matches = elemMatchIndex.get(elem);
if (matches == null) {
logger.log(Level.FINE, "new element detected", elem);
matches = new HashMap<>();
elemMatchIndex.put(elem, matches);
elemToUpdate.add(elem);
}
for (OsmPrimitive prim : primMatchIndex.keySet()) {
reconsider(prim, elem);
}
}
/**
* Internal method for doing the actual Q value update.
*/
private void reconsider(OsmPrimitive prim, AddressElement elem) {
assert transactionOpened;
int oldQ = getQ(prim, elem);
int newQ = evalQ(prim, elem, oldQ);
if (oldQ != newQ) {
logger.log(Level.FINE, "reconsidering match",
"q=" + String.valueOf(oldQ) + "→" + String.valueOf(newQ) + "; " +
"elem=„" + elem + "“; " +
"prim=„" + AddressElement.getName(prim) + "“");
putQ(prim, elem, newQ);
primToUpdate.add(prim);
elemToUpdate.add(elem);
primToUpdate.addAll(elemMatchIndex.get(elem).keySet());
elemToUpdate.addAll(primMatchIndex.get(prim).keySet());
}
}
/**
* Sets the relation's Q value to highest possible.
*
* <p>Regardless of how well the primitive and element pair fits
* together, it assings their Q value to {@code MATCH_OVERWRITE}.</p>
*
* <p><b>Transactions:</b> This method requires an open transaction.</p>
*/
public void doOverwrite(OsmPrimitive prim, AddressElement elem) {
logger.log(Level.FINER, "overwriting match",
"elem=„" + elem + "“; " +
"prim=„" + AddressElement.getName(prim) + "“");
assert transactionOpened;
update(prim);
update(elem);
putQ(prim, elem, MATCH_OVERWRITE);
primToUpdate.add(prim);
elemToUpdate.add(elem);
}
/**
* Sets the relation to its original Q value.
*
* <p>If the element-primitive pair was previously edited by
* {@code doOverwrite()} method, this returns their Q to the
* original value, which is determined by {@code evalQ()}.</p>
*
* <p><b>Transactions:</b> This method requires an open transaction.</p>
*/
public void unOverwrite(OsmPrimitive prim, AddressElement elem) {
logger.log(Level.FINER, "unoverwriting match",
"elem=„" + elem + "“; " +
"prim=„" + AddressElement.getName(prim) + "“");
assert transactionOpened;
update(prim);
update(elem);
putQ(prim, elem, evalQ(prim, elem, MATCH_NOMATCH));
primToUpdate.add(prim);
elemToUpdate.add(elem);
}
/**
* Returns the Q value of the given primitive-element relation.
*/
private int getQ(OsmPrimitive prim, AddressElement elem) {
if (elemMatchIndex.get(elem) == null) return MATCH_NOMATCH;
if (primMatchIndex.get(prim) == null) return MATCH_NOMATCH;
assert primMatchIndex.get(prim).get(elem)
== elemMatchIndex.get(elem).get(prim);
if (primMatchIndex.get(prim).get(elem) == null)
return 0;
else
return primMatchIndex.get(prim).get(elem);
}
/**
* Sets the Q value of the given primitive-element relation.
*/
private void putQ(OsmPrimitive prim, AddressElement elem, int qVal) {
if (qVal == MATCH_NOMATCH) {
primMatchIndex.get(prim).remove(elem);
elemMatchIndex.get(elem).remove(prim);
} else {
primMatchIndex.get(prim).put(elem, qVal);
elemMatchIndex.get(elem).put(prim, qVal);
}
}
/**
* Evaluates the Q value between the given primitive and element.
*
* <p>If {@code oldQ} is {@code MATCH_OVERWRITE}, it is preserved.</p>
*/
private int evalQ(OsmPrimitive prim, AddressElement elem, Integer oldQ) {
if (prim.isDeleted())
return MATCH_NOMATCH;
if (oldQ == MATCH_OVERWRITE)
return MATCH_OVERWRITE;
return elem.getQ(prim);
}
//==============================================================================
// OUTPUT METHODS
//==============================================================================
/**
* Returns the primitive, which is a unique counterpart of the element.
*
* <p>This method is probably the single most used method of the reasoner.
* It allows the unique translation between map and the database.</p>
*
* <p>An element <i>elem</i> and primitive <i>prim</i> can be translated
* between each other iff
* <center>[∄ <i>prim'</i>. Q(elem, prim') ≥ Q(elem, prim)] ∧
* [∄ <i>elem'</i>. Q(elem', prim) ≥ Q(elem, prim)].</center>
* In other words, the cell at Q(elem, prim) is the strictly greatest one
* in both its row and the column of the Q matrix.</p>
*
* <p>This method depends on {@code getStrictlyBest()}, which induces its
* complexity properties.</p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.
* However if the transaction is closed, its time-complexity reduces to
* constant thanks to using indexes.</p>
*/
public AddressElement translate(OsmPrimitive prim) {
if (prim == null) return null;
AddressElement elem = getStrictlyBest(prim);
if (getStrictlyBest(elem) == prim)
return elem;
return null;
}
/**
* Returns the element, which is a unique counterpart of the primitive.
*
* <p>This method is probably the single most used method of the reasoner.
* It allows the unique translation between the map and the database.</p>
*
* <p>An element <i>elem</i> and primitive <i>prim</i> can be translated
* between each other iff
* <center>[∄ <i>prim'</i>. Q(elem, prim') ≥ Q(elem, prim)] ∧
* [∄ <i>elem'</i>. Q(elem', prim) ≥ Q(elem, prim)].</center>
* In other words, the cell at Q(elem, prim) is the strictly greatest one
* in both its row and the column of the Q matrix.</p>
*
* <p>This method depends on {@code getStrictlyBest()}, which induces its
* complexity properties.</p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.
* However if the transaction is closed, its time-complexity reduces to
* constant thanks to using indexes.</p>
*/
public OsmPrimitive translate(AddressElement elem) {
if (elem == null) return null;
OsmPrimitive prim = getStrictlyBest(elem);
if (getStrictlyBest(prim) == elem)
return prim;
return null;
}
/**
* Says whether the given primitive has a conflict.
*
* <p>There are two conditions for a primitive to be in a conflict. It must
* be at least partially fitting to some element, but it cannot be
* uniquely translatable.
* <center> [∃ elem. Q(elem, prim) > NO_MATCH] ∧
* ]∄ elem. elem = translate(prim)] ,</center>
* which is equivalent to saying that
* <center>|getCandidates(prim)| ≥ 2</center></p>
*/
public boolean inConflict(OsmPrimitive prim) {
if (primMatchIndex.get(prim) == null) return false;
return primMatchIndex.get(prim).size() > 0
&& translate(translate(prim)) != prim;
}
/**
* Says whether the given element has a conflict.
*
* <p>There are two conditions for a element to be in a conflict. It must
* be at least partially fitting to some primitive, but it cannot be
* uniquely translatable.
* <center> [∃ prim. Q(elem, prim) > NO_MATCH] ∧
* [∄ prim. prim = translate(elem)] ,</center>
* which is equivalent to saying that
* <center>|getCandidates(prim)| ≥ 2</center></p>
*/
public boolean inConflict(AddressElement elem) {
if (elemMatchIndex.get(elem) == null) return false;
return elemMatchIndex.get(elem).size() > 0
&& translate(translate(elem)) != elem;
}
/**
* Returns elements having the best quality for the given primitive.
*
* <p>It searches among all Q values corresponding to the given primitive
* and returns a set of all elements, whose relation has the greatest
* Q value. Formally we can write that the output is a set
* <center>{elem | Q(elem, prim) > MATCH_NOMATCH
* ∧ ∀ elem'. Q(elem, prim) ≥ Q(elem', prim)}.</center></p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.</p>
*
* @return A new set, which can be freely manipulated. Changes are not
* reflected in the reasoner.
*/
public Set<AddressElement> getCandidates(OsmPrimitive prim) {
Set<AddressElement> result = new HashSet<>();
if (primMatchIndex.get(prim) == null) return result;
int best = MATCH_NOMATCH;
for (AddressElement elem : primMatchIndex.get(prim).keySet()) {
int cand = primMatchIndex.get(prim).get(elem);
if (best < cand)
best = cand;
}
for (AddressElement elem : primMatchIndex.get(prim).keySet()) {
int cand = primMatchIndex.get(prim).get(elem);
if (best == cand)
result.add(elem);
}
return result;
}
/**
* Returns primitives having the best quality for the given element.
*
* <p>It searches among all Q values corresponding to the given element
* and returns a set of all primitives, whose relation has the greatest
* Q value. Formally we can write that the output is a set
* <center>{prim | Q(elem, prim) > MATCH_NOMATCH
* ∧ ∀ prim'. Q(elem, prim) ≥ Q(elem, prim')}.</center></p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.</p>
*
* @return A new set, which can be freely manipulated. Changes are not
* reflected in the reasoner.
*/
public Set<OsmPrimitive> getCandidates(AddressElement elem) {
Set<OsmPrimitive> result = new HashSet<>();
if (elemMatchIndex.get(elem) == null) return result;
int best = MATCH_NOMATCH;
for (OsmPrimitive prim : elemMatchIndex.get(elem).keySet()) {
int cand = elemMatchIndex.get(elem).get(prim);
if (best < cand)
best = cand;
}
for (OsmPrimitive prim : elemMatchIndex.get(elem).keySet()) {
int cand = elemMatchIndex.get(elem).get(prim);
if (best == cand)
result.add(prim);
}
return result;
}
/**
* Returns the element having the best quality for the given primitive.
*
* <p>It searches among all Q values corresponding to the given primitive
* and returns such an element, whose relation has the greatest
* Q value. If there are more of them, {@code null} is returned.
* Formally we can write that the output is a primitive
* <center>elem: Q(elem, prim) > MATCH_NOMATCH
* ∧ ∀ elem'. Q(elem, prim) > Q(elem', prim)}.</center></p>
*
* <p>If {@code getCandidates(prim)} returns exactly one element,
* this method should return the same one. Otherwise {@code null}.</p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.
* However if the transaction is closed, its time-complexity reduces to
* constant thanks to using indexes.</p>
*/
public AddressElement getStrictlyBest(OsmPrimitive prim) {
AddressElement result = null;
try {
if (!transactionOpened)
return primBestIndex.get(prim);
Map<AddressElement, Integer> matches = primMatchIndex.get(prim);
if (matches == null) {
return null;
}
int bestQ = MATCH_NOMATCH;
for (AddressElement elem : matches.keySet()) {
if (matches.get(elem) == bestQ)
result = null;
if (matches.get(elem) > bestQ) {
bestQ = matches.get(elem);
result = elem;
}
}
} catch (NullPointerException except) {
System.err.println("Strange exception occurred." +
" If you find a way to reproduce this situation, please "+
"e-mail the author of the CzechAddress plugin.");
except.printStackTrace();
}
return result;
}
/**
* Returns the primitive having the best quality for the given element.
*
* <p>It searches among all Q values corresponding to the given element
* and returns sucha primitive, whose relation has the greatest
* Q value. If there are more of them, {@code null} is returned.
* Formally we can write that the output is a primitive
* <center>prim: Q(elem, prim) > MATCH_NOMATCH
* ∧ ∀ prim'. Q(elem, prim) > Q(elem', prim)}.</center></p>
*
* <p>If {@code getCandidates(elem)} returns exactly one primitive,
* this method should return the same one. Otherwise {@code null}.</p>
*
* <p><b>Transactions:</b> Can be called regardless of transaction state.
* However if the transaction is closed, its time-complexity reduces to
* constant thanks to using indexes.</p>
*/
public OsmPrimitive getStrictlyBest(AddressElement elem) {
OsmPrimitive result = null;
try {
if (!transactionOpened)
return elemBestIndex.get(elem);
Map<OsmPrimitive, Integer> matches = elemMatchIndex.get(elem);
if (matches == null) {
return null;
}
int bestQ = MATCH_NOMATCH;
for (OsmPrimitive prim : matches.keySet()) {
if (matches.get(prim) == bestQ) {
result = null;
}
if (matches.get(prim) > bestQ) {
bestQ = matches.get(prim);
result = prim;
}
}
} catch (NullPointerException except) {
System.err.println("Strange exception occurred." +
" If you find a way to reproduce this situation, please "+
"e-mail the author of the CzechAddress plugin.");
except.printStackTrace();
}
return result;
}
/**
* Returns all elements which are not translatable.
*/
public Set<AddressElement> getUnassignedElements() {
Set<AddressElement> result = new HashSet<>();
for (AddressElement elem : elemMatchIndex.keySet()) {
if (translate(elem) == null)
result.add(elem);
}
return result;
}
/**
* Returns all primitives which are not translatable.
*/
public Set<OsmPrimitive> getUnassignedPrimitives() {
Set<OsmPrimitive> result = new HashSet<>();
for (OsmPrimitive prim : primMatchIndex.keySet()) {
if (translate(prim) == null)
result.add(prim);
}
return result;
}
/**
* Returns all elements, which were {@code update}d from
* the last {@code reset}.
*/
public Set<AddressElement> getAllElements() {
Set<AddressElement> result = new HashSet<>();
result.addAll(elemMatchIndex.keySet());
return result;
}
/**
* Returns all elements, which were {@code update}d from
* the last {@code reset}.
*/
public Set<OsmPrimitive> getAllPrimitives() {
Set<OsmPrimitive> result = new HashSet<>();
result.addAll(primMatchIndex.keySet());
return result;
}
//==============================================================================
// MISC METHODS
//==============================================================================
/**
* Returns proposals to fit all translatable primitives to their elements.
*/
public ProposalDatabase getProposals() {
ProposalDatabase database = new ProposalDatabase();
// We can go only over primBestIndex to save some iterations.
// A primitive cannot be translated unless contained in primBestIndex.
for (OsmPrimitive prim : primBestIndex.keySet()) {
AddressElement elem = translate(prim);
if (elem == null) continue;
ProposalContainer container = new ProposalContainer(prim);
container.addProposals(elem.getDiff(prim));
if (container.getProposals().size() > 0)
database.addContainer(container);
}
Collections.sort(database.getContainers());
return database;
}
/**
* Set of listeners currently hooked to changes in this reasoner.
*/
private Set<ReasonerListener> listeners = new HashSet<>();
/**
* Adds a new listener to receive reasoner's status changes.
*/
public void addListener(ReasonerListener listener) {
listeners.add(listener);
}
/**
* Stops the listener to receive reasoner's status changes.
*/
public void removeListener(ReasonerListener listener) {
listeners.remove(listener);
}
}