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}