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