source: josm/trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java

Last change on this file was 18613, checked in by taylor.smock, 2 months ago

Fix #21605: Add tabs to ImageViewerDialog for use with different image layers

This allows users to have multiple geotagged image layers, and
quickly switch between them.

This is a complete rework of the functionality introduced in r18591 after user feedback.

Changes:

  • Tabs are now scrollable
  • Tabs now have a close button
  • Removes the functions where plugins could send a layer in (plugins should use IGeoImageLayer instead)
  • Tabs are sorted (by layer order)
  • GeoImageLayers will use a darker red for selected but not viewed images
  • Tabs are only opened when the user clicks on a geoimage from a different layer
  • GpxMarkers now implement IQuadBucketType. This was to speed up the containsImage method in MarkerLayer.
  • Property svn:eol-style set to native
File size: 29.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.BasicStroke;
10import java.awt.Color;
11import java.awt.Component;
12import java.awt.Graphics2D;
13import java.awt.Point;
14import java.awt.event.ActionEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.io.File;
18import java.net.URI;
19import java.net.URISyntaxException;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.Comparator;
24import java.util.HashMap;
25import java.util.List;
26import java.util.ListIterator;
27import java.util.Map;
28import java.util.Objects;
29import java.util.Optional;
30
31import javax.swing.AbstractAction;
32import javax.swing.Action;
33import javax.swing.Icon;
34import javax.swing.JCheckBoxMenuItem;
35import javax.swing.JOptionPane;
36
37import org.openstreetmap.josm.actions.AutoScaleAction;
38import org.openstreetmap.josm.actions.RenameLayerAction;
39import org.openstreetmap.josm.data.Bounds;
40import org.openstreetmap.josm.data.coor.LatLon;
41import org.openstreetmap.josm.data.gpx.GpxConstants;
42import org.openstreetmap.josm.data.gpx.GpxData;
43import org.openstreetmap.josm.data.gpx.GpxExtension;
44import org.openstreetmap.josm.data.gpx.GpxLink;
45import org.openstreetmap.josm.data.gpx.IGpxLayerPrefs;
46import org.openstreetmap.josm.data.gpx.WayPoint;
47import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
48import org.openstreetmap.josm.data.osm.BBox;
49import org.openstreetmap.josm.data.osm.QuadBuckets;
50import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
51import org.openstreetmap.josm.data.preferences.IntegerProperty;
52import org.openstreetmap.josm.data.preferences.NamedColorProperty;
53import org.openstreetmap.josm.data.preferences.StrokeProperty;
54import org.openstreetmap.josm.gui.MainApplication;
55import org.openstreetmap.josm.gui.MapView;
56import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
57import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
58import org.openstreetmap.josm.gui.layer.CustomizeColor;
59import org.openstreetmap.josm.gui.layer.GpxLayer;
60import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
61import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
62import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
63import org.openstreetmap.josm.gui.layer.Layer;
64import org.openstreetmap.josm.gui.layer.geoimage.IGeoImageLayer;
65import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
66import org.openstreetmap.josm.gui.layer.gpx.ConvertFromMarkerLayerAction;
67import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
68import org.openstreetmap.josm.io.audio.AudioPlayer;
69import org.openstreetmap.josm.spi.preferences.Config;
70import org.openstreetmap.josm.tools.ColorHelper;
71import org.openstreetmap.josm.tools.ImageProvider;
72import org.openstreetmap.josm.tools.ListenerList;
73import org.openstreetmap.josm.tools.Logging;
74import org.openstreetmap.josm.tools.Utils;
75
76/**
77 * A layer holding markers.
78 *
79 * Markers are GPS points with a name and, optionally, a symbol code attached;
80 * marker layers can be created from waypoints when importing raw GPS data,
81 * but they may also come from other sources.
82 *
83 * The symbol code is for future use.
84 *
85 * The data is read only.
86 */
87public class MarkerLayer extends Layer implements JumpToMarkerLayer, IGeoImageLayer {
88
89    /**
90     * A list of markers.
91     */
92    public final MarkerData data;
93    private boolean mousePressed;
94    public GpxLayer fromLayer;
95    private Marker currentMarker;
96    public AudioMarker syncAudioMarker;
97    private Color color, realcolor;
98    final int markerSize = new IntegerProperty("draw.rawgps.markers.size", 4).get();
99    final BasicStroke markerStroke = new StrokeProperty("draw.rawgps.markers.stroke", "1").get();
100
101    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListenerListenerList = ListenerList.create();
102
103    /**
104     * The default color that is used for drawing markers.
105     */
106    public static final NamedColorProperty DEFAULT_COLOR_PROPERTY = new NamedColorProperty(marktr("gps marker"), Color.magenta);
107
108    /**
109     * Constructs a new {@code MarkerLayer}.
110     * @param indata The GPX data for this layer
111     * @param name The marker layer name
112     * @param associatedFile The associated GPX file
113     * @param fromLayer The associated GPX layer
114     */
115    public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
116        super(name);
117        this.setAssociatedFile(associatedFile);
118        this.data = new MarkerData();
119        this.fromLayer = fromLayer;
120        double firstTime = -1.0;
121        String lastLinkedFile = "";
122
123        if (fromLayer == null || fromLayer.data == null) {
124            data.ownLayerPrefs = indata.getLayerPrefs();
125        }
126
127        String cs = GPXSettingsPanel.tryGetDataPrefLocal(data, "markers.color");
128        Color c = null;
129        if (cs != null) {
130            c = ColorHelper.html2color(cs);
131            if (c == null) {
132                Logging.warn("Could not read marker color: " + cs);
133            }
134        }
135        setPrivateColors(c);
136
137        for (WayPoint wpt : indata.waypoints) {
138            /* calculate time differences in waypoints */
139            double time = wpt.getTime();
140            boolean wptHasLink = wpt.attr.containsKey(GpxConstants.META_LINKS);
141            if (firstTime < 0 && wptHasLink) {
142                firstTime = time;
143                for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
144                    lastLinkedFile = oneLink.uri;
145                    break;
146                }
147            }
148            if (wptHasLink) {
149                for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
150                    String uri = oneLink.uri;
151                    if (uri != null) {
152                        if (!uri.equals(lastLinkedFile)) {
153                            firstTime = time;
154                        }
155                        lastLinkedFile = uri;
156                        break;
157                    }
158                }
159            }
160            Double offset = null;
161            // If we have an explicit offset, take it.
162            // Otherwise, for a group of markers with the same Link-URI (e.g. an
163            // audio file) calculate the offset relative to the first marker of
164            // that group. This way the user can jump to the corresponding
165            // playback positions in a long audio track.
166            GpxExtension offsetExt = wpt.getExtensions().get("josm", "offset");
167            if (offsetExt != null && offsetExt.getValue() != null) {
168                try {
169                    offset = Double.valueOf(offsetExt.getValue());
170                } catch (NumberFormatException nfe) {
171                    Logging.warn(nfe);
172                }
173            }
174            if (offset == null) {
175                offset = time - firstTime;
176            }
177            final Collection<Marker> markers = Marker.createMarkers(wpt, indata.storageFile, this, time, offset);
178            if (markers != null) {
179                data.addAll(markers);
180            }
181        }
182    }
183
184    @Override
185    public synchronized void destroy() {
186        if (data.contains(AudioMarker.recentlyPlayedMarker())) {
187            AudioMarker.resetRecentlyPlayedMarker();
188        }
189        syncAudioMarker = null;
190        currentMarker = null;
191        fromLayer = null;
192        data.forEach(Marker::destroy);
193        data.clear();
194        super.destroy();
195    }
196
197    @Override
198    public LayerPainter attachToMapView(MapViewEvent event) {
199        event.getMapView().addMouseListener(new MarkerMouseAdapter());
200
201        if (event.getMapView().playHeadMarker == null) {
202            event.getMapView().playHeadMarker = PlayHeadMarker.create();
203        }
204
205        return super.attachToMapView(event);
206    }
207
208    /**
209     * Return a static icon.
210     */
211    @Override
212    public Icon getIcon() {
213        return ImageProvider.get("layer", "marker_small");
214    }
215
216    @Override
217    public void paint(Graphics2D g, MapView mv, Bounds box) {
218        boolean showTextOrIcon = isTextOrIconShown();
219        g.setColor(realcolor);
220        if (mousePressed) {
221            boolean mousePressedTmp = mousePressed;
222            Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
223            for (Marker mkr : data) {
224                if (mousePos != null && mkr.containsPoint(mousePos)) {
225                    mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
226                    mousePressedTmp = false;
227                }
228            }
229        } else {
230            for (Marker mkr : data) {
231                mkr.paint(g, mv, false, showTextOrIcon);
232            }
233        }
234    }
235
236    @Override
237    public String getToolTipText() {
238        return Integer.toString(data.size())+' '+trn("marker", "markers", data.size());
239    }
240
241    @Override
242    public void mergeFrom(Layer from) {
243        if (from instanceof MarkerLayer) {
244            data.addAll(((MarkerLayer) from).data);
245            data.sort(Comparator.comparingDouble(o -> o.time));
246        }
247    }
248
249    @Override public boolean isMergable(Layer other) {
250        return other instanceof MarkerLayer;
251    }
252
253    @Override public void visitBoundingBox(BoundingXYVisitor v) {
254        for (Marker mkr : data) {
255            v.visit(mkr);
256        }
257    }
258
259    @Override public Object getInfoComponent() {
260        return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers",
261                data.size(), Utils.escapeReservedCharactersHTML(getName()), data.size()) + "</html>";
262    }
263
264    @Override public Action[] getMenuEntries() {
265        Collection<Action> components = new ArrayList<>();
266        components.add(LayerListDialog.getInstance().createShowHideLayerAction());
267        components.add(new ShowHideMarkerText(this));
268        components.add(LayerListDialog.getInstance().createDeleteLayerAction());
269        components.add(MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER));
270        components.add(LayerListDialog.getInstance().createMergeLayerAction(this));
271        components.add(SeparatorLayerAction.INSTANCE);
272        components.add(new CustomizeColor(this));
273        components.add(SeparatorLayerAction.INSTANCE);
274        components.add(new SynchronizeAudio());
275        if (Config.getPref().getBoolean("marker.traceaudio", true)) {
276            components.add(new MoveAudio());
277        }
278        components.add(new JumpToNextMarker(this));
279        components.add(new JumpToPreviousMarker(this));
280        components.add(new ConvertFromMarkerLayerAction(this));
281        components.add(new RenameLayerAction(getAssociatedFile(), this));
282        components.add(SeparatorLayerAction.INSTANCE);
283        components.add(new LayerListPopup.InfoAction(this));
284        return components.toArray(new Action[0]);
285    }
286
287    public boolean synchronizeAudioMarkers(final AudioMarker startMarker) {
288        syncAudioMarker = startMarker;
289        if (syncAudioMarker != null && !data.contains(syncAudioMarker)) {
290            syncAudioMarker = null;
291        }
292        if (syncAudioMarker == null) {
293            // find the first audioMarker in this layer
294            syncAudioMarker = Utils.filteredCollection(data, AudioMarker.class).stream()
295                    .findFirst().orElse(syncAudioMarker);
296        }
297        if (syncAudioMarker == null)
298            return false;
299
300        // apply adjustment to all subsequent audio markers in the layer
301        double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds
302        boolean seenStart = false;
303        try {
304            URI uri = syncAudioMarker.url().toURI();
305            for (Marker m : data) {
306                if (m == syncAudioMarker) {
307                    seenStart = true;
308                }
309                if (seenStart && m instanceof AudioMarker) {
310                    AudioMarker ma = (AudioMarker) m;
311                    // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection
312                    // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details
313                    if (ma.url().toURI().equals(uri)) {
314                        ma.adjustOffset(adjustment);
315                    }
316                }
317            }
318        } catch (URISyntaxException e) {
319            Logging.warn(e);
320        }
321        return true;
322    }
323
324    public AudioMarker addAudioMarker(double time, LatLon coor) {
325        // find first audio marker to get absolute start time
326        double offset = 0.0;
327        AudioMarker am = null;
328        for (Marker m : data) {
329            if (m.getClass() == AudioMarker.class) {
330                am = (AudioMarker) m;
331                offset = time - am.time;
332                break;
333            }
334        }
335        if (am == null) {
336            JOptionPane.showMessageDialog(
337                    MainApplication.getMainFrame(),
338                    tr("No existing audio markers in this layer to offset from."),
339                    tr("Error"),
340                    JOptionPane.ERROR_MESSAGE
341                    );
342            return null;
343        }
344
345        // make our new marker
346        AudioMarker newAudioMarker = new AudioMarker(coor,
347                null, AudioPlayer.url(), this, time, offset);
348
349        // insert it at the right place in a copy the collection
350        Collection<Marker> newData = new ArrayList<>();
351        am = null;
352        AudioMarker ret = newAudioMarker; // save to have return value
353        for (Marker m : data) {
354            if (m.getClass() == AudioMarker.class) {
355                am = (AudioMarker) m;
356                if (newAudioMarker != null && offset < am.offset) {
357                    newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
358                    newData.add(newAudioMarker);
359                    newAudioMarker = null;
360                }
361            }
362            newData.add(m);
363        }
364
365        if (newAudioMarker != null) {
366            if (am != null) {
367                newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
368            }
369            newData.add(newAudioMarker); // insert at end
370        }
371
372        // replace the collection
373        data.clear();
374        data.addAll(newData);
375        return ret;
376    }
377
378    @Override
379    public void jumpToNextMarker() {
380        if (currentMarker == null) {
381            currentMarker = data.get(0);
382        } else {
383            boolean foundCurrent = false;
384            for (Marker m: data) {
385                if (foundCurrent) {
386                    currentMarker = m;
387                    break;
388                } else if (currentMarker == m) {
389                    foundCurrent = true;
390                }
391            }
392        }
393        MainApplication.getMap().mapView.zoomTo(currentMarker);
394    }
395
396    @Override
397    public void jumpToPreviousMarker() {
398        if (currentMarker == null) {
399            currentMarker = data.get(data.size() - 1);
400        } else {
401            boolean foundCurrent = false;
402            for (int i = data.size() - 1; i >= 0; i--) {
403                Marker m = data.get(i);
404                if (foundCurrent) {
405                    currentMarker = m;
406                    break;
407                } else if (currentMarker == m) {
408                    foundCurrent = true;
409                }
410            }
411        }
412        MainApplication.getMap().mapView.zoomTo(currentMarker);
413    }
414
415    /**
416     * Set the current marker
417     * @param newMarker The marker to set
418     */
419    void setCurrentMarker(Marker newMarker) {
420        this.currentMarker = newMarker;
421    }
422
423    public static void playAudio() {
424        playAdjacentMarker(null, true);
425    }
426
427    public static void playNextMarker() {
428        playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
429    }
430
431    public static void playPreviousMarker() {
432        playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
433    }
434
435    private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
436        Marker previousMarker = null;
437        boolean nextTime = false;
438        if (layer.getClass() == MarkerLayer.class) {
439            MarkerLayer markerLayer = (MarkerLayer) layer;
440            for (Marker marker : markerLayer.data) {
441                if (marker == startMarker) {
442                    if (next) {
443                        nextTime = true;
444                    } else {
445                        if (previousMarker == null) {
446                            previousMarker = startMarker; // if no previous one, play the first one again
447                        }
448                        return previousMarker;
449                    }
450                } else if (marker.getClass() == AudioMarker.class) {
451                    if (nextTime || startMarker == null)
452                        return marker;
453                    previousMarker = marker;
454                }
455            }
456            if (nextTime) // there was no next marker in that layer, so play the last one again
457                return startMarker;
458        }
459        return null;
460    }
461
462    private static void playAdjacentMarker(Marker startMarker, boolean next) {
463        if (!MainApplication.isDisplayingMapView())
464            return;
465        Marker m = null;
466        Layer l = MainApplication.getLayerManager().getActiveLayer();
467        if (l != null) {
468            m = getAdjacentMarker(startMarker, next, l);
469        }
470        if (m == null) {
471            for (Layer layer : MainApplication.getLayerManager().getLayers()) {
472                m = getAdjacentMarker(startMarker, next, layer);
473                if (m != null) {
474                    break;
475                }
476            }
477        }
478        if (m != null) {
479            ((AudioMarker) m).play();
480        }
481    }
482
483    /**
484     * Get state of text display.
485     * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
486     */
487    private boolean isTextOrIconShown() {
488        return Boolean.parseBoolean(GPXSettingsPanel.getDataPref(data, "markers.show-text"));
489    }
490
491    @Override
492    public boolean hasColor() {
493        return true;
494    }
495
496    @Override
497    public Color getColor() {
498        return color;
499    }
500
501    @Override
502    public void setColor(Color color) {
503        setPrivateColors(color);
504        String cs = null;
505        if (color != null) {
506            cs = ColorHelper.color2html(color);
507        }
508        GPXSettingsPanel.putDataPrefLocal(data, "markers.color", cs);
509        invalidate();
510    }
511
512    private void setPrivateColors(Color color) {
513        this.color = color;
514        this.realcolor = Optional.ofNullable(color).orElse(DEFAULT_COLOR_PROPERTY.get());
515    }
516
517    @Override
518    public void clearSelection() {
519        this.currentMarker = null;
520    }
521
522    @Override
523    public List<? extends IImageEntry<?>> getSelection() {
524        if (this.currentMarker instanceof ImageMarker) {
525            return Collections.singletonList(((ImageMarker) this.currentMarker).getRemoteEntry());
526        }
527        return Collections.emptyList();
528    }
529
530    @Override
531    public boolean containsImage(IImageEntry<?> imageEntry) {
532        if (imageEntry instanceof RemoteEntry) {
533            RemoteEntry entry = (RemoteEntry) imageEntry;
534            if (entry.getPos() != null && entry.getPos().isLatLonKnown()) {
535                List<Marker> markers = this.data.search(new BBox(entry.getPos()));
536                return checkIfListContainsEntry(markers, entry);
537            } else if (entry.getExifCoor() != null && entry.getExifCoor().isLatLonKnown()) {
538                List<Marker> markers = this.data.search(new BBox(entry.getExifCoor()));
539                return checkIfListContainsEntry(markers, entry);
540            } else {
541                return checkIfListContainsEntry(this.data, entry);
542            }
543        }
544        return false;
545    }
546
547    /**
548     * Check if a list contains an entry
549     * @param markerList The list to look through
550     * @param imageEntry The image entry to check
551     * @return {@code true} if the entry is in the list
552     */
553    private static boolean checkIfListContainsEntry(List<Marker> markerList, RemoteEntry imageEntry) {
554        for (Marker marker : markerList) {
555            if (marker instanceof ImageMarker) {
556                ImageMarker imageMarker = (ImageMarker) marker;
557                try {
558                    if (Objects.equals(imageMarker.imageUrl.toURI(), imageEntry.getImageURI())) {
559                        return true;
560                    }
561                } catch (URISyntaxException e) {
562                    Logging.trace(e);
563                }
564            }
565        }
566        return false;
567    }
568
569    @Override
570    public void addImageChangeListener(ImageChangeListener listener) {
571        this.imageChangeListenerListenerList.addListener(listener);
572    }
573
574    @Override
575    public void removeImageChangeListener(ImageChangeListener listener) {
576        this.imageChangeListenerListenerList.removeListener(listener);
577    }
578
579    private final class MarkerMouseAdapter extends MouseAdapter {
580        @Override
581        public void mousePressed(MouseEvent e) {
582            if (e.getButton() != MouseEvent.BUTTON1)
583                return;
584            boolean mousePressedInButton = data.stream().anyMatch(mkr -> mkr.containsPoint(e.getPoint()));
585            if (!mousePressedInButton)
586                return;
587            mousePressed = true;
588            if (isVisible()) {
589                invalidate();
590            }
591        }
592
593        @Override
594        public void mouseReleased(MouseEvent ev) {
595            if (ev.getButton() != MouseEvent.BUTTON1 || !mousePressed)
596                return;
597            mousePressed = false;
598            if (!isVisible())
599                return;
600            for (Marker mkr : data) {
601                if (mkr.containsPoint(ev.getPoint())) {
602                    mkr.actionPerformed(new ActionEvent(this, 0, null));
603                }
604            }
605            invalidate();
606        }
607    }
608
609    public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
610        private final transient MarkerLayer layer;
611
612        public ShowHideMarkerText(MarkerLayer layer) {
613            super(tr("Show Text/Icons"));
614            new ImageProvider("dialogs", "showhide").getResource().attachImageIcon(this, true);
615            putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
616            putValue("help", ht("/Action/ShowHideTextIcons"));
617            this.layer = layer;
618        }
619
620        @Override
621        public void actionPerformed(ActionEvent e) {
622            GPXSettingsPanel.putDataPrefLocal(layer.data, "markers.show-text", Boolean.toString(!layer.isTextOrIconShown()));
623            layer.invalidate();
624        }
625
626        @Override
627        public Component createMenuComponent() {
628            JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
629            showMarkerTextItem.setState(layer.isTextOrIconShown());
630            return showMarkerTextItem;
631        }
632
633        @Override
634        public boolean supportLayers(List<Layer> layers) {
635            return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
636        }
637    }
638
639    private class SynchronizeAudio extends AbstractAction {
640
641        /**
642         * Constructs a new {@code SynchronizeAudio} action.
643         */
644        SynchronizeAudio() {
645            super(tr("Synchronize Audio"));
646            new ImageProvider("audio-sync").getResource().attachImageIcon(this, true);
647            putValue("help", ht("/Action/SynchronizeAudio"));
648        }
649
650        @Override
651        public void actionPerformed(ActionEvent e) {
652            if (!AudioPlayer.paused()) {
653                JOptionPane.showMessageDialog(
654                        MainApplication.getMainFrame(),
655                        tr("You need to pause audio at the moment when you hear your synchronization cue."),
656                        tr("Warning"),
657                        JOptionPane.WARNING_MESSAGE
658                        );
659                return;
660            }
661            AudioMarker recent = AudioMarker.recentlyPlayedMarker();
662            if (synchronizeAudioMarkers(recent)) {
663                JOptionPane.showMessageDialog(
664                        MainApplication.getMainFrame(),
665                        tr("Audio synchronized at point {0}.", syncAudioMarker.getText()),
666                        tr("Information"),
667                        JOptionPane.INFORMATION_MESSAGE
668                        );
669            } else {
670                JOptionPane.showMessageDialog(
671                        MainApplication.getMainFrame(),
672                        tr("Unable to synchronize in layer being played."),
673                        tr("Error"),
674                        JOptionPane.ERROR_MESSAGE
675                        );
676            }
677        }
678    }
679
680    private class MoveAudio extends AbstractAction {
681
682        MoveAudio() {
683            super(tr("Make Audio Marker at Play Head"));
684            new ImageProvider("addmarkers").getResource().attachImageIcon(this, true);
685            putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
686        }
687
688        @Override
689        public void actionPerformed(ActionEvent e) {
690            if (!AudioPlayer.paused()) {
691                JOptionPane.showMessageDialog(
692                        MainApplication.getMainFrame(),
693                        tr("You need to have paused audio at the point on the track where you want the marker."),
694                        tr("Warning"),
695                        JOptionPane.WARNING_MESSAGE
696                        );
697                return;
698            }
699            PlayHeadMarker playHeadMarker = MainApplication.getMap().mapView.playHeadMarker;
700            if (playHeadMarker == null)
701                return;
702            addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
703            invalidate();
704        }
705    }
706
707    /**
708     * the data of a MarkerLayer
709     * @since 18287
710     */
711    public class MarkerData extends QuadBuckets<Marker> implements List<Marker>, IGpxLayerPrefs {
712
713        private Map<String, String> ownLayerPrefs;
714        private final List<Marker> markerList = new ArrayList<>();
715
716        @Override
717        public Map<String, String> getLayerPrefs() {
718            if (ownLayerPrefs == null && fromLayer != null && fromLayer.data != null) {
719                return fromLayer.data.getLayerPrefs();
720            }
721            // fallback to own layerPrefs if the corresponding gpxLayer has already been deleted
722            // by the user or never existed when loaded from a session file
723            if (ownLayerPrefs == null) {
724                ownLayerPrefs = new HashMap<>();
725            }
726            return ownLayerPrefs;
727        }
728
729        /**
730         * Transfers the layerPrefs from the GpxData to MarkerData (when GpxData is deleted)
731         * @param gpxLayerPrefs the layerPrefs from the GpxData object
732         */
733        public void transferLayerPrefs(Map<String, String> gpxLayerPrefs) {
734            ownLayerPrefs = new HashMap<>(gpxLayerPrefs);
735        }
736
737        @Override
738        public void setModified(boolean value) {
739            if (fromLayer != null && fromLayer.data != null) {
740                fromLayer.data.setModified(value);
741            }
742        }
743
744        @Override
745        public boolean addAll(int index, Collection<? extends Marker> c) {
746            c.forEach(this::add);
747            return this.markerList.addAll(index, c);
748        }
749
750        @Override
751        public boolean addAll(Collection<? extends Marker> objects) {
752            return this.markerList.addAll(objects) && super.addAll(objects);
753        }
754
755        @Override
756        public Marker get(int index) {
757            return this.markerList.get(index);
758        }
759
760        @Override
761        public Marker set(int index, Marker element) {
762            Marker original = this.markerList.set(index, element);
763            this.remove(original);
764            return original;
765        }
766
767        @Override
768        public void add(int index, Marker element) {
769            this.add(element);
770            this.markerList.add(index, element);
771        }
772
773        @Override
774        public Marker remove(int index) {
775            Marker toRemove = this.markerList.remove(index);
776            this.remove(toRemove);
777            return toRemove;
778        }
779
780        @Override
781        public int indexOf(Object o) {
782            return this.markerList.indexOf(o);
783        }
784
785        @Override
786        public int lastIndexOf(Object o) {
787            return this.markerList.lastIndexOf(o);
788        }
789
790        @Override
791        public ListIterator<Marker> listIterator() {
792            return this.markerList.listIterator();
793        }
794
795        @Override
796        public ListIterator<Marker> listIterator(int index) {
797            return this.markerList.listIterator(index);
798        }
799
800        @Override
801        public List<Marker> subList(int fromIndex, int toIndex) {
802            return this.markerList.subList(fromIndex, toIndex);
803        }
804
805        @Override
806        public boolean retainAll(Collection<?> objects) {
807            return this.markerList.retainAll(objects) && super.retainAll(objects);
808        }
809
810        @Override
811        public boolean contains(Object o) {
812            return this.markerList.contains(o) && super.contains(o);
813        }
814    }
815}
Note: See TracBrowser for help on using the repository browser.