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