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 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            contentInsets = new Insets(10, 15, 5, 15);
259            JPanel pnl = new JPanel(new GridBagLayout());
260            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
261                    "You can also enter east and north offset in the {0} coordinates.\n" +
262                    "If you want to save the offset as bookmark, enter the bookmark name below",
263                    ProjectionRegistry.getProjection().toString())), GBC.eop());
264            pnl.add(new JLabel(tr("Offset:")), GBC.std());
265            pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
266            pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
267            pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
268            tOffset.setColumns(16);
269            updateOffsetIntl();
270            tOffset.addFocusListener(this);
271            setContent(pnl);
272            setupDialog();
273            setRememberWindowGeometry(getClass().getName() + ".geometry",
274                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), getSize()));
275        }
276
277        private boolean areFieldsInFocus() {
278            return tOffset.hasFocus();
279        }
280
281        @Override
282        public void focusGained(FocusEvent e) {
283            // Do nothing
284        }
285
286        @Override
287        public void focusLost(FocusEvent e) {
288            if (ignoreListener) return;
289            String ostr = tOffset.getText();
290            int semicolon = ostr.indexOf(';');
291            if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) {
292                try {
293                    String easting = ostr.substring(0, semicolon).trim();
294                    String northing = ostr.substring(semicolon + 1).trim();
295                    double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting);
296                    double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing);
297                    tempOffset.setDisplacement(new EastNorth(dx, dy));
298                    layer.getDisplaySettings().setOffsetBookmark(tempOffset);
299                } catch (NumberFormatException nfe) {
300                    // we repaint offset numbers in any case
301                    Logging.trace(nfe);
302                }
303            }
304            updateOffsetIntl();
305            if (layer != null) {
306                layer.invalidate();
307            }
308        }
309
310        private void updateOffset() {
311            ignoreListener = true;
312            updateOffsetIntl();
313            ignoreListener = false;
314        }
315
316        private void updateOffsetIntl() {
317            if (layer != null) {
318                // Support projections with very small numbers (e.g. 4326)
319                int precision = ProjectionRegistry.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
320                // US locale to force decimal separator to be '.'
321                try (Formatter us = new Formatter(Locale.US)) {
322                    EastNorth displacement = layer.getDisplaySettings().getDisplacement();
323                    tOffset.setText(us.format(new StringBuilder()
324                        .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
325                        displacement.east(), displacement.north()).toString());
326                }
327            }
328        }
329
330        private boolean confirmOverwriteBookmark() {
331            return new ConfirmOverwriteBookmarkDialog().showDialog().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 || (!exitingMode && 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        private void exitModeAndRestoreOldMode() {
379            restoreOldMode = true;
380            doExitMode();
381            restoreOldMode = false;
382        }
383    }
384
385    @Override
386    public void destroy() {
387        super.destroy();
388        removeListeners();
389        this.layer = null;
390        this.oldMapMode = null;
391    }
392}