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