001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.gui; 003 004import java.awt.BorderLayout; 005import java.awt.Color; 006import java.awt.Component; 007import java.awt.event.ActionEvent; 008import java.awt.image.BufferedImage; 009import java.io.ByteArrayInputStream; 010import java.io.IOException; 011import java.text.MessageFormat; 012import java.util.Arrays; 013import java.util.List; 014 015import javax.imageio.ImageIO; 016import javax.swing.AbstractAction; 017import javax.swing.Action; 018import javax.swing.JComponent; 019import javax.swing.KeyStroke; 020import javax.swing.SwingUtilities; 021 022import org.apache.log4j.Logger; 023import org.openstreetmap.josm.data.cache.CacheEntry; 024import org.openstreetmap.josm.data.cache.CacheEntryAttributes; 025import org.openstreetmap.josm.data.cache.ICachedLoaderListener; 026import org.openstreetmap.josm.gui.SideButton; 027import org.openstreetmap.josm.gui.dialogs.ToggleDialog; 028import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage; 029import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener; 030import org.openstreetmap.josm.plugins.streetside.StreetsideImage; 031import org.openstreetmap.josm.plugins.streetside.StreetsideLayer; 032import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin; 033import org.openstreetmap.josm.plugins.streetside.actions.WalkListener; 034import org.openstreetmap.josm.plugins.streetside.actions.WalkThread; 035import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache; 036import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoHelpPopup; 037import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties; 038import org.openstreetmap.josm.tools.I18n; 039import org.openstreetmap.josm.tools.ImageProvider; 040 041/** 042 * Toggle dialog that shows an image and some buttons. 043 * 044 * @author nokutu 045 * @author renerr18 046 */ 047public final class StreetsideMainDialog extends ToggleDialog implements 048 ICachedLoaderListener, StreetsideDataListener { 049 050 private static final long serialVersionUID = 2645654786827812861L; 051 052 final static Logger logger = Logger.getLogger(StreetsideMainDialog.class); 053 054 public static final String BASE_TITLE = I18n.marktr("Microsoft Streetside image"); 055 056 private static final String MESSAGE_SEPARATOR = " — "; 057 058 private static StreetsideMainDialog instance; 059 060 private volatile StreetsideAbstractImage image; 061 062 private final SideButton nextButton = new SideButton(new NextPictureAction()); 063 private final SideButton previousButton = new SideButton(new PreviousPictureAction()); 064 /** 065 * Button used to jump to the image following the red line 066 */ 067 public final SideButton redButton = new SideButton(new RedAction()); 068 /** 069 * Button used to jump to the image following the blue line 070 */ 071 public final SideButton blueButton = new SideButton(new BlueAction()); 072 073 private final SideButton playButton = new SideButton(new PlayAction()); 074 private final SideButton pauseButton = new SideButton(new PauseAction()); 075 private final SideButton stopButton = new SideButton(new StopAction()); 076 077 private ImageInfoHelpPopup imageInfoHelp; 078 079 /** 080 * Buttons mode. 081 * 082 * @author nokutu 083 */ 084 public enum MODE { 085 /** 086 * Standard mode to view pictures. 087 */ 088 NORMAL, 089 /** 090 * Mode when in walk. 091 */ 092 WALK 093 } 094 095 /** 096 * Object containing the shown image and that handles zoom and drag 097 */ 098 public StreetsideImageDisplay streetsideImageDisplay; 099 100 private StreetsideCache imageCache; 101 public StreetsideCache thumbnailCache; 102 103 private StreetsideMainDialog() { 104 super(I18n.tr(StreetsideMainDialog.BASE_TITLE), "streetside-main", I18n.tr("Open Streetside window"), null, 200, 105 true, StreetsidePreferenceSetting.class); 106 addShortcuts(); 107 108 streetsideImageDisplay = new StreetsideImageDisplay(); 109 110 blueButton.setForeground(Color.BLUE); 111 redButton.setForeground(Color.RED); 112 113 setMode(MODE.NORMAL); 114 } 115 116 /** 117 * Adds the shortcuts to the buttons. 118 */ 119 private void addShortcuts() { 120 nextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_DOWN"), "next"); 121 nextButton.getActionMap().put("next", new NextPictureAction()); 122 previousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_UP"), "previous"); 123 previousButton.getActionMap().put("previous", new PreviousPictureAction()); 124 blueButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_UP"), "blue"); 125 blueButton.getActionMap().put("blue", new BlueAction()); 126 redButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_DOWN"), "red"); 127 redButton.getActionMap().put("red", new RedAction()); 128 } 129 130 /** 131 * Returns the unique instance of the class. 132 * 133 * @return The unique instance of the class. 134 */ 135 public static synchronized StreetsideMainDialog getInstance() { 136 if (StreetsideMainDialog.instance == null) { 137 StreetsideMainDialog.instance = new StreetsideMainDialog(); 138 } 139 return StreetsideMainDialog.instance; 140 } 141 142 /** 143 * @return true, iff the singleton instance is present 144 */ 145 public static boolean hasInstance() { 146 return StreetsideMainDialog.instance != null; 147 } 148 149 public synchronized void setImageInfoHelp(ImageInfoHelpPopup popup) { 150 imageInfoHelp = popup; 151 } 152 153 /** 154 * Sets a new mode for the dialog. 155 * 156 * @param mode The mode to be set. Must not be {@code null}. 157 */ 158 public void setMode(MODE mode) { 159 switch (mode) { 160 case WALK: 161 createLayout( 162 streetsideImageDisplay, 163 Arrays.asList(playButton, pauseButton, stopButton) 164 ); 165 break; 166 case NORMAL: 167 default: 168 createLayout( 169 streetsideImageDisplay, 170 Arrays.asList(blueButton, previousButton, nextButton, redButton) 171 ); 172 break; 173 } 174 disableAllButtons(); 175 if (MODE.NORMAL.equals(mode)) { 176 updateImage(); 177 } 178 revalidate(); 179 repaint(); 180 } 181 182 /** 183 * Destroys the unique instance of the class. 184 */ 185 public static synchronized void destroyInstance() { 186 StreetsideMainDialog.instance = null; 187 } 188 189 /** 190 * Downloads the full quality picture of the selected StreetsideImage and sets 191 * in the StreetsideImageDisplay object. 192 */ 193 public synchronized void updateImage() { 194 updateImage(true); 195 } 196 197 /** 198 * Downloads the picture of the selected StreetsideImage and sets in the 199 * StreetsideImageDisplay object. 200 * 201 * @param fullQuality If the full quality picture must be downloaded or just the 202 * thumbnail. 203 */ 204 public synchronized void updateImage(boolean fullQuality) { 205 if (!SwingUtilities.isEventDispatchThread()) { 206 SwingUtilities.invokeLater(this::updateImage); 207 } else { 208 if (!StreetsideLayer.hasInstance()) { 209 return; 210 } 211 if (image == null) { 212 streetsideImageDisplay.setImage(null, null); 213 setTitle(I18n.tr(StreetsideMainDialog.BASE_TITLE)); 214 return; 215 } 216 217 if (imageInfoHelp != null && StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() > 0 && imageInfoHelp.showPopup()) { 218 // Count down the number of times the popup will be displayed 219 StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() - 1); 220 } 221 222 if (image instanceof StreetsideImage) { 223 final StreetsideImage streetsideImage = (StreetsideImage) image; 224 // Downloads the thumbnail. 225 streetsideImageDisplay.setImage(null, null); 226 if (thumbnailCache != null) { 227 thumbnailCache.cancelOutstandingTasks(); 228 } 229 thumbnailCache = new StreetsideCache(streetsideImage.getId(), 230 StreetsideCache.Type.THUMBNAIL); 231 try { 232 thumbnailCache.submit(this, false); 233 } catch (final IOException e) { 234 logger.error(e); 235 } 236 237 // Downloads the full resolution image. 238 if (fullQuality || new StreetsideCache(streetsideImage.getId(), 239 StreetsideCache.Type.FULL_IMAGE).get() != null) { 240 if (imageCache != null) { 241 imageCache.cancelOutstandingTasks(); 242 } 243 imageCache = new StreetsideCache(streetsideImage.getId(), 244 StreetsideCache.Type.FULL_IMAGE); 245 try { 246 imageCache.submit(this, false); 247 } catch (final IOException e) { 248 logger.error(e); 249 } 250 } 251 } 252 updateTitle(); 253 } 254 } 255 256 /** 257 * Disables all the buttons in the dialog 258 */ 259 private void disableAllButtons() { 260 nextButton.setEnabled(false); 261 previousButton.setEnabled(false); 262 blueButton.setEnabled(false); 263 redButton.setEnabled(false); 264 } 265 266 /** 267 * Sets a new StreetsideImage to be shown. 268 * 269 * @param image The image to be shown. 270 */ 271 public synchronized void setImage(StreetsideAbstractImage image) { 272 this.image = image; 273 } 274 275 /** 276 * Updates the title of the dialog. 277 */ 278 public synchronized void updateTitle() { 279 if (!SwingUtilities.isEventDispatchThread()) { 280 SwingUtilities.invokeLater(this::updateTitle); 281 } else if (image != null) { 282 final StringBuilder title = new StringBuilder(I18n.tr(StreetsideMainDialog.BASE_TITLE)); 283 if (image instanceof StreetsideImage) { 284 title.append(StreetsideMainDialog.MESSAGE_SEPARATOR).append(MessageFormat.format("(heading {0}°)",Double.toString(image.getHe()))); 285 setTitle(title.toString()); 286 } 287 } 288 } 289 290 /** 291 * Returns the {@link StreetsideAbstractImage} object which is being shown. 292 * 293 * @return The {@link StreetsideAbstractImage} object which is being shown. 294 */ 295 public synchronized StreetsideAbstractImage getImage() { 296 return image; 297 } 298 299 /** 300 * Action class form the next image button. 301 * 302 * @author nokutu 303 */ 304 private static class NextPictureAction extends AbstractAction { 305 306 private static final long serialVersionUID = 6333692154558730392L; 307 308 /** 309 * Constructs a normal NextPictureAction 310 */ 311 NextPictureAction() { 312 super(I18n.tr("Next picture")); 313 putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the next picture in the sequence")); 314 new ImageProvider("help", "next").getResource().attachImageIcon(this, true); 315 } 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 StreetsideLayer.getInstance().getData().selectNext(); 320 } 321 } 322 323 /** 324 * Action class for the previous image button. 325 * 326 * @author nokutu 327 */ 328 private static class PreviousPictureAction extends AbstractAction { 329 330 private static final long serialVersionUID = 4390593660514657107L; 331 332 /** 333 * Constructs a normal PreviousPictureAction 334 */ 335 PreviousPictureAction() { 336 super(I18n.tr("Previous picture")); 337 putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the previous picture in the sequence")); 338 new ImageProvider("help", "previous").getResource().attachImageIcon(this, true); 339 } 340 341 @Override 342 public void actionPerformed(ActionEvent e) { 343 StreetsideLayer.getInstance().getData().selectPrevious(); 344 } 345 } 346 347 /** 348 * Action class to jump to the image following the red line. 349 * 350 * @author nokutu 351 */ 352 private static class RedAction extends AbstractAction { 353 354 private static final long serialVersionUID = -1244456062285831231L; 355 356 /** 357 * Constructs a normal RedAction 358 */ 359 RedAction() { 360 putValue(Action.NAME, I18n.tr("Jump to red")); 361 putValue(Action.SHORT_DESCRIPTION, 362 I18n.tr("Jumps to the picture at the other side of the red line")); 363 new ImageProvider("dialogs", "red").getResource().attachImageIcon(this, true); 364 } 365 366 @Override 367 public void actionPerformed(ActionEvent e) { 368 if (StreetsideMainDialog.getInstance().getImage() != null) { 369 StreetsideLayer.getInstance().getData() 370 .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(1), true); 371 } 372 } 373 } 374 375 /** 376 * Action class to jump to the image following the blue line. 377 * 378 * @author nokutu 379 */ 380 private static class BlueAction extends AbstractAction { 381 382 private static final long serialVersionUID = 5951233534212838780L; 383 384 /** 385 * Constructs a normal BlueAction 386 */ 387 BlueAction() { 388 putValue(Action.NAME, I18n.tr("Jump to blue")); 389 putValue(Action.SHORT_DESCRIPTION, 390 I18n.tr("Jumps to the picture at the other side of the blue line")); 391 new ImageProvider("dialogs", "blue").getResource().attachImageIcon(this, true); 392 } 393 394 @Override 395 public void actionPerformed(ActionEvent e) { 396 if (StreetsideMainDialog.getInstance().getImage() != null) { 397 StreetsideLayer.getInstance().getData() 398 .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(2), true); 399 } 400 } 401 } 402 403 private static class StopAction extends AbstractAction implements WalkListener { 404 405 private static final long serialVersionUID = 8789972456611625341L; 406 407 private WalkThread thread; 408 409 /** 410 * Constructs a normal StopAction 411 */ 412 StopAction() { 413 putValue(Action.NAME, I18n.tr("Stop")); 414 putValue(Action.SHORT_DESCRIPTION, I18n.tr("Stops the walk.")); 415 new ImageProvider("dialogs/streetsideStop.png").getResource().attachImageIcon(this, true); 416 StreetsidePlugin.getStreetsideWalkAction().addListener(this); 417 } 418 419 @Override 420 public void actionPerformed(ActionEvent e) { 421 if (thread != null) { 422 thread.stopWalk(); 423 } 424 } 425 426 @Override 427 public void walkStarted(WalkThread thread) { 428 this.thread = thread; 429 } 430 } 431 432 private static class PlayAction extends AbstractAction implements WalkListener { 433 434 private static final long serialVersionUID = -1572747020946842769L; 435 436 private transient WalkThread thread; 437 438 /** 439 * Constructs a normal PlayAction 440 */ 441 PlayAction() { 442 putValue(Action.NAME, I18n.tr("Play")); 443 putValue(Action.SHORT_DESCRIPTION, I18n.tr("Continues with the paused walk.")); 444 new ImageProvider("dialogs/streetsidePlay.png").getResource().attachImageIcon(this, true); 445 StreetsidePlugin.getStreetsideWalkAction().addListener(this); 446 } 447 448 @Override 449 public void actionPerformed(ActionEvent e) { 450 if (thread != null) { 451 thread.play(); 452 } 453 } 454 455 @Override 456 public void walkStarted(WalkThread thread) { 457 if (thread != null) { 458 this.thread = thread; 459 } 460 } 461 } 462 463 private static class PauseAction extends AbstractAction implements WalkListener { 464 465 /** 466 * 467 */ 468 private static final long serialVersionUID = -8758326399460817222L; 469 private WalkThread thread; 470 471 /** 472 * Constructs a normal PauseAction 473 */ 474 PauseAction() { 475 putValue(Action.NAME, I18n.tr("Pause")); 476 putValue(Action.SHORT_DESCRIPTION, I18n.tr("Pauses the walk.")); 477 new ImageProvider("dialogs/streetsidePause.png").getResource().attachImageIcon(this, true); 478 StreetsidePlugin.getStreetsideWalkAction().addListener(this); 479 } 480 481 @Override 482 public void actionPerformed(ActionEvent e) { 483 thread.pause(); 484 } 485 486 @Override 487 public void walkStarted(WalkThread thread) { 488 this.thread = thread; 489 } 490 } 491 492 /** 493 * When the pictures are returned from the cache, they are set in the 494 * {@link StreetsideImageDisplay} object. 495 */ 496 @Override 497 public void loadingFinished(final CacheEntry data, final CacheEntryAttributes attributes, final LoadResult result) { 498 if (!SwingUtilities.isEventDispatchThread()) { 499 SwingUtilities.invokeLater(() -> loadingFinished(data, attributes, result)); 500 501 } else if (data != null && result == LoadResult.SUCCESS) { 502 try { 503 final BufferedImage img = ImageIO.read(new ByteArrayInputStream(data.getContent())); 504 if (img == null) { 505 return; 506 } 507 if ( 508 streetsideImageDisplay.getImage() == null 509 || img.getHeight() > streetsideImageDisplay.getImage().getHeight() 510 ) { 511 streetsideImageDisplay.setImage( 512 img, 513 null); 514 } 515 } catch (final IOException e) { 516 logger.error(e); 517 } 518 } 519 } 520 521 522 /** 523 * Creates the layout of the dialog. 524 * 525 * @param data The content of the dialog 526 * @param buttons The buttons where you can click 527 */ 528 public void createLayout(Component data, List<SideButton> buttons) { 529 removeAll(); 530 createLayout(data, true, buttons); 531 add(titleBar, BorderLayout.NORTH); 532 } 533 534 @Override 535 public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) { 536 setImage(newImage); 537 if (newImage != null && newImage.getSequence() != null) { 538 if (newImage.next() != null) { 539 nextButton.setEnabled(true); 540 } 541 if (newImage.previous() != null) { 542 previousButton.setEnabled(true); 543 } 544 } 545 updateImage(); 546 } 547 548 @Override 549 public void imagesAdded() { 550 // This method is enforced by StreetsideDataListener, but only selectedImageChanged() is needed 551 } 552 553 /** 554 * @return the streetsideImageDisplay 555 */ 556 public StreetsideImageDisplay getStreetsideImageDisplay() { 557 return streetsideImageDisplay; 558 } 559 560 /** 561 * @param streetsideImageDisplay the streetsideImageDisplay to set 562 */ 563 public void setStreetsideImageDisplay(StreetsideImageDisplay streetsideImageDisplay) { 564 this.streetsideImageDisplay = streetsideImageDisplay; 565 } 566}