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