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