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            return Objects.hash(relation, layer);
057        }
058
059        @Override
060        public boolean equals(Object obj) {
061            if (this == obj) return true;
062            if (obj == null || getClass() != obj.getClass()) return false;
063            DialogContext that = (DialogContext) obj;
064            return Objects.equals(relation, that.relation) &&
065                    Objects.equals(layer, that.layer);
066        }
067
068        public boolean matchesLayer(OsmDataLayer layer) {
069            if (layer == null) return false;
070            return this.layer.equals(layer);
071        }
072
073        @Override
074        public String toString() {
075            return "[Context: layer=" + layer.getName() + ",relation=" + relation.getId() + ']';
076        }
077    }
078
079    /** the map of open dialogs */
080    private final Map<DialogContext, RelationEditor> openDialogs;
081
082    /**
083     * constructor
084     */
085    public RelationDialogManager() {
086        openDialogs = new HashMap<>();
087    }
088
089    /**
090     * Register the relation editor for a relation managed by a
091     * {@link OsmDataLayer}.
092     *
093     * @param layer the layer
094     * @param relation the relation
095     * @param editor the editor
096     */
097    public void register(OsmDataLayer layer, Relation relation, RelationEditor editor) {
098        if (relation == null) {
099            relation = new Relation();
100        }
101        DialogContext context = new DialogContext(layer, relation);
102        openDialogs.put(context, editor);
103        editor.addWindowListener(this);
104    }
105
106    public void updateContext(OsmDataLayer layer, Relation relation, RelationEditor editor) {
107        // lookup the entry for editor and remove it
108        //
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        //
118        DialogContext context = new DialogContext(layer, relation);
119        openDialogs.put(context, editor);
120    }
121
122    /**
123     * Closes the editor open for a specific layer and a specific relation.
124     *
125     * @param layer  the layer
126     * @param relation the relation
127     */
128    public void close(OsmDataLayer layer, Relation relation) {
129        DialogContext context = new DialogContext(layer, relation);
130        RelationEditor editor = openDialogs.get(context);
131        if (editor != null) {
132            editor.setVisible(false);
133        }
134    }
135
136    /**
137     * Replies true if there is an open relation editor for the relation managed
138     * by the given layer. Replies false if relation is null.
139     *
140     * @param layer  the layer
141     * @param relation  the relation. May be null.
142     * @return true if there is an open relation editor for the relation managed
143     * by the given layer; false otherwise
144     */
145    public boolean isOpenInEditor(OsmDataLayer layer, Relation relation) {
146        if (relation == null) return false;
147        DialogContext context = new DialogContext(layer, relation);
148        return openDialogs.keySet().contains(context);
149
150    }
151
152    /**
153     * Replies the editor for the relation managed by layer. Null, if no such editor
154     * is currently open. Returns null, if relation is null.
155     *
156     * @param layer the layer
157     * @param relation the relation
158     * @return the editor for the relation managed by layer. Null, if no such editor
159     * is currently open.
160     *
161     * @see #isOpenInEditor(OsmDataLayer, Relation)
162     */
163    public RelationEditor getEditorForRelation(OsmDataLayer layer, Relation relation) {
164        if (relation == null) return null;
165        DialogContext context = new DialogContext(layer, relation);
166        return openDialogs.get(context);
167    }
168
169    /**
170     * called when a layer is removed
171     *
172     */
173    @Override
174    public void layerRemoved(Layer oldLayer) {
175        if (!(oldLayer instanceof OsmDataLayer))
176            return;
177        OsmDataLayer dataLayer = (OsmDataLayer) oldLayer;
178
179        Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator();
180        while (it.hasNext()) {
181            Entry<DialogContext, RelationEditor> entry = it.next();
182            if (entry.getKey().matchesLayer(dataLayer)) {
183                RelationEditor editor = entry.getValue();
184                it.remove();
185                editor.setVisible(false);
186                editor.dispose();
187            }
188        }
189    }
190
191    @Override
192    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
193        // do nothing
194    }
195
196    @Override
197    public void layerAdded(Layer newLayer) {
198        // do nothing
199    }
200
201    @Override
202    public void windowClosed(WindowEvent e) {
203        RelationEditor editor = (RelationEditor) e.getWindow();
204        for (Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); it.hasNext();) {
205            if (editor.equals(it.next().getValue())) {
206                it.remove();
207                break;
208            }
209        }
210    }
211
212    /**
213     * Replies true, if there is another open {@link RelationEditor} whose
214     * upper left corner is close to <code>p</code>.
215     *
216     * @param p the reference point to check
217     * @param thisEditor the current editor
218     * @return true, if there is another open {@link RelationEditor} whose
219     * upper left corner is close to <code>p</code>.
220     */
221    protected boolean hasEditorWithCloseUpperLeftCorner(Point p, RelationEditor thisEditor) {
222        for (RelationEditor editor: openDialogs.values()) {
223            if (editor == thisEditor) {
224                continue;
225            }
226            Point corner = editor.getLocation();
227            if (p.x >= corner.x -5 && corner.x + 5 >= p.x
228                    && p.y >= corner.y -5 && corner.y + 5 >= p.y)
229                return true;
230        }
231        return false;
232    }
233
234    /**
235     * Positions a {@link RelationEditor} on the screen. Tries to center it on the
236     * screen. If it hide another instance of an editor at the same position this
237     * method tries to reposition <code>editor</code> by moving it slightly down and
238     * slightly to the right.
239     *
240     * @param editor the editor
241     */
242    public void positionOnScreen(RelationEditor editor) {
243        if (editor == null) return;
244        if (!openDialogs.isEmpty()) {
245            Point corner = editor.getLocation();
246            while (hasEditorWithCloseUpperLeftCorner(corner, editor)) {
247                // shift a little, so that the dialogs are not exactly on top of each other
248                corner.x += 20;
249                corner.y += 20;
250            }
251            editor.setLocation(corner);
252        }
253    }
254
255}