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