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}