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.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 MouseListener, MouseMotionListener, AWTEventListener{
041    private static ImageryOffsetDialog offsetDialog;
042    private static Cursor cursor = ImageProvider.getCursor("normal", "move");
043
044    private double oldDx, oldDy;
045    private EastNorth prevEastNorth;
046    private 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", 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, dy = 0;
121        switch (kev.getKeyCode()) {
122        case KeyEvent.VK_UP : dy = +1; break;
123        case KeyEvent.VK_DOWN : dy = -1; break;
124        case KeyEvent.VK_LEFT : dx = -1; break;
125        case KeyEvent.VK_RIGHT : dx = +1; break;
126        }
127        if (dx != 0 || dy != 0) {
128            double ppd = layer.getPPD();
129            layer.displace(dx / ppd, dy / ppd);
130            if (offsetDialog != null) {
131                offsetDialog.updateOffset();
132            }
133            kev.consume();
134            Main.map.repaint();
135        }
136    }
137
138    @Override
139    public void mousePressed(MouseEvent e) {
140        if (e.getButton() != MouseEvent.BUTTON1)
141            return;
142
143        if (layer.isVisible()) {
144            requestFocusInMapView();
145            prevEastNorth=Main.map.mapView.getEastNorth(e.getX(),e.getY());
146            Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
147        }
148    }
149
150    @Override
151    public void mouseDragged(MouseEvent e) {
152        if (layer == null || prevEastNorth == null) return;
153        EastNorth eastNorth =
154            Main.map.mapView.getEastNorth(e.getX(),e.getY());
155        double dx = layer.getDx()+eastNorth.east()-prevEastNorth.east();
156        double dy = layer.getDy()+eastNorth.north()-prevEastNorth.north();
157        layer.setOffset(dx, dy);
158        if (offsetDialog != null) {
159            offsetDialog.updateOffset();
160        }
161        Main.map.repaint();
162        prevEastNorth = eastNorth;
163    }
164
165    @Override
166    public void mouseReleased(MouseEvent e) {
167        Main.map.mapView.repaint();
168        Main.map.mapView.resetCursor(this);
169        prevEastNorth = null;
170    }
171
172    @Override
173    public void actionPerformed(ActionEvent e) {
174        if (offsetDialog != null || layer == null || Main.map == null)
175            return;
176        oldMapMode = Main.map.mapMode;
177        super.actionPerformed(e);
178    }
179
180    private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
181        private final JosmTextField tOffset = new JosmTextField();
182        private final JosmTextField tBookmarkName = new JosmTextField();
183        private boolean ignoreListener;
184
185        /**
186         * Constructs a new {@code ImageryOffsetDialog}.
187         */
188        public ImageryOffsetDialog() {
189            super(Main.parent,
190                    tr("Adjust imagery offset"),
191                    new String[] { tr("OK"),tr("Cancel") },
192                    false);
193            setButtonIcons(new String[] { "ok", "cancel" });
194            contentInsets = new Insets(10, 15, 5, 15);
195            JPanel pnl = new JPanel(new GridBagLayout());
196            pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
197                    "You can also enter east and north offset in the {0} coordinates.\n" +
198                    "If you want to save the offset as bookmark, enter the bookmark name below",
199                    Main.getProjection().toString())), GBC.eop());
200            pnl.add(new JLabel(tr("Offset: ")),GBC.std());
201            pnl.add(tOffset,GBC.eol().fill(GBC.HORIZONTAL).insets(0,0,0,5));
202            pnl.add(new JLabel(tr("Bookmark name: ")),GBC.std());
203            pnl.add(tBookmarkName,GBC.eol().fill(GBC.HORIZONTAL));
204            tOffset.setColumns(16);
205            updateOffsetIntl();
206            tOffset.addFocusListener(this);
207            setContent(pnl);
208            setupDialog();
209        }
210
211        private boolean areFieldsInFocus() {
212            return tOffset.hasFocus();
213        }
214
215        @Override
216        public void focusGained(FocusEvent e) {
217            // Do nothing
218        }
219
220        @Override
221        public void focusLost(FocusEvent e) {
222            if (ignoreListener) return;
223            String ostr = tOffset.getText();
224            int semicolon = ostr.indexOf(';');
225            if( semicolon >= 0 && semicolon + 1 < ostr.length() ) {
226                try {
227                    // here we assume that Double.parseDouble() needs '.' as a decimal separator
228                    String easting = ostr.substring(0, semicolon).trim().replace(',', '.');
229                    String northing = ostr.substring(semicolon + 1).trim().replace(',', '.');
230                    double dx = Double.parseDouble(easting);
231                    double dy = Double.parseDouble(northing);
232                    layer.setOffset(dx, dy);
233                } catch (NumberFormatException nfe) {
234                    // we repaint offset numbers in any case
235                }
236            }
237            updateOffsetIntl();
238            if (Main.isDisplayingMapView()) {
239                Main.map.repaint();
240            }
241        }
242
243        private final void updateOffset() {
244            ignoreListener = true;
245            updateOffsetIntl();
246            ignoreListener = false;
247        }
248
249        private final void updateOffsetIntl() {
250            // Support projections with very small numbers (e.g. 4326)
251            int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
252            // US locale to force decimal separator to be '.'
253            try (Formatter us = new Formatter(Locale.US)) {
254                tOffset.setText(us.format(
255                    "%1." + precision + "f; %1." + precision + "f",
256                    layer.getDx(), layer.getDy()).toString());
257            }
258        }
259
260        private boolean confirmOverwriteBookmark() {
261            ExtendedDialog dialog = new ExtendedDialog(
262                    Main.parent,
263                    tr("Overwrite"),
264                    new String[] {tr("Overwrite"), tr("Cancel")}
265            ) {{
266                contentInsets = new Insets(10, 15, 10, 15);
267            }};
268            dialog.setContent(tr("Offset bookmark already exists. Overwrite?"));
269            dialog.setButtonIcons(new String[] {"ok.png", "cancel.png"});
270            dialog.setupDialog();
271            dialog.setVisible(true);
272            return dialog.getValue() == 1;
273        }
274
275        @Override
276        protected void buttonAction(int buttonIndex, ActionEvent evt) {
277            if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() &&
278                    OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null &&
279                    !confirmOverwriteBookmark()) {
280                return;
281            }
282            super.buttonAction(buttonIndex, evt);
283        }
284
285        @Override
286        public void setVisible(boolean visible) {
287            super.setVisible(visible);
288            if (visible) return;
289            offsetDialog = null;
290            if (getValue() != 1) {
291                layer.setOffset(oldDx, oldDy);
292            } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) {
293                OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
294            }
295            Main.main.menu.imageryMenu.refreshOffsetMenu();
296            if (Main.map == null) return;
297            if (oldMapMode != null) {
298                Main.map.selectMapMode(oldMapMode);
299                oldMapMode = null;
300            } else {
301                Main.map.selectSelectTool(false);
302            }
303        }
304    }
305
306    @Override
307    public void destroy() {
308        super.destroy();
309        removeListeners();
310        this.layer = null;
311        this.oldMapMode = null;
312    }
313}