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}