001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.relation; 003 004import java.awt.Point; 005import java.awt.Window; 006import java.awt.event.WindowAdapter; 007import java.awt.event.WindowEvent; 008import java.util.HashMap; 009import java.util.Iterator; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.Objects; 013import java.util.Optional; 014 015import org.openstreetmap.josm.data.osm.Relation; 016import org.openstreetmap.josm.gui.MainApplication; 017import org.openstreetmap.josm.gui.layer.Layer; 018import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 019import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 020import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 021import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 022import org.openstreetmap.josm.gui.layer.OsmDataLayer; 023 024/** 025 * RelationDialogManager keeps track of the open relation editors. 026 * 027 */ 028public class RelationDialogManager extends WindowAdapter implements LayerChangeListener { 029 030 /** keeps track of open relation editors */ 031 private static RelationDialogManager relationDialogManager; 032 033 /** 034 * Replies the singleton {@link RelationDialogManager} 035 * 036 * @return the singleton {@link RelationDialogManager} 037 */ 038 public static RelationDialogManager getRelationDialogManager() { 039 if (RelationDialogManager.relationDialogManager == null) { 040 RelationDialogManager.relationDialogManager = new RelationDialogManager(); 041 MainApplication.getLayerManager().addLayerChangeListener(RelationDialogManager.relationDialogManager); 042 } 043 return RelationDialogManager.relationDialogManager; 044 } 045 046 /** 047 * Helper class for keeping the context of a relation editor. A relation editor 048 * is open for a specific relation managed by a specific {@link OsmDataLayer} 049 * 050 */ 051 private static class DialogContext { 052 public final Relation relation; 053 public final OsmDataLayer layer; 054 055 DialogContext(OsmDataLayer layer, Relation relation) { 056 this.layer = layer; 057 this.relation = relation; 058 } 059 060 @Override 061 public int hashCode() { 062 return Objects.hash(relation, layer); 063 } 064 065 @Override 066 public boolean equals(Object obj) { 067 if (this == obj) return true; 068 if (obj == null || getClass() != obj.getClass()) return false; 069 DialogContext that = (DialogContext) obj; 070 return Objects.equals(relation, that.relation) && 071 Objects.equals(layer, that.layer); 072 } 073 074 public boolean matchesLayer(OsmDataLayer layer) { 075 if (layer == null) return false; 076 return this.layer.equals(layer); 077 } 078 079 @Override 080 public String toString() { 081 return "[Context: layer=" + layer.getName() + ",relation=" + relation.getId() + ']'; 082 } 083 } 084 085 /** the map of open dialogs */ 086 private final Map<DialogContext, RelationEditor> openDialogs; 087 088 /** 089 * constructor 090 */ 091 public RelationDialogManager() { 092 openDialogs = new HashMap<>(); 093 } 094 095 /** 096 * Register the relation editor for a relation managed by a {@link OsmDataLayer}. 097 * 098 * @param layer the layer 099 * @param relation the relation 100 * @param editor the editor 101 */ 102 public void register(OsmDataLayer layer, Relation relation, RelationEditor editor) { 103 openDialogs.put(new DialogContext(layer, Optional.ofNullable(relation).orElseGet(Relation::new)), editor); 104 editor.addWindowListener(this); 105 } 106 107 public void updateContext(OsmDataLayer layer, Relation relation, RelationEditor editor) { 108 // lookup the entry for editor and remove it 109 for (Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); it.hasNext();) { 110 Entry<DialogContext, RelationEditor> entry = it.next(); 111 if (Objects.equals(entry.getValue(), editor)) { 112 it.remove(); 113 break; 114 } 115 } 116 // don't add a window listener. Editor is already known to the relation dialog manager 117 openDialogs.put(new DialogContext(layer, relation), editor); 118 } 119 120 /** 121 * Closes the editor open for a specific layer and a specific relation. 122 * 123 * @param layer the layer 124 * @param relation the relation 125 */ 126 public void close(OsmDataLayer layer, Relation relation) { 127 DialogContext context = new DialogContext(layer, relation); 128 RelationEditor editor = openDialogs.get(context); 129 if (editor != null) { 130 editor.setVisible(false); 131 } 132 } 133 134 /** 135 * Replies true if there is an open relation editor for the relation managed 136 * by the given layer. Replies false if relation is null. 137 * 138 * @param layer the layer 139 * @param relation the relation. May be null. 140 * @return true if there is an open relation editor for the relation managed 141 * by the given layer; false otherwise 142 */ 143 public boolean isOpenInEditor(OsmDataLayer layer, Relation relation) { 144 if (relation == null) return false; 145 DialogContext context = new DialogContext(layer, relation); 146 return openDialogs.containsKey(context); 147 } 148 149 /** 150 * Replies the editor for the relation managed by layer. Null, if no such editor 151 * is currently open. Returns null, if relation is null. 152 * 153 * @param layer the layer 154 * @param relation the relation 155 * @return the editor for the relation managed by layer. Null, if no such editor 156 * is currently open. 157 * 158 * @see #isOpenInEditor(OsmDataLayer, Relation) 159 */ 160 public RelationEditor getEditorForRelation(OsmDataLayer layer, Relation relation) { 161 if (relation == null) return null; 162 DialogContext context = new DialogContext(layer, relation); 163 return openDialogs.get(context); 164 } 165 166 @Override 167 public void layerRemoving(LayerRemoveEvent e) { 168 Layer oldLayer = e.getRemovedLayer(); 169 if (!(oldLayer instanceof OsmDataLayer)) 170 return; 171 OsmDataLayer dataLayer = (OsmDataLayer) oldLayer; 172 173 Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); 174 while (it.hasNext()) { 175 Entry<DialogContext, RelationEditor> entry = it.next(); 176 if (entry.getKey().matchesLayer(dataLayer)) { 177 RelationEditor editor = entry.getValue(); 178 it.remove(); 179 editor.setVisible(false); 180 editor.dispose(); 181 } 182 } 183 } 184 185 @Override 186 public void layerAdded(LayerAddEvent e) { 187 // ignore 188 } 189 190 @Override 191 public void layerOrderChanged(LayerOrderChangeEvent e) { 192 // ignore 193 } 194 195 @Override 196 public void windowClosed(WindowEvent e) { 197 Window w = e.getWindow(); 198 if (w instanceof RelationEditor) { 199 RelationEditor editor = (RelationEditor) w; 200 for (Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); it.hasNext();) { 201 if (editor.equals(it.next().getValue())) { 202 it.remove(); 203 break; 204 } 205 } 206 } 207 } 208 209 /** 210 * Replies true, if there is another open {@link RelationEditor} whose 211 * upper left corner is close to <code>p</code>. 212 * 213 * @param p the reference point to check 214 * @param thisEditor the current editor 215 * @return true, if there is another open {@link RelationEditor} whose 216 * upper left corner is close to <code>p</code>. 217 */ 218 protected boolean hasEditorWithCloseUpperLeftCorner(Point p, RelationEditor thisEditor) { 219 for (RelationEditor editor: openDialogs.values()) { 220 if (editor == thisEditor) { 221 continue; 222 } 223 Point corner = editor.getLocation(); 224 if (p.x >= corner.x -5 && corner.x + 5 >= p.x 225 && p.y >= corner.y -5 && corner.y + 5 >= p.y) 226 return true; 227 } 228 return false; 229 } 230 231 /** 232 * Positions a {@link RelationEditor} on the screen. Tries to center it on the 233 * screen. If it hide another instance of an editor at the same position this 234 * method tries to reposition <code>editor</code> by moving it slightly down and 235 * slightly to the right. 236 * 237 * @param editor the editor 238 */ 239 public void positionOnScreen(RelationEditor editor) { 240 if (editor == null) return; 241 if (!openDialogs.isEmpty()) { 242 Point corner = editor.getLocation(); 243 while (hasEditorWithCloseUpperLeftCorner(corner, editor)) { 244 // shift a little, so that the dialogs are not exactly on top of each other 245 corner.x += 20; 246 corner.y += 20; 247 } 248 editor.setLocation(corner); 249 } 250 } 251 252}