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