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