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