001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.AWTEvent;
007import java.awt.Cursor;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010import java.awt.Toolkit;
011import java.awt.event.AWTEventListener;
012import java.awt.event.ActionEvent;
013import java.awt.event.FocusEvent;
014import java.awt.event.FocusListener;
015import java.awt.event.KeyEvent;
016import java.awt.event.MouseEvent;
017import java.util.Locale;
018
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021
022import org.openstreetmap.josm.actions.mapmode.MapMode;
023import org.openstreetmap.josm.data.coor.EastNorth;
024import org.openstreetmap.josm.data.coor.LatLon;
025import org.openstreetmap.josm.data.imagery.OffsetBookmark;
026import org.openstreetmap.josm.data.projection.ProjectionRegistry;
027import org.openstreetmap.josm.gui.ExtendedDialog;
028import org.openstreetmap.josm.gui.MainApplication;
029import org.openstreetmap.josm.gui.MapFrame;
030import org.openstreetmap.josm.gui.MapView;
031import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
032import org.openstreetmap.josm.gui.util.WindowGeometry;
033import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
034import org.openstreetmap.josm.gui.widgets.JosmTextField;
035import org.openstreetmap.josm.tools.GBC;
036import org.openstreetmap.josm.tools.ImageProvider;
037import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
038import org.openstreetmap.josm.tools.Logging;
039import org.openstreetmap.josm.tools.Utils;
040
041/**
042 * Adjust the position of an imagery layer.
043 * @since 3715
044 */
045public class ImageryAdjustAction extends MapMode implements AWTEventListener {
046    private static ImageryOffsetDialog offsetDialog;
047
048    private transient OffsetBookmark old;
049    private transient OffsetBookmark tempOffset;
050    private EastNorth prevEastNorth;
051    private transient AbstractTileSourceLayer<?> layer;
052    private MapMode oldMapMode;
053    private boolean exitingMode;
054    private boolean restoreOldMode;
055
056    /**
057     * Constructs a new {@code ImageryAdjustAction} for the given layer.
058     * @param layer The imagery layer
059     */
060    public ImageryAdjustAction(AbstractTileSourceLayer<?> layer) {
061        super(tr("New offset"), "adjustimg", tr("Adjust the position of this imagery layer"),
062                ImageProvider.getCursor("normal", "move"));
063        putValue("toolbar", Boolean.FALSE);
064        this.layer = layer;
065    }
066
067    @Override
068    public void enterMode() {
069        super.enterMode();
070        if (layer == null)
071            return;
072        if (!layer.isVisible()) {
073            layer.setVisible(true);
074        }
075        old = layer.getDisplaySettings().getOffsetBookmark();
076        EastNorth curOff = old == null ? EastNorth.ZERO : old.getDisplacement(ProjectionRegistry.getProjection());
077        LatLon center;
078        if (MainApplication.isDisplayingMapView()) {
079            center = ProjectionRegistry.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter());
080        } else {
081            center = LatLon.ZERO;
082        }
083        tempOffset = new OffsetBookmark(
084                ProjectionRegistry.getProjection().toCode(),
085                layer.getInfo().getId(),
086                layer.getInfo().getName(),
087                null,
088                curOff, center);
089        layer.getDisplaySettings().setOffsetBookmark(tempOffset);
090        addListeners();
091        showOffsetDialog(new ImageryOffsetDialog());
092    }
093
094    private static void showOffsetDialog(ImageryOffsetDialog dlg) {
095        offsetDialog = dlg;
096        offsetDialog.setVisible(true);
097    }
098
099    private static void hideOffsetDialog() {
100        offsetDialog.setVisible(false);
101        offsetDialog = null;
102    }
103
104    protected void addListeners() {
105        MapView mapView = MainApplication.getMap().mapView;
106        mapView.addMouseListener(this);
107        mapView.addMouseMotionListener(this);
108        try {
109            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
110        } catch (SecurityException ex) {
111            Logging.error(ex);
112        }
113    }
114
115    @Override
116    public void exitMode() {
117        // do not restore old mode here - this is called when the new mode is already known.
118        restoreOldMode = false;
119        doExitMode();
120    }
121
122    private void doExitMode() {
123        exitingMode = true;
124        try {
125            super.exitMode();
126        } catch (IllegalArgumentException e) {
127            Logging.trace(e);
128        }
129        if (offsetDialog != null) {
130            if (layer != null) {
131                layer.getDisplaySettings().setOffsetBookmark(old);
132            }
133            hideOffsetDialog();
134        }
135        removeListeners();
136        exitingMode = false;
137    }
138
139    protected void removeListeners() {
140        try {
141            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
142        } catch (SecurityException ex) {
143            Logging.error(ex);
144        }
145        if (MainApplication.isDisplayingMapView()) {
146            MapFrame map = MainApplication.getMap();
147            map.mapView.removeMouseMotionListener(this);
148            map.mapView.removeMouseListener(this);
149        }
150    }
151
152    @Override
153    public void eventDispatched(AWTEvent event) {
154        if (!(event instanceof KeyEvent)
155          || (event.getID() != KeyEvent.KEY_PRESSED)
156          || (layer == null)
157          || (offsetDialog != null && offsetDialog.areFieldsInFocus())) {
158            return;
159        }
160        KeyEvent kev = (KeyEvent) event;
161        int dx = 0;
162        int dy = 0;
163        switch (kev.getKeyCode()) {
164        case KeyEvent.VK_UP : dy = +1; break;
165        case KeyEvent.VK_DOWN : dy = -1; break;
166        case KeyEvent.VK_LEFT : dx = -1; break;
167        case KeyEvent.VK_RIGHT : dx = +1; break;
168        case KeyEvent.VK_ESCAPE:
169            if (offsetDialog != null) {
170                restoreOldMode = true;
171                offsetDialog.setVisible(false);
172                return;
173            }
174            break;
175        default: // Do nothing
176        }
177        if (dx != 0 || dy != 0) {
178            double ppd = layer.getPPD();
179            EastNorth d = tempOffset.getDisplacement().add(new EastNorth(dx / ppd, dy / ppd));
180            tempOffset.setDisplacement(d);
181            layer.getDisplaySettings().setOffsetBookmark(tempOffset);
182            if (offsetDialog != null) {
183                offsetDialog.updateOffset();
184            }
185            if (Logging.isDebugEnabled()) {
186                Logging.debug("{0} consuming event {1}", getClass().getName(), kev);
187            }
188            kev.consume();
189        }
190    }
191
192    @Override
193    public void mousePressed(MouseEvent e) {
194        if (e.getButton() != MouseEvent.BUTTON1)
195            return;
196
197        if (layer.isVisible()) {
198            requestFocusInMapView();
199            MapView mapView = MainApplication.getMap().mapView;
200            prevEastNorth = mapView.getEastNorth(e.getX(), e.getY());
201            mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
202        }
203    }
204
205    @Override
206    public void mouseDragged(MouseEvent e) {
207        if (layer == null || prevEastNorth == null) return;
208        EastNorth eastNorth = MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY());
209        EastNorth d = tempOffset.getDisplacement().add(eastNorth).subtract(prevEastNorth);
210        tempOffset.setDisplacement(d);
211        layer.getDisplaySettings().setOffsetBookmark(tempOffset);
212        if (offsetDialog != null) {
213            offsetDialog.updateOffset();
214        }
215        prevEastNorth = eastNorth;
216    }
217
218    @Override
219    public void mouseReleased(MouseEvent e) {
220        MapView mapView = MainApplication.getMap().mapView;
221        mapView.repaint();
222        mapView.resetCursor(this);
223        prevEastNorth = null;
224    }
225
226    @Override
227    public void actionPerformed(ActionEvent e) {
228        MapFrame map = MainApplication.getMap();
229        if (offsetDialog != null || layer == null || map == null)
230            return;
231        oldMapMode = map.mapMode;
232        super.actionPerformed(e);
233    }
234
235    private static final class ConfirmOverwriteBookmarkDialog extends ExtendedDialog {
236        ConfirmOverwriteBookmarkDialog() {
237            super(MainApplication.getMainFrame(), tr("Overwrite"), tr("Overwrite"), tr("Cancel"));
238            contentInsets = new Insets(10, 15, 10, 15);
239            setContent(tr("Offset bookmark already exists. Overwrite?"));
240            setButtonIcons("ok", "cancel");
241        }
242    }
243
244    private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
245        private final JosmTextField tOffset = new JosmTextField();
246        private final JosmTextField tBookmarkName = new JosmTextField();
247        private boolean ignoreListener;
248
249        /**
250         * Constructs a new {@code ImageryOffsetDialog}.
251         */
252        ImageryOffsetDialog() {
253            super(MainApplication.getMainFrame(),
254                    tr("Adjust imagery offset"),
255                    new String[] {tr("OK"), tr("Cancel")},
256                    false, false); // Do not dispose on close, so HIDE_ON_CLOSE remains the default behaviour and setVisible is called
257            setButtonIcons("ok", "cancel");
258            configureContextsensitiveHelp("Action/ImageryAdjust", true);
259            contentInsets = new Insets(10, 15, 5, 15);
260            JPanel pnl = new JPanel(new GridBagLayout());
261            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
262                    "You can also enter east and north offset in the {0} coordinates.\n" +
263                    "If you want to save the offset as bookmark, enter the bookmark name below",
264                    ProjectionRegistry.getProjection().toString())), GBC.eop());
265            pnl.add(new JLabel(tr("Offset:")), GBC.std());
266            pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
267            pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
268            pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
269            tOffset.setColumns(16);
270            updateOffsetIntl();
271            tOffset.addFocusListener(this);
272            setContent(pnl);
273            setupDialog();
274            setRememberWindowGeometry(getClass().getName() + ".geometry",
275                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), getSize()));
276        }
277
278        private boolean areFieldsInFocus() {
279            return tOffset.hasFocus();
280        }
281
282        @Override
283        public void focusGained(FocusEvent e) {
284            // Do nothing
285        }
286
287        @Override
288        public void focusLost(FocusEvent e) {
289            if (ignoreListener) return;
290            String ostr = tOffset.getText();
291            int semicolon = ostr.indexOf(';');
292            if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) {
293                try {
294                    String easting = ostr.substring(0, semicolon).trim();
295                    String northing = ostr.substring(semicolon + 1).trim();
296                    double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting);
297                    double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing);
298                    tempOffset.setDisplacement(new EastNorth(dx, dy));
299                    layer.getDisplaySettings().setOffsetBookmark(tempOffset);
300                } catch (NumberFormatException nfe) {
301                    // we repaint offset numbers in any case
302                    Logging.trace(nfe);
303                }
304            }
305            updateOffsetIntl();
306            if (layer != null) {
307                layer.invalidate();
308            }
309        }
310
311        private void updateOffset() {
312            ignoreListener = true;
313            updateOffsetIntl();
314            ignoreListener = false;
315        }
316
317        private void updateOffsetIntl() {
318            if (layer != null) {
319                // ROOT locale to force decimal separator to be '.'
320                tOffset.setText(layer.getDisplaySettings().getDisplacementString(Locale.ROOT));
321            }
322        }
323
324        private boolean confirmOverwriteBookmark() {
325            return new ConfirmOverwriteBookmarkDialog().showDialog().getValue() == 1;
326        }
327
328        @Override
329        protected void buttonAction(int buttonIndex, ActionEvent evt) {
330            restoreOldMode = true;
331            if (buttonIndex == 0 && !Utils.isEmpty(tBookmarkName.getText()) &&
332                    OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null &&
333                    !confirmOverwriteBookmark()) {
334                return;
335            }
336            super.buttonAction(buttonIndex, evt);
337        }
338
339        @Override
340        public void setVisible(boolean visible) {
341            super.setVisible(visible);
342            if (visible)
343                return;
344            ignoreListener = true;
345            offsetDialog = null;
346            if (layer != null) {
347                if (getValue() != 1) {
348                    layer.getDisplaySettings().setOffsetBookmark(old);
349                } else if (!Utils.isEmpty(tBookmarkName.getText())) {
350                    OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
351                }
352            }
353            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
354            restoreMapModeState();
355        }
356
357        private void restoreMapModeState() {
358            MapFrame map = MainApplication.getMap();
359            if (map == null)
360                return;
361            if (oldMapMode != null) {
362                if (restoreOldMode || (!exitingMode && getValue() == ExtendedDialog.DialogClosedOtherwise)) {
363                    map.selectMapMode(oldMapMode);
364                }
365                oldMapMode = null;
366            } else if (!exitingMode && !map.selectSelectTool(false)) {
367                exitModeAndRestoreOldMode();
368                map.mapMode = null;
369            }
370        }
371
372        private void exitModeAndRestoreOldMode() {
373            restoreOldMode = true;
374            doExitMode();
375            restoreOldMode = false;
376        }
377    }
378
379    @Override
380    public void destroy() {
381        super.destroy();
382        removeListeners();
383        this.layer = null;
384        this.oldMapMode = null;
385    }
386}