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().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 exitModeAndRestoreOldMode() {
123        restoreOldMode = true;
124        doExitMode();
125        restoreOldMode = false;
126    }
127
128    private void doExitMode() {
129        exitingMode = true;
130        try {
131            super.exitMode();
132        } catch (IllegalArgumentException e) {
133            Logging.trace(e);
134        }
135        if (offsetDialog != null) {
136            if (layer != null) {
137                layer.getDisplaySettings().setOffsetBookmark(old);
138            }
139            hideOffsetDialog();
140        }
141        removeListeners();
142        exitingMode = false;
143    }
144
145    protected void removeListeners() {
146        try {
147            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
148        } catch (SecurityException ex) {
149            Logging.error(ex);
150        }
151        if (MainApplication.isDisplayingMapView()) {
152            MapFrame map = MainApplication.getMap();
153            map.mapView.removeMouseMotionListener(this);
154            map.mapView.removeMouseListener(this);
155        }
156    }
157
158    @Override
159    public void eventDispatched(AWTEvent event) {
160        if (!(event instanceof KeyEvent)
161          || (event.getID() != KeyEvent.KEY_PRESSED)
162          || (layer == null)
163          || (offsetDialog != null && offsetDialog.areFieldsInFocus())) {
164            return;
165        }
166        KeyEvent kev = (KeyEvent) event;
167        int dx = 0;
168        int dy = 0;
169        switch (kev.getKeyCode()) {
170        case KeyEvent.VK_UP : dy = +1; break;
171        case KeyEvent.VK_DOWN : dy = -1; break;
172        case KeyEvent.VK_LEFT : dx = -1; break;
173        case KeyEvent.VK_RIGHT : dx = +1; break;
174        case KeyEvent.VK_ESCAPE:
175            if (offsetDialog != null) {
176                restoreOldMode = true;
177                offsetDialog.setVisible(false);
178                return;
179            }
180            break;
181        default: // Do nothing
182        }
183        if (dx != 0 || dy != 0) {
184            double ppd = layer.getPPD();
185            EastNorth d = tempOffset.getDisplacement().add(new EastNorth(dx / ppd, dy / ppd));
186            tempOffset.setDisplacement(d);
187            layer.getDisplaySettings().setOffsetBookmark(tempOffset);
188            if (offsetDialog != null) {
189                offsetDialog.updateOffset();
190            }
191            if (Logging.isDebugEnabled()) {
192                Logging.debug("{0} consuming event {1}", getClass().getName(), kev);
193            }
194            kev.consume();
195        }
196    }
197
198    @Override
199    public void mousePressed(MouseEvent e) {
200        if (e.getButton() != MouseEvent.BUTTON1)
201            return;
202
203        if (layer.isVisible()) {
204            requestFocusInMapView();
205            MapView mapView = MainApplication.getMap().mapView;
206            prevEastNorth = mapView.getEastNorth(e.getX(), e.getY());
207            mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
208        }
209    }
210
211    @Override
212    public void mouseDragged(MouseEvent e) {
213        if (layer == null || prevEastNorth == null) return;
214        EastNorth eastNorth = MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY());
215        EastNorth d = tempOffset.getDisplacement().add(eastNorth).subtract(prevEastNorth);
216        tempOffset.setDisplacement(d);
217        layer.getDisplaySettings().setOffsetBookmark(tempOffset);
218        if (offsetDialog != null) {
219            offsetDialog.updateOffset();
220        }
221        prevEastNorth = eastNorth;
222    }
223
224    @Override
225    public void mouseReleased(MouseEvent e) {
226        MapView mapView = MainApplication.getMap().mapView;
227        mapView.repaint();
228        mapView.resetCursor(this);
229        prevEastNorth = null;
230    }
231
232    @Override
233    public void actionPerformed(ActionEvent e) {
234        MapFrame map = MainApplication.getMap();
235        if (offsetDialog != null || layer == null || map == null)
236            return;
237        oldMapMode = map.mapMode;
238        super.actionPerformed(e);
239    }
240
241    private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
242        private final JosmTextField tOffset = new JosmTextField();
243        private final JosmTextField tBookmarkName = new JosmTextField();
244        private boolean ignoreListener;
245
246        /**
247         * Constructs a new {@code ImageryOffsetDialog}.
248         */
249        ImageryOffsetDialog() {
250            super(Main.parent,
251                    tr("Adjust imagery offset"),
252                    new String[] {tr("OK"), tr("Cancel")},
253                    false, false); // Do not dispose on close, so HIDE_ON_CLOSE remains the default behaviour and setVisible is called
254            setButtonIcons("ok", "cancel");
255            contentInsets = new Insets(10, 15, 5, 15);
256            JPanel pnl = new JPanel(new GridBagLayout());
257            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
258                    "You can also enter east and north offset in the {0} coordinates.\n" +
259                    "If you want to save the offset as bookmark, enter the bookmark name below",
260                    Main.getProjection().toString())), GBC.eop());
261            pnl.add(new JLabel(tr("Offset:")), GBC.std());
262            pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
263            pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
264            pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
265            tOffset.setColumns(16);
266            updateOffsetIntl();
267            tOffset.addFocusListener(this);
268            setContent(pnl);
269            setupDialog();
270            setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, getSize()));
271        }
272
273        private boolean areFieldsInFocus() {
274            return tOffset.hasFocus();
275        }
276
277        @Override
278        public void focusGained(FocusEvent e) {
279            // Do nothing
280        }
281
282        @Override
283        public void focusLost(FocusEvent e) {
284            if (ignoreListener) return;
285            String ostr = tOffset.getText();
286            int semicolon = ostr.indexOf(';');
287            if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) {
288                try {
289                    String easting = ostr.substring(0, semicolon).trim();
290                    String northing = ostr.substring(semicolon + 1).trim();
291                    double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting);
292                    double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing);
293                    tempOffset.setDisplacement(new EastNorth(dx, dy));
294                    layer.getDisplaySettings().setOffsetBookmark(tempOffset);
295                } catch (NumberFormatException nfe) {
296                    // we repaint offset numbers in any case
297                    Logging.trace(nfe);
298                }
299            }
300            updateOffsetIntl();
301            if (layer != null) {
302                layer.invalidate();
303            }
304        }
305
306        private void updateOffset() {
307            ignoreListener = true;
308            updateOffsetIntl();
309            ignoreListener = false;
310        }
311
312        private void updateOffsetIntl() {
313            if (layer != null) {
314                // Support projections with very small numbers (e.g. 4326)
315                int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
316                // US locale to force decimal separator to be '.'
317                try (Formatter us = new Formatter(Locale.US)) {
318                    EastNorth displacement = layer.getDisplaySettings().getDisplacement();
319                    tOffset.setText(us.format(new StringBuilder()
320                        .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
321                        displacement.east(), displacement.north()).toString());
322                }
323            }
324        }
325
326        private boolean confirmOverwriteBookmark() {
327            ExtendedDialog dialog = new ExtendedDialog(
328                    Main.parent,
329                    tr("Overwrite"),
330                    tr("Overwrite"), tr("Cancel")
331            ) { {
332                contentInsets = new Insets(10, 15, 10, 15);
333            } };
334            dialog.setContent(tr("Offset bookmark already exists. Overwrite?"));
335            dialog.setButtonIcons("ok", "cancel");
336            dialog.setupDialog();
337            dialog.setVisible(true);
338            return dialog.getValue() == 1;
339        }
340
341        @Override
342        protected void buttonAction(int buttonIndex, ActionEvent evt) {
343            restoreOldMode = true;
344            if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() &&
345                    OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null &&
346                    !confirmOverwriteBookmark()) {
347                return;
348            }
349            super.buttonAction(buttonIndex, evt);
350        }
351
352        @Override
353        public void setVisible(boolean visible) {
354            super.setVisible(visible);
355            if (visible)
356                return;
357            ignoreListener = true;
358            offsetDialog = null;
359            if (layer != null) {
360                if (getValue() != 1) {
361                    layer.getDisplaySettings().setOffsetBookmark(old);
362                } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) {
363                    OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
364                }
365            }
366            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
367            restoreMapModeState();
368        }
369
370        private void restoreMapModeState() {
371            MapFrame map = MainApplication.getMap();
372            if (map == null)
373                return;
374            if (oldMapMode != null) {
375                if (restoreOldMode || (!exitingMode && getValue() == ExtendedDialog.DialogClosedOtherwise)) {
376                    map.selectMapMode(oldMapMode);
377                }
378                oldMapMode = null;
379            } else if (!exitingMode && !map.selectSelectTool(false)) {
380                exitModeAndRestoreOldMode();
381                map.mapMode = null;
382            }
383        }
384    }
385
386    @Override
387    public void destroy() {
388        super.destroy();
389        removeListeners();
390        this.layer = null;
391        this.oldMapMode = null;
392    }
393}