001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins.streetside;
003
004import java.awt.AlphaComposite;
005import java.awt.BasicStroke;
006import java.awt.Color;
007import java.awt.Composite;
008import java.awt.Graphics2D;
009import java.awt.GraphicsEnvironment;
010import java.awt.Point;
011import java.awt.Rectangle;
012import java.awt.RenderingHints;
013import java.awt.TexturePaint;
014import java.awt.geom.Line2D;
015import java.awt.image.BufferedImage;
016import java.util.Comparator;
017import java.util.IntSummaryStatistics;
018import java.util.Optional;
019
020import javax.swing.Action;
021import javax.swing.Icon;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.data.Bounds;
025import org.openstreetmap.josm.data.osm.DataSet;
026import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
027import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
028import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
029import org.openstreetmap.josm.gui.MainApplication;
030import org.openstreetmap.josm.gui.MapView;
031import org.openstreetmap.josm.gui.NavigatableComponent;
032import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
033import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
034import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
035import org.openstreetmap.josm.gui.layer.Layer;
036import org.openstreetmap.josm.gui.layer.LayerManager;
037import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
038import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
039import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
040import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
041import org.openstreetmap.josm.plugins.streetside.history.StreetsideRecord;
042import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader;
043import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader.DOWNLOAD_MODE;
044import org.openstreetmap.josm.plugins.streetside.mode.AbstractMode;
045import org.openstreetmap.josm.plugins.streetside.mode.JoinMode;
046import org.openstreetmap.josm.plugins.streetside.mode.SelectMode;
047import org.openstreetmap.josm.plugins.streetside.utils.MapViewGeometryUtil;
048import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
049import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
050import org.openstreetmap.josm.plugins.streetside.utils.StreetsideUtils;
051import org.openstreetmap.josm.tools.I18n;
052import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
053
054/**
055 * This class represents the layer shown in JOSM. There can only exist one
056 * instance of this object.
057 *
058 * @author nokutu
059 */
060public final class StreetsideLayer extends AbstractModifiableLayer implements
061ActiveLayerChangeListener, StreetsideDataListener {
062
063        /** The radius of the image marker */
064        private static final int IMG_MARKER_RADIUS = 7;
065        /** The radius of the circular sector that indicates the camera angle */
066        private static final int CA_INDICATOR_RADIUS = 15;
067        /** The angle of the circular sector that indicates the camera angle */
068        private static final int CA_INDICATOR_ANGLE = 40;
069        /** Length of the edge of the small sign, which indicates that traffic signs have been found in an image. */
070        private static final int TRAFFIC_SIGN_SIZE = 6;
071        /** A third of the height of the sign, for easier calculations */
072        private static final double TRAFFIC_SIGN_HEIGHT_3RD = Math.sqrt(
073                        Math.pow(StreetsideLayer.TRAFFIC_SIGN_SIZE, 2) - Math.pow(StreetsideLayer.TRAFFIC_SIGN_SIZE / 2d, 2)
074                        ) / 3;
075
076        private static final DataSetListenerAdapter DATASET_LISTENER =
077                        new DataSetListenerAdapter(e -> {
078                                if (e instanceof DataChangedEvent && StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
079                                        // When more data is downloaded, a delayed update is thrown, in order to
080                                        // wait for the data bounds to be set.
081                                        MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
082                                }
083                        });
084
085        /** Unique instance of the class. */
086        private static StreetsideLayer instance;
087        /** The nearest images to the selected image from different sequences sorted by distance from selection. */
088        private StreetsideImage[] nearestImages = {};
089        /** {@link StreetsideData} object that stores the database. */
090        private final StreetsideData data;
091
092        /** Mode of the layer. */
093        public AbstractMode mode;
094
095        private volatile TexturePaint hatched;
096        private final StreetsideLocationChangeset locationChangeset = new StreetsideLocationChangeset();
097
098        private StreetsideLayer() {
099                super(I18n.tr("Microsoft Streetside Images"));
100                data = new StreetsideData();
101                data.addListener(this);
102        }
103
104        /**
105         * Initializes the Layer.
106         */
107        private void init() {
108                final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
109                if (ds != null) {
110                        ds.addDataSetListener(StreetsideLayer.DATASET_LISTENER);
111                }
112                MainApplication.getLayerManager().addLayer(this);
113                MainApplication.getLayerManager().addActiveLayerChangeListener(this);
114                if (!GraphicsEnvironment.isHeadless()) {
115                        setMode(new SelectMode());
116                        if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
117                                StreetsideDownloader.downloadOSMArea();
118                        }
119                        if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.VISIBLE_AREA) {
120                                mode.zoomChanged();
121                        }
122                }
123                // Does not execute when in headless mode
124                if (Main.main != null && !StreetsideMainDialog.getInstance().isShowing()) {
125                        StreetsideMainDialog.getInstance().showDialog();
126                }
127                if (StreetsidePlugin.getMapView() != null) {
128                        StreetsideMainDialog.getInstance().getStreetsideImageDisplay().repaint();
129
130                        // There is no delete image action for Streetside (Mapillary functionality here removed).
131
132                        //getLocationChangeset().addChangesetListener(StreetsideChangesetDialog.getInstance());
133                }
134                createHatchTexture();
135                invalidate();
136        }
137
138        public static void invalidateInstance() {
139                if (StreetsideLayer.hasInstance()) {
140                        StreetsideLayer.getInstance().invalidate();
141                }
142        }
143
144        /**
145         * Changes the mode the the given one.
146         *
147         * @param mode The mode that is going to be activated.
148         */
149        public void setMode(AbstractMode mode) {
150                final MapView mv = StreetsidePlugin.getMapView();
151                if (this.mode != null && mv != null) {
152                        mv.removeMouseListener(this.mode);
153                        mv.removeMouseMotionListener(this.mode);
154                        NavigatableComponent.removeZoomChangeListener(this.mode);
155                }
156                this.mode = mode;
157                if (mode != null && mv != null) {
158                        mv.setNewCursor(mode.cursor, this);
159                        mv.addMouseListener(mode);
160                        mv.addMouseMotionListener(mode);
161                        NavigatableComponent.addZoomChangeListener(mode);
162                        StreetsideUtils.updateHelpText();
163                }
164        }
165
166        private static synchronized void clearInstance() {
167                StreetsideLayer.instance = null;
168        }
169
170        /**
171         * Returns the unique instance of this class.
172         *
173         * @return The unique instance of this class.
174         */
175        public static synchronized StreetsideLayer getInstance() {
176                if (StreetsideLayer.instance != null) {
177                        if (!MainApplication.getLayerManager().containsLayer(StreetsideLayer.instance)) {
178                                MainApplication.getLayerManager().addLayer(StreetsideLayer.instance);
179                        }
180                        return StreetsideLayer.instance;
181                }
182                final StreetsideLayer layer = new StreetsideLayer();
183                StreetsideLayer.instance = layer;
184                layer.init();
185                return layer;
186        }
187
188        /**
189         * @return if the unique instance of this layer is currently instantiated and added to the {@link LayerManager}
190         */
191        public static boolean hasInstance() {
192                return StreetsideLayer.instance != null && MainApplication.getLayerManager().containsLayer(StreetsideLayer.instance);
193        }
194
195        /**
196         * Returns the {@link StreetsideData} object, which acts as the database of the
197         * Layer.
198         *
199         * @return The {@link StreetsideData} object that stores the database.
200         */
201        public StreetsideData getData() {
202                return data;
203        }
204
205        /**
206         * Returns the n-nearest image, for n=1 the nearest one is returned, for n=2 the second nearest one and so on.
207         * The "n-nearest image" is picked from the list of one image from every sequence that is nearest to the currently
208         * selected image, excluding the sequence to which the selected image belongs.
209         * @param n the index for picking from the list of "nearest images", beginning from 1
210         * @return the n-nearest image to the currently selected image
211         */
212        public synchronized StreetsideImage getNNearestImage(final int n) {
213                return n >= 1 && n <= nearestImages.length ? nearestImages[n - 1] : null;
214        }
215
216        /**
217           * Returns the {@link StreetsideLocationChangeset} object, which acts as the database of the
218           * Layer.
219           *
220           * @return The {@link MapillaryData} object that stores the database.
221           */
222          public StreetsideLocationChangeset getLocationChangeset() {
223            return locationChangeset;
224          }
225
226
227        @Override
228        public synchronized void destroy() {
229                // TODO: Add destroy code for CubemapBuilder, et al.? @rrh
230                StreetsideLayer.clearInstance();
231                setMode(null);
232                StreetsideRecord.getInstance().reset();
233                AbstractMode.resetThread();
234                StreetsideDownloader.stopAll();
235                if (StreetsideMainDialog.hasInstance()) {
236                        StreetsideMainDialog.getInstance().setImage(null);
237                        StreetsideMainDialog.getInstance().updateImage();
238                }
239                final MapView mv = StreetsidePlugin.getMapView();
240                if (mv != null) {
241                        mv.removeMouseListener(mode);
242                        mv.removeMouseMotionListener(mode);
243                }
244                try {
245                        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
246                        if (MainApplication.getLayerManager().getEditDataSet() != null) {
247                                MainApplication.getLayerManager().getEditDataSet().removeDataSetListener(StreetsideLayer.DATASET_LISTENER);
248                        }
249                } catch (final IllegalArgumentException e) {
250                        // TODO: It would be ideal, to fix this properly. But for the moment let's catch this, for when a listener has already been removed.
251                }
252                super.destroy();
253        }
254
255        @Override
256        public boolean isModified() {
257                // TODO: Add cubemap modification here? @rrh
258                return data.getImages().parallelStream().anyMatch(StreetsideAbstractImage::isModified);
259        }
260
261        @Override
262        public void setVisible(boolean visible) {
263                super.setVisible(visible);
264                getData().getImages().parallelStream().forEach(img -> img.setVisible(visible));
265                if (MainApplication.getMap() != null) {
266                        //StreetsideFilterDialog.getInstance().refresh();
267                }
268        }
269
270        /**
271         * Initialize the hatch pattern used to paint the non-downloaded area.
272         */
273        private void createHatchTexture() {
274                final BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
275                final Graphics2D big = bi.createGraphics();
276                big.setColor(StreetsideProperties.BACKGROUND.get());
277                final Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
278                big.setComposite(comp);
279                big.fillRect(0, 0, 15, 15);
280                big.setColor(StreetsideProperties.OUTSIDE_DOWNLOADED_AREA.get());
281                big.drawLine(0, 15, 15, 0);
282                final Rectangle r = new Rectangle(0, 0, 15, 15);
283                hatched = new TexturePaint(bi, r);
284        }
285
286        @Override
287        public synchronized void paint(final Graphics2D g, final MapView mv, final Bounds box) {
288                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
289                if (MainApplication.getLayerManager().getActiveLayer() == this) {
290                        // paint remainder
291                        g.setPaint(hatched);
292                        g.fill(MapViewGeometryUtil.getNonDownloadedArea(mv, data.getBounds()));
293                }
294
295                // Draw the blue and red line
296                synchronized (StreetsideLayer.class) {
297                        final StreetsideAbstractImage selectedImg = data.getSelectedImage();
298                        for (int i = 0; i < nearestImages.length && selectedImg != null; i++) {
299                                if (i == 0) {
300                                        g.setColor(Color.RED);
301                                } else {
302                                        g.setColor(Color.BLUE);
303                                }
304                                final Point selected = mv.getPoint(selectedImg.getMovingLatLon());
305                                final Point p = mv.getPoint(nearestImages[i].getMovingLatLon());
306                                g.draw(new Line2D.Double(p.getX(), p.getY(), selected.getX(), selected.getY()));
307                        }
308                }
309
310                // Draw sequence line
311                g.setStroke(new BasicStroke(2));
312                final StreetsideAbstractImage selectedImage = getData().getSelectedImage();
313                for (final StreetsideSequence seq : getData().getSequences()) {
314                        if (seq.getImages().contains(selectedImage)) {
315                                g.setColor(
316                                                seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED : StreetsideColorScheme.SEQ_SELECTED
317                                                );
318                        } else {
319                                g.setColor(
320                                                seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED : StreetsideColorScheme.SEQ_UNSELECTED
321                                                );
322                        }
323                        g.draw(MapViewGeometryUtil.getSequencePath(mv, seq));
324                }
325                /*for (final StreetsideAbstractImage imageAbs : data.getImages()) {
326                        if (imageAbs.isVisible() && mv != null && mv.contains(mv.getPoint(imageAbs.getMovingLatLon()))) {
327                                drawImageMarker(g, imageAbs);
328                        }
329                }*/
330                if (mode instanceof JoinMode) {
331                        mode.paint(g, mv, box);
332                }
333        }
334
335        /**
336         * Draws an image marker onto the given Graphics context.
337         * @param g the Graphics context
338         * @param img the image to be drawn onto the Graphics context
339         */
340        /*private void drawImageMarker(final Graphics2D g, final StreetsideAbstractImage img) {
341                if (img == null || img.getLatLon() == null) {
342                        Logging.warn("An image is not painted, because it is null or has no LatLon!");
343                        return;
344                }
345                final StreetsideAbstractImage selectedImg = getData().getSelectedImage();
346                final Point p = MainApplication.getMap().mapView.getPoint(img.getMovingLatLon());
347
348                // Determine colors
349                final Color markerC;
350                final Color directionC;
351                if (selectedImg != null && getData().getMultiSelectedImages().contains(img)) {
352                        markerC = img instanceof StreetsideImportedImage
353                                        ? StreetsideColorScheme.SEQ_IMPORTED_HIGHLIGHTED
354                                                        : StreetsideColorScheme.SEQ_HIGHLIGHTED;
355                        directionC = img instanceof StreetsideImportedImage
356                                        ? StreetsideColorScheme.SEQ_IMPORTED_HIGHLIGHTED_CA
357                                                        : StreetsideColorScheme.SEQ_HIGHLIGHTED_CA;
358                } else if (selectedImg != null && selectedImg.getSequence() != null && selectedImg.getSequence().equals(img.getSequence())) {
359                        markerC = img instanceof StreetsideImportedImage
360                                        ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED
361                                                        : StreetsideColorScheme.SEQ_SELECTED;
362                        directionC = img instanceof StreetsideImportedImage
363                                        ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED_CA
364                                                        : StreetsideColorScheme.SEQ_SELECTED_CA;
365                } else {
366                        markerC = img instanceof StreetsideImportedImage
367                                        ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED
368                                                        : StreetsideColorScheme.SEQ_UNSELECTED;
369                        directionC = img instanceof StreetsideImportedImage
370                                        ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED_CA
371                                                        : StreetsideColorScheme.SEQ_UNSELECTED_CA;
372                }
373
374                // Paint direction indicator
375                g.setColor(directionC);
376                g.fillArc(p.x - StreetsideLayer.CA_INDICATOR_RADIUS, p.y - StreetsideLayer.CA_INDICATOR_RADIUS, 2 * StreetsideLayer.CA_INDICATOR_RADIUS, 2 * StreetsideLayer.CA_INDICATOR_RADIUS, (int) (90 - img.getMovingHe() - StreetsideLayer.CA_INDICATOR_ANGLE / 2d), StreetsideLayer.CA_INDICATOR_ANGLE);
377                // Paint image marker
378                g.setColor(markerC);
379                g.fillOval(p.x - StreetsideLayer.IMG_MARKER_RADIUS, p.y - StreetsideLayer.IMG_MARKER_RADIUS, 2 * StreetsideLayer.IMG_MARKER_RADIUS, 2 * StreetsideLayer.IMG_MARKER_RADIUS);
380
381                // Paint highlight for selected or highlighted images
382                if (img.equals(getData().getHighlightedImage()) || getData().getMultiSelectedImages().contains(img)) {
383                        g.setColor(Color.WHITE);
384                        g.setStroke(new BasicStroke(2));
385                        g.drawOval(p.x - StreetsideLayer.IMG_MARKER_RADIUS, p.y - StreetsideLayer.IMG_MARKER_RADIUS, 2 * StreetsideLayer.IMG_MARKER_RADIUS, 2 * StreetsideLayer.IMG_MARKER_RADIUS);
386                }
387
388                // TODO: reimplement detections for Bing Metadata? RRH
389                if (img instanceof StreetsideImage && !((StreetsideImage) img).getDetections().isEmpty()) {
390                        final Path2D trafficSign = new Path2D.Double();
391                        trafficSign.moveTo(p.getX() - StreetsideLayer.TRAFFIC_SIGN_SIZE / 2d, p.getY() - StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
392                        trafficSign.lineTo(p.getX() + StreetsideLayer.TRAFFIC_SIGN_SIZE / 2d, p.getY() - StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
393                        trafficSign.lineTo(p.getX(), p.getY() + 2 * StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
394                        trafficSign.closePath();
395                        g.setColor(Color.WHITE);
396                        g.fill(trafficSign);
397                        g.setStroke(new BasicStroke(1));
398                        g.setColor(Color.RED);
399                        g.draw(trafficSign);
400                }
401        }*/
402
403        @Override
404        public Icon getIcon() {
405                return StreetsidePlugin.LOGO.setSize(ImageSizes.LAYER).get();
406        }
407
408        @Override
409        public boolean isMergable(Layer other) {
410                return false;
411        }
412
413        @Override
414        public void mergeFrom(Layer from) {
415                throw new UnsupportedOperationException(
416                                "This layer does not support merging yet");
417        }
418
419        @Override
420        public Action[] getMenuEntries() {
421                return new Action[]{
422                                LayerListDialog.getInstance().createShowHideLayerAction(),
423                                LayerListDialog.getInstance().createDeleteLayerAction(),
424                                new LayerListPopup.InfoAction(this)
425                };
426        }
427
428        @Override
429        public Object getInfoComponent() {
430                final IntSummaryStatistics seqSizeStats = getData().getSequences().stream().mapToInt(seq -> seq.getImages().size()).summaryStatistics();
431                return new StringBuilder(I18n.tr("Streetside layer"))
432                                .append("\n")
433                                .append(I18n.tr(
434                                                "{0} sequences, each containing between {1} and {2} images (ΓΈ {3})",
435                                                getData().getSequences().size(),
436                                                seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMin(),
437                                                                seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMax(),
438                                                                                seqSizeStats.getAverage()
439                                                ))
440                                .append("\n\n")
441                                .append(I18n.tr(
442                                                "{0} imported images",
443                                                getData().getImages().stream().filter(i -> i instanceof StreetsideImportedImage).count()
444                                                ))
445                                .append("\n+ ")
446                                .append(I18n.tr(
447                                                "{0} downloaded images",
448                                                getData().getImages().stream().filter(i -> i instanceof StreetsideImage).count()
449                                                ))
450                                .append("\n= ")
451                                .append(I18n.tr(
452                                                "{0} images in total",
453                                                getData().getImages().size()
454                                                )).toString();
455        }
456
457        @Override
458        public String getToolTipText() {
459                return I18n.tr("{0} images in {1} sequences", getData().getImages().size(), getData().getSequences().size());
460        }
461
462        @Override
463        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
464                if (MainApplication.getLayerManager().getActiveLayer() == this) {
465                        StreetsideUtils.updateHelpText();
466                }
467
468                if (MainApplication.getLayerManager().getEditLayer() != e.getPreviousDataLayer()) {
469                        if (MainApplication.getLayerManager().getEditLayer() != null) {
470                                MainApplication.getLayerManager().getEditLayer().getDataSet().addDataSetListener(StreetsideLayer.DATASET_LISTENER);
471                        }
472                        if (e.getPreviousDataLayer() != null) {
473                                e.getPreviousDataLayer().getDataSet().removeDataSetListener(StreetsideLayer.DATASET_LISTENER);
474                        }
475                }
476        }
477
478        @Override
479        public void visitBoundingBox(BoundingXYVisitor v) {
480        }
481
482        /* (non-Javadoc)
483         * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded()
484         */
485        @Override
486        public void imagesAdded() {
487                // TODO: Never used - could this be of use? @rrh
488                updateNearestImages();
489        }
490
491        /* (non-Javadoc)
492         * @see org.openstreetmap.josm.plugins.mapillary.StreetsideDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage, org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage)
493         */
494        @Override
495        public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
496                updateNearestImages();
497        }
498
499        /**
500         * Returns the closest images belonging to a different sequence and
501         * different from the specified target image.
502         *
503         * @param target the image for which you want to find the nearest other images
504         * @param limit the maximum length of the returned array
505         * @return An array containing the closest images belonging to different sequences sorted by distance from target.
506         */
507        private StreetsideImage[] getNearestImagesFromDifferentSequences(StreetsideAbstractImage target, int limit) {
508                return data.getSequences().parallelStream()
509                                .filter(seq -> seq.getId() != null && !seq.getId().equals(target.getSequence().getId()))
510                                .map(seq -> { // Maps sequence to image from sequence that is nearest to target
511                                        final Optional<StreetsideAbstractImage> resImg = seq.getImages().parallelStream()
512                                                        .filter(img -> img instanceof StreetsideImage && img.isVisible())
513                                                        .min(new NearestImgToTargetComparator(target));
514                                        return resImg.orElse(null);
515                                })
516                                .filter(img -> // Filters out images too far away from target
517                                img != null &&
518                                img.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
519                                < StreetsideProperties.SEQUENCE_MAX_JUMP_DISTANCE.get()
520                                                )
521                                .sorted(new NearestImgToTargetComparator(target))
522                                .limit(limit)
523                                .toArray(StreetsideImage[]::new);
524        }
525
526        /**
527         * Returns the closest images belonging to a different sequence and
528         * different from the specified target image.
529         *
530         * @param target the image for which you want to find the nearest other images
531         * @param limit the maximum length of the returned array
532         * @return An array containing the closest images belonging to different sequences sorted by distance from target.
533         */
534        /*private StreetsideCubemap[] getNearestCubemapsFromDifferentSequences(StreetsideAbstractImage target, int limit) {
535                return data.getSequences().parallelStream()
536                                .filter(seq -> seq.getId() != null && !seq.getId().equals(target.getSequence().getId()))
537                                .map(seq -> { // Maps sequence to image from sequence that is nearest to target
538                                        final Optional<StreetsideAbstractImage> resCb = seq.getImages().parallelStream()
539                                                        .filter(cb -> cb instanceof StreetsideCubemap && cb.isVisible())
540                                                        .min(new NearestCbToTargetComparator(target));
541                                        return resCb.orElse(null);
542                                })
543                                .filter(cb -> // Filters out images too far away from target
544                                cb != null &&
545                                cb.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
546                                < StreetsideProperties.SEQUENCE_MAX_JUMP_DISTANCE.get()
547                                                )
548                                .sorted(new NearestCbToTargetComparator(target))
549                                .limit(limit)
550                                .toArray(StreetsideCubemap[]::new);
551        }*/
552
553        private synchronized void updateNearestImages() {
554                final StreetsideAbstractImage selected = data.getSelectedImage();
555                if (selected != null) {
556                        // TODO: could this be used to pre-cache cubemaps? @rrh
557                        nearestImages = getNearestImagesFromDifferentSequences(selected, 2);
558                } else {
559                        nearestImages = new StreetsideImage[0];
560                }
561                if (MainApplication.isDisplayingMapView()) {
562                        StreetsideMainDialog.getInstance().redButton.setEnabled(nearestImages.length >= 1);
563                        StreetsideMainDialog.getInstance().blueButton.setEnabled(nearestImages.length >= 2);
564                }
565                if (nearestImages.length >= 1) {
566                        CacheUtils.downloadPicture(nearestImages[0]);
567                        // TODO: download/pre-caches cubemaps here?
568                        //CacheUtils.downloadCubemap(nearestImages[0]);
569                        if (nearestImages.length >= 2) {
570                                CacheUtils.downloadPicture(nearestImages[1]);
571                                // TODO: download/pre-caches cubemaps here?
572                                //CacheUtils.downloadCubemap(nearestImages[1]);
573                        }
574                }
575        }
576
577        private static class NearestImgToTargetComparator implements Comparator<StreetsideAbstractImage> {
578                private final StreetsideAbstractImage target;
579
580                public NearestImgToTargetComparator(StreetsideAbstractImage target) {
581                        this.target = target;
582                }
583                /* (non-Javadoc)
584                 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
585                 */
586                @Override
587                public int compare(StreetsideAbstractImage img1, StreetsideAbstractImage img2) {
588                        return (int) Math.signum(
589                                        img1.getMovingLatLon().greatCircleDistance(target.getMovingLatLon()) -
590                                        img2.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
591                                        );
592                }
593        }
594
595        private static class NearestCbToTargetComparator implements Comparator<StreetsideAbstractImage> {
596                private final StreetsideAbstractImage target;
597
598                public NearestCbToTargetComparator(StreetsideAbstractImage target) {
599                        this.target = target;
600                }
601                /* (non-Javadoc)
602                 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
603                 */
604                @Override
605                public int compare(StreetsideAbstractImage img1, StreetsideAbstractImage img2) {
606                        return (int) Math.signum(
607                                        img1.getMovingLatLon().greatCircleDistance(target.getMovingLatLon()) -
608                                        img2.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
609                                        );
610                }
611        }
612}