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.ImageryLayer;
031import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
032import org.openstreetmap.josm.gui.widgets.JosmTextField;
033import org.openstreetmap.josm.tools.GBC;
034import org.openstreetmap.josm.tools.ImageProvider;
035
036/**
037 * Adjust the position of an imagery layer.
038 * @since 3715
039 */
040public class ImageryAdjustAction extends MapMode implements AWTEventListener {
041    private static volatile ImageryOffsetDialog offsetDialog;
042    private static Cursor cursor = ImageProvider.getCursor("normal", "move");
043
044    private double oldDx, oldDy;
045    private EastNorth prevEastNorth;
046    private transient ImageryLayer layer;
047    private MapMode oldMapMode;
048
049    /**
050     * Constructs a new {@code ImageryAdjustAction} for the given layer.
051     * @param layer The imagery layer
052     */
053    public ImageryAdjustAction(ImageryLayer layer) {
054        super(tr("New offset"), "adjustimg",
055                tr("Adjust the position of this imagery layer"), Main.map,
056                cursor);
057        putValue("toolbar", Boolean.FALSE);
058        this.layer = layer;
059    }
060
061    @Override
062    public void enterMode() {
063        super.enterMode();
064        if (layer == null)
065            return;
066        if (!layer.isVisible()) {
067            layer.setVisible(true);
068        }
069        oldDx = layer.getDx();
070        oldDy = layer.getDy();
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.setOffset(oldDx, oldDy);
092            }
093            offsetDialog.setVisible(false);
094            offsetDialog = null;
095        }
096        removeListeners();
097    }
098
099    protected void removeListeners() {
100        try {
101            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
102        } catch (SecurityException ex) {
103            Main.error(ex);
104        }
105        if (Main.isDisplayingMapView()) {
106            Main.map.mapView.removeMouseMotionListener(this);
107            Main.map.mapView.removeMouseListener(this);
108        }
109    }
110
111    @Override
112    public void eventDispatched(AWTEvent event) {
113        if (!(event instanceof KeyEvent)
114          || (event.getID() != KeyEvent.KEY_PRESSED)
115          || (layer == null)
116          || (offsetDialog != null && offsetDialog.areFieldsInFocus())) {
117            return;
118        }
119        KeyEvent kev = (KeyEvent) event;
120        int dx = 0;
121        int dy = 0;
122        switch (kev.getKeyCode()) {
123        case KeyEvent.VK_UP : dy = +1; break;
124        case KeyEvent.VK_DOWN : dy = -1; break;
125        case KeyEvent.VK_LEFT : dx = -1; break;
126        case KeyEvent.VK_RIGHT : dx = +1; break;
127        default: // Do nothing
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        }
140    }
141
142    @Override
143    public void mousePressed(MouseEvent e) {
144        if (e.getButton() != MouseEvent.BUTTON1)
145            return;
146
147        if (layer.isVisible()) {
148            requestFocusInMapView();
149            prevEastNorth = Main.map.mapView.getEastNorth(e.getX(), e.getY());
150            Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
151        }
152    }
153
154    @Override
155    public void mouseDragged(MouseEvent e) {
156        if (layer == null || prevEastNorth == null) return;
157        EastNorth eastNorth =
158            Main.map.mapView.getEastNorth(e.getX(), e.getY());
159        double dx = layer.getDx()+eastNorth.east()-prevEastNorth.east();
160        double dy = layer.getDy()+eastNorth.north()-prevEastNorth.north();
161        layer.setOffset(dx, dy);
162        if (offsetDialog != null) {
163            offsetDialog.updateOffset();
164        }
165        prevEastNorth = eastNorth;
166    }
167
168    @Override
169    public void mouseReleased(MouseEvent e) {
170        Main.map.mapView.repaint();
171        Main.map.mapView.resetCursor(this);
172        prevEastNorth = null;
173    }
174
175    @Override
176    public void actionPerformed(ActionEvent e) {
177        if (offsetDialog != null || layer == null || Main.map == null)
178            return;
179        oldMapMode = Main.map.mapMode;
180        super.actionPerformed(e);
181    }
182
183    private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
184        private final JosmTextField tOffset = new JosmTextField();
185        private final JosmTextField tBookmarkName = new JosmTextField();
186        private boolean ignoreListener;
187
188        /**
189         * Constructs a new {@code ImageryOffsetDialog}.
190         */
191        ImageryOffsetDialog() {
192            super(Main.parent,
193                    tr("Adjust imagery offset"),
194                    new String[] {tr("OK"), tr("Cancel")},
195                    false);
196            setButtonIcons(new String[] {"ok", "cancel"});
197            contentInsets = new Insets(10, 15, 5, 15);
198            JPanel pnl = new JPanel(new GridBagLayout());
199            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
200                    "You can also enter east and north offset in the {0} coordinates.\n" +
201                    "If you want to save the offset as bookmark, enter the bookmark name below",
202                    Main.getProjection().toString())), GBC.eop());
203            pnl.add(new JLabel(tr("Offset: ")), GBC.std());
204            pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
205            pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
206            pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
207            tOffset.setColumns(16);
208            updateOffsetIntl();
209            tOffset.addFocusListener(this);
210            setContent(pnl);
211            setupDialog();
212            addWindowListener(new WindowEventHandler());
213        }
214
215        private boolean areFieldsInFocus() {
216            return tOffset.hasFocus();
217        }
218
219        @Override
220        public void focusGained(FocusEvent e) {
221            // Do nothing
222        }
223
224        @Override
225        public void focusLost(FocusEvent e) {
226            if (ignoreListener) return;
227            String ostr = tOffset.getText();
228            int semicolon = ostr.indexOf(';');
229            if (semicolon >= 0 && semicolon + 1 < ostr.length()) {
230                try {
231                    // here we assume that Double.parseDouble() needs '.' as a decimal separator
232                    String easting = ostr.substring(0, semicolon).trim().replace(',', '.');
233                    String northing = ostr.substring(semicolon + 1).trim().replace(',', '.');
234                    double dx = Double.parseDouble(easting);
235                    double dy = Double.parseDouble(northing);
236                    layer.setOffset(dx, dy);
237                } catch (NumberFormatException nfe) {
238                    // we repaint offset numbers in any case
239                    if (Main.isTraceEnabled()) {
240                        Main.trace(nfe.getMessage());
241                    }
242                }
243            }
244            updateOffsetIntl();
245            if (Main.isDisplayingMapView()) {
246                Main.map.repaint();
247            }
248        }
249
250        private void updateOffset() {
251            ignoreListener = true;
252            updateOffsetIntl();
253            ignoreListener = false;
254        }
255
256        private void updateOffsetIntl() {
257            // Support projections with very small numbers (e.g. 4326)
258            int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
259            // US locale to force decimal separator to be '.'
260            try (Formatter us = new Formatter(Locale.US)) {
261                tOffset.setText(us.format(new StringBuilder()
262                    .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
263                    layer.getDx(), layer.getDy()).toString());
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        }
291
292        @Override
293        public void setVisible(boolean visible) {
294            super.setVisible(visible);
295            if (visible)
296                return;
297            offsetDialog = null;
298            if (layer != 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            }
305            Main.main.menu.imageryMenu.refreshOffsetMenu();
306            if (Main.map == null)
307                return;
308            if (oldMapMode != null) {
309                Main.map.selectMapMode(oldMapMode);
310                oldMapMode = null;
311            } else {
312                Main.map.selectSelectTool(false);
313            }
314        }
315
316        class WindowEventHandler extends WindowAdapter {
317            @Override
318            public void windowClosing(WindowEvent e) {
319                setVisible(false);
320            }
321        }
322    }
323
324    @Override
325    public void destroy() {
326        super.destroy();
327        removeListeners();
328        this.layer = null;
329        this.oldMapMode = null;
330    }
331}