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