    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}