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