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