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.actions.mapmode.MapMode;
024import org.openstreetmap.josm.data.coor.EastNorth;
025import org.openstreetmap.josm.data.coor.LatLon;
026import org.openstreetmap.josm.data.imagery.OffsetBookmark;
027import org.openstreetmap.josm.data.projection.ProjectionRegistry;
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(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 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 static final class ConfirmOverwriteBookmarkDialog extends ExtendedDialog {
242        ConfirmOverwriteBookmarkDialog() {
243            super(MainApplication.getMainFrame(), tr("Overwrite"), new String[] {tr("Overwrite"), tr("Cancel")});
244            contentInsets = new Insets(10, 15, 10, 15);
245            setContent(tr("Offset bookmark already exists. Overwrite?"));
246            setButtonIcons("ok", "cancel");
247        }
248    }
249
250    private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
251        private final JosmTextField tOffset = new JosmTextField();
252        private final JosmTextField tBookmarkName = new JosmTextField();
253        private boolean ignoreListener;
254
255        /**
256         * Constructs a new {@code ImageryOffsetDialog}.
257         */
258        ImageryOffsetDialog() {
259            super(MainApplication.getMainFrame(),
260                    tr("Adjust imagery offset"),
261                    new String[] {tr("OK"), tr("Cancel")},
262                    false, false); // Do not dispose on close, so HIDE_ON_CLOSE remains the default behaviour and setVisible is called
263            setButtonIcons("ok", "cancel");
264            contentInsets = new Insets(10, 15, 5, 15);
265            JPanel pnl = new JPanel(new GridBagLayout());
266            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
267                    "You can also enter east and north offset in the {0} coordinates.\n" +
268                    "If you want to save the offset as bookmark, enter the bookmark name below",
269                    ProjectionRegistry.getProjection().toString())), GBC.eop());
270            pnl.add(new JLabel(tr("Offset:")), GBC.std());
271            pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
272            pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
273            pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
274            tOffset.setColumns(16);
275            updateOffsetIntl();
276            tOffset.addFocusListener(this);
277            setContent(pnl);
278            setupDialog();
279            setRememberWindowGeometry(getClass().getName() + ".geometry",
280                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), getSize()));
281        }
282
283        private boolean areFieldsInFocus() {
284            return tOffset.hasFocus();
285        }
286
287        @Override
288        public void focusGained(FocusEvent e) {
289            // Do nothing
290        }
291
292        @Override
293        public void focusLost(FocusEvent e) {
294            if (ignoreListener) return;
295            String ostr = tOffset.getText();
296            int semicolon = ostr.indexOf(';');
297            if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) {
298                try {
299                    String easting = ostr.substring(0, semicolon).trim();
300                    String northing = ostr.substring(semicolon + 1).trim();
301                    double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting);
302                    double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing);
303                    tempOffset.setDisplacement(new EastNorth(dx, dy));
304                    layer.getDisplaySettings().setOffsetBookmark(tempOffset);
305                } catch (NumberFormatException nfe) {
306                    // we repaint offset numbers in any case
307                    Logging.trace(nfe);
308                }
309            }
310            updateOffsetIntl();
311            if (layer != null) {
312                layer.invalidate();
313            }
314        }
315
316        private void updateOffset() {
317            ignoreListener = true;
318            updateOffsetIntl();
319            ignoreListener = false;
320        }
321
322        private void updateOffsetIntl() {
323            if (layer != null) {
324                // Support projections with very small numbers (e.g. 4326)
325                int precision = ProjectionRegistry.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
326                // US locale to force decimal separator to be '.'
327                try (Formatter us = new Formatter(Locale.US)) {
328                    EastNorth displacement = layer.getDisplaySettings().getDisplacement();
329                    tOffset.setText(us.format(new StringBuilder()
330                        .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
331                        displacement.east(), displacement.north()).toString());
332                }
333            }
334        }
335
336        private boolean confirmOverwriteBookmark() {
337            return new ConfirmOverwriteBookmarkDialog().showDialog().getValue() == 1;
338        }
339
340        @Override
341        protected void buttonAction(int buttonIndex, ActionEvent evt) {
342            restoreOldMode = true;
343            if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() &&
344                    OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null &&
345                    !confirmOverwriteBookmark()) {
346                return;
347            }
348            super.buttonAction(buttonIndex, evt);
349        }
350
351        @Override
352        public void setVisible(boolean visible) {
353            super.setVisible(visible);
354            if (visible)
355                return;
356            ignoreListener = true;
357            offsetDialog = null;
358            if (layer != null) {
359                if (getValue() != 1) {
360                    layer.getDisplaySettings().setOffsetBookmark(old);
361                } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) {
362                    OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
363                }
364            }
365            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
366            restoreMapModeState();
367        }
368
369        private void restoreMapModeState() {
370            MapFrame map = MainApplication.getMap();
371            if (map == null)
372                return;
373            if (oldMapMode != null) {
374                if (restoreOldMode || (!exitingMode && getValue() == ExtendedDialog.DialogClosedOtherwise)) {
375                    map.selectMapMode(oldMapMode);
376                }
377                oldMapMode = null;
378            } else if (!exitingMode && !map.selectSelectTool(false)) {
379                exitModeAndRestoreOldMode();
380                map.mapMode = null;
381            }
382        }
383    }
384
385    @Override
386    public void destroy() {
387        super.destroy();
388        removeListeners();
389        this.layer = null;
390        this.oldMapMode = null;
391    }
392}