// 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); } }