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