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