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