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.util.Formatter; 018import java.util.Locale; 019 020import javax.swing.JLabel; 021import javax.swing.JPanel; 022 023import org.openstreetmap.josm.Main; 024import org.openstreetmap.josm.actions.mapmode.MapMode; 025import org.openstreetmap.josm.data.coor.EastNorth; 026import org.openstreetmap.josm.data.coor.LatLon; 027import org.openstreetmap.josm.data.imagery.OffsetBookmark; 028import org.openstreetmap.josm.gui.ExtendedDialog; 029import org.openstreetmap.josm.gui.MainApplication; 030import org.openstreetmap.josm.gui.MapFrame; 031import org.openstreetmap.josm.gui.MapView; 032import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 033import org.openstreetmap.josm.gui.util.WindowGeometry; 034import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 035import org.openstreetmap.josm.gui.widgets.JosmTextField; 036import org.openstreetmap.josm.tools.GBC; 037import org.openstreetmap.josm.tools.ImageProvider; 038import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider; 039import org.openstreetmap.josm.tools.Logging; 040 041/** 042 * Adjust the position of an imagery layer. 043 * @since 3715 044 */ 045public class ImageryAdjustAction extends MapMode implements AWTEventListener { 046 private static volatile ImageryOffsetDialog offsetDialog; 047 private static Cursor cursor = ImageProvider.getCursor("normal", "move"); 048 049 private OffsetBookmark old; 050 private OffsetBookmark tempOffset; 051 private EastNorth prevEastNorth; 052 private transient AbstractTileSourceLayer<?> layer; 053 private MapMode oldMapMode; 054 private boolean exitingMode; 055 private boolean restoreOldMode; 056 057 /** 058 * Constructs a new {@code ImageryAdjustAction} for the given layer. 059 * @param layer The imagery layer 060 */ 061 public ImageryAdjustAction(AbstractTileSourceLayer<?> layer) { 062 super(tr("New offset"), "adjustimg", tr("Adjust the position of this imagery layer"), cursor); 063 putValue("toolbar", Boolean.FALSE); 064 this.layer = layer; 065 } 066 067 @Override 068 public void enterMode() { 069 super.enterMode(); 070 if (layer == null) 071 return; 072 if (!layer.isVisible()) { 073 layer.setVisible(true); 074 } 075 old = layer.getDisplaySettings().getOffsetBookmark(); 076 EastNorth curOff = old == null ? EastNorth.ZERO : old.getDisplacement(Main.getProjection()); 077 LatLon center; 078 if (MainApplication.isDisplayingMapView()) { 079 center = Main.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter()); 080 } else { 081 center = LatLon.ZERO; 082 } 083 tempOffset = new OffsetBookmark( 084 Main.getProjection().toCode(), 085 layer.getInfo().getId(), 086 layer.getInfo().getName(), 087 null, 088 curOff, center); 089 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 090 addListeners(); 091 showOffsetDialog(new ImageryOffsetDialog()); 092 } 093 094 private static void showOffsetDialog(ImageryOffsetDialog dlg) { 095 offsetDialog = dlg; 096 offsetDialog.setVisible(true); 097 } 098 099 private static void hideOffsetDialog() { 100 offsetDialog.setVisible(false); 101 offsetDialog = null; 102 } 103 104 protected void addListeners() { 105 MapView mapView = MainApplication.getMap().mapView; 106 mapView.addMouseListener(this); 107 mapView.addMouseMotionListener(this); 108 try { 109 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 110 } catch (SecurityException ex) { 111 Logging.error(ex); 112 } 113 } 114 115 @Override 116 public void exitMode() { 117 // do not restore old mode here - this is called when the new mode is already known. 118 restoreOldMode = false; 119 doExitMode(); 120 } 121 122 private void exitModeAndRestoreOldMode() { 123 restoreOldMode = true; 124 doExitMode(); 125 restoreOldMode = false; 126 } 127 128 private void doExitMode() { 129 exitingMode = true; 130 try { 131 super.exitMode(); 132 } catch (IllegalArgumentException e) { 133 Logging.trace(e); 134 } 135 if (offsetDialog != null) { 136 if (layer != null) { 137 layer.getDisplaySettings().setOffsetBookmark(old); 138 } 139 hideOffsetDialog(); 140 } 141 removeListeners(); 142 exitingMode = false; 143 } 144 145 protected void removeListeners() { 146 try { 147 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 148 } catch (SecurityException ex) { 149 Logging.error(ex); 150 } 151 if (MainApplication.isDisplayingMapView()) { 152 MapFrame map = MainApplication.getMap(); 153 map.mapView.removeMouseMotionListener(this); 154 map.mapView.removeMouseListener(this); 155 } 156 } 157 158 @Override 159 public void eventDispatched(AWTEvent event) { 160 if (!(event instanceof KeyEvent) 161 || (event.getID() != KeyEvent.KEY_PRESSED) 162 || (layer == null) 163 || (offsetDialog != null && offsetDialog.areFieldsInFocus())) { 164 return; 165 } 166 KeyEvent kev = (KeyEvent) event; 167 int dx = 0; 168 int dy = 0; 169 switch (kev.getKeyCode()) { 170 case KeyEvent.VK_UP : dy = +1; break; 171 case KeyEvent.VK_DOWN : dy = -1; break; 172 case KeyEvent.VK_LEFT : dx = -1; break; 173 case KeyEvent.VK_RIGHT : dx = +1; break; 174 case KeyEvent.VK_ESCAPE: 175 if (offsetDialog != null) { 176 restoreOldMode = true; 177 offsetDialog.setVisible(false); 178 return; 179 } 180 break; 181 default: // Do nothing 182 } 183 if (dx != 0 || dy != 0) { 184 double ppd = layer.getPPD(); 185 EastNorth d = tempOffset.getDisplacement().add(new EastNorth(dx / ppd, dy / ppd)); 186 tempOffset.setDisplacement(d); 187 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 188 if (offsetDialog != null) { 189 offsetDialog.updateOffset(); 190 } 191 if (Logging.isDebugEnabled()) { 192 Logging.debug("{0} consuming event {1}", getClass().getName(), kev); 193 } 194 kev.consume(); 195 } 196 } 197 198 @Override 199 public void mousePressed(MouseEvent e) { 200 if (e.getButton() != MouseEvent.BUTTON1) 201 return; 202 203 if (layer.isVisible()) { 204 requestFocusInMapView(); 205 MapView mapView = MainApplication.getMap().mapView; 206 prevEastNorth = mapView.getEastNorth(e.getX(), e.getY()); 207 mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 208 } 209 } 210 211 @Override 212 public void mouseDragged(MouseEvent e) { 213 if (layer == null || prevEastNorth == null) return; 214 EastNorth eastNorth = MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY()); 215 EastNorth d = tempOffset.getDisplacement().add(eastNorth).subtract(prevEastNorth); 216 tempOffset.setDisplacement(d); 217 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 218 if (offsetDialog != null) { 219 offsetDialog.updateOffset(); 220 } 221 prevEastNorth = eastNorth; 222 } 223 224 @Override 225 public void mouseReleased(MouseEvent e) { 226 MapView mapView = MainApplication.getMap().mapView; 227 mapView.repaint(); 228 mapView.resetCursor(this); 229 prevEastNorth = null; 230 } 231 232 @Override 233 public void actionPerformed(ActionEvent e) { 234 MapFrame map = MainApplication.getMap(); 235 if (offsetDialog != null || layer == null || map == null) 236 return; 237 oldMapMode = map.mapMode; 238 super.actionPerformed(e); 239 } 240 241 private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener { 242 private final JosmTextField tOffset = new JosmTextField(); 243 private final JosmTextField tBookmarkName = new JosmTextField(); 244 private boolean ignoreListener; 245 246 /** 247 * Constructs a new {@code ImageryOffsetDialog}. 248 */ 249 ImageryOffsetDialog() { 250 super(Main.parent, 251 tr("Adjust imagery offset"), 252 new String[] {tr("OK"), tr("Cancel")}, 253 false, false); // Do not dispose on close, so HIDE_ON_CLOSE remains the default behaviour and setVisible is called 254 setButtonIcons("ok", "cancel"); 255 contentInsets = new Insets(10, 15, 5, 15); 256 JPanel pnl = new JPanel(new GridBagLayout()); 257 pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" + 258 "You can also enter east and north offset in the {0} coordinates.\n" + 259 "If you want to save the offset as bookmark, enter the bookmark name below", 260 Main.getProjection().toString())), GBC.eop()); 261 pnl.add(new JLabel(tr("Offset:")), GBC.std()); 262 pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5)); 263 pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std()); 264 pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL)); 265 tOffset.setColumns(16); 266 updateOffsetIntl(); 267 tOffset.addFocusListener(this); 268 setContent(pnl); 269 setupDialog(); 270 setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, getSize())); 271 } 272 273 private boolean areFieldsInFocus() { 274 return tOffset.hasFocus(); 275 } 276 277 @Override 278 public void focusGained(FocusEvent e) { 279 // Do nothing 280 } 281 282 @Override 283 public void focusLost(FocusEvent e) { 284 if (ignoreListener) return; 285 String ostr = tOffset.getText(); 286 int semicolon = ostr.indexOf(';'); 287 if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) { 288 try { 289 String easting = ostr.substring(0, semicolon).trim(); 290 String northing = ostr.substring(semicolon + 1).trim(); 291 double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting); 292 double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing); 293 tempOffset.setDisplacement(new EastNorth(dx, dy)); 294 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 295 } catch (NumberFormatException nfe) { 296 // we repaint offset numbers in any case 297 Logging.trace(nfe); 298 } 299 } 300 updateOffsetIntl(); 301 if (layer != null) { 302 layer.invalidate(); 303 } 304 } 305 306 private void updateOffset() { 307 ignoreListener = true; 308 updateOffsetIntl(); 309 ignoreListener = false; 310 } 311 312 private void updateOffsetIntl() { 313 if (layer != null) { 314 // Support projections with very small numbers (e.g. 4326) 315 int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7; 316 // US locale to force decimal separator to be '.' 317 try (Formatter us = new Formatter(Locale.US)) { 318 EastNorth displacement = layer.getDisplaySettings().getDisplacement(); 319 tOffset.setText(us.format(new StringBuilder() 320 .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(), 321 displacement.east(), displacement.north()).toString()); 322 } 323 } 324 } 325 326 private boolean confirmOverwriteBookmark() { 327 ExtendedDialog dialog = new ExtendedDialog( 328 Main.parent, 329 tr("Overwrite"), 330 tr("Overwrite"), tr("Cancel") 331 ) { { 332 contentInsets = new Insets(10, 15, 10, 15); 333 } }; 334 dialog.setContent(tr("Offset bookmark already exists. Overwrite?")); 335 dialog.setButtonIcons("ok", "cancel"); 336 dialog.setupDialog(); 337 dialog.setVisible(true); 338 return dialog.getValue() == 1; 339 } 340 341 @Override 342 protected void buttonAction(int buttonIndex, ActionEvent evt) { 343 restoreOldMode = true; 344 if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() && 345 OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null && 346 !confirmOverwriteBookmark()) { 347 return; 348 } 349 super.buttonAction(buttonIndex, evt); 350 } 351 352 @Override 353 public void setVisible(boolean visible) { 354 super.setVisible(visible); 355 if (visible) 356 return; 357 ignoreListener = true; 358 offsetDialog = null; 359 if (layer != null) { 360 if (getValue() != 1) { 361 layer.getDisplaySettings().setOffsetBookmark(old); 362 } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) { 363 OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer); 364 } 365 } 366 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 367 restoreMapModeState(); 368 } 369 370 private void restoreMapModeState() { 371 MapFrame map = MainApplication.getMap(); 372 if (map == null) 373 return; 374 if (oldMapMode != null) { 375 if (restoreOldMode || (!exitingMode && getValue() == ExtendedDialog.DialogClosedOtherwise)) { 376 map.selectMapMode(oldMapMode); 377 } 378 oldMapMode = null; 379 } else if (!exitingMode && !map.selectSelectTool(false)) { 380 exitModeAndRestoreOldMode(); 381 map.mapMode = null; 382 } 383 } 384 } 385 386 @Override 387 public void destroy() { 388 super.destroy(); 389 removeListeners(); 390 this.layer = null; 391 this.oldMapMode = null; 392 } 393}