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}