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            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(new StringBuilder()
264                    .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
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)
298                return;
299            offsetDialog = null;
300            if (layer != null) {
301                if (getValue() != 1) {
302                    layer.setOffset(oldDx, oldDy);
303                } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) {
304                    OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
305                }
306            }
307            Main.main.menu.imageryMenu.refreshOffsetMenu();
308            if (Main.map == null)
309                return;
310            if (oldMapMode != null) {
311                Main.map.selectMapMode(oldMapMode);
312                oldMapMode = null;
313            } else {
314                Main.map.selectSelectTool(false);
315            }
316        }
317
318        class WindowEventHandler extends WindowAdapter {
319            @Override
320            public void windowClosing(WindowEvent e) {
321                setVisible(false);
322            }
323        }
324    }
325
326    @Override
327    public void destroy() {
328        super.destroy();
329        removeListeners();
330        this.layer = null;
331        this.oldMapMode = null;
332    }
333}