001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.layer.markerlayer;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.marktr;
006    import static org.openstreetmap.josm.tools.I18n.tr;
007    import static org.openstreetmap.josm.tools.I18n.trn;
008    
009    import java.awt.Color;
010    import java.awt.Component;
011    import java.awt.Graphics2D;
012    import java.awt.Point;
013    import java.awt.event.ActionEvent;
014    import java.awt.event.MouseAdapter;
015    import java.awt.event.MouseEvent;
016    import java.io.File;
017    import java.net.URL;
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.List;
021    
022    import javax.swing.AbstractAction;
023    import javax.swing.Action;
024    import javax.swing.Icon;
025    import javax.swing.JCheckBoxMenuItem;
026    import javax.swing.JOptionPane;
027    
028    import org.openstreetmap.josm.Main;
029    import org.openstreetmap.josm.actions.RenameLayerAction;
030    import org.openstreetmap.josm.data.Bounds;
031    import org.openstreetmap.josm.data.coor.LatLon;
032    import org.openstreetmap.josm.data.gpx.GpxData;
033    import org.openstreetmap.josm.data.gpx.GpxLink;
034    import org.openstreetmap.josm.data.gpx.WayPoint;
035    import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
036    import org.openstreetmap.josm.gui.MapView;
037    import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
038    import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
039    import org.openstreetmap.josm.gui.layer.CustomizeColor;
040    import org.openstreetmap.josm.gui.layer.GpxLayer;
041    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
042    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
043    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
044    import org.openstreetmap.josm.gui.layer.Layer;
045    import org.openstreetmap.josm.tools.AudioPlayer;
046    import org.openstreetmap.josm.tools.ImageProvider;
047    
048    /**
049     * A layer holding markers.
050     *
051     * Markers are GPS points with a name and, optionally, a symbol code attached;
052     * marker layers can be created from waypoints when importing raw GPS data,
053     * but they may also come from other sources.
054     *
055     * The symbol code is for future use.
056     *
057     * The data is read only.
058     */
059    public class MarkerLayer extends Layer implements JumpToMarkerLayer {
060    
061        /**
062         * A list of markers.
063         */
064        public final List<Marker> data;
065        private boolean mousePressed = false;
066        public GpxLayer fromLayer = null;
067        private Marker currentMarker;
068    
069        @Deprecated
070        public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer, boolean addMouseHandlerInConstructor) {
071            this(indata, name, associatedFile, fromLayer);
072        }
073    
074        public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
075            super(name);
076            this.setAssociatedFile(associatedFile);
077            this.data = new ArrayList<Marker>();
078            this.fromLayer = fromLayer;
079            double firstTime = -1.0;
080            String lastLinkedFile = "";
081    
082            for (WayPoint wpt : indata.waypoints) {
083                /* calculate time differences in waypoints */
084                double time = wpt.time;
085                boolean wpt_has_link = wpt.attr.containsKey(GpxData.META_LINKS);
086                if (firstTime < 0 && wpt_has_link) {
087                    firstTime = time;
088                    for (Object oneLink : wpt.getCollection(GpxData.META_LINKS)) {
089                        if (oneLink instanceof GpxLink) {
090                            lastLinkedFile = ((GpxLink)oneLink).uri;
091                            break;
092                        }
093                    }
094                }
095                if (wpt_has_link) {
096                    for (Object oneLink : wpt.getCollection(GpxData.META_LINKS)) {
097                        if (oneLink instanceof GpxLink) {
098                            String uri = ((GpxLink)oneLink).uri;
099                            if (!uri.equals(lastLinkedFile)) {
100                                firstTime = time;
101                            }
102                            lastLinkedFile = uri;
103                            break;
104                        }
105                    }
106                }
107                Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, time - firstTime);
108                if (m != null) {
109                    data.add(m);
110                }
111            }
112        }
113    
114        @Override
115        public void hookUpMapView() {
116            Main.map.mapView.addMouseListener(new MouseAdapter() {
117                @Override public void mousePressed(MouseEvent e) {
118                    if (e.getButton() != MouseEvent.BUTTON1)
119                        return;
120                    boolean mousePressedInButton = false;
121                    if (e.getPoint() != null) {
122                        for (Marker mkr : data) {
123                            if (mkr.containsPoint(e.getPoint())) {
124                                mousePressedInButton = true;
125                                break;
126                            }
127                        }
128                    }
129                    if (! mousePressedInButton)
130                        return;
131                    mousePressed  = true;
132                    if (isVisible()) {
133                        Main.map.mapView.repaint();
134                    }
135                }
136                @Override public void mouseReleased(MouseEvent ev) {
137                    if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
138                        return;
139                    mousePressed = false;
140                    if (!isVisible())
141                        return;
142                    if (ev.getPoint() != null) {
143                        for (Marker mkr : data) {
144                            if (mkr.containsPoint(ev.getPoint())) {
145                                mkr.actionPerformed(new ActionEvent(this, 0, null));
146                            }
147                        }
148                    }
149                    Main.map.mapView.repaint();
150                }
151            });
152        }
153    
154        /**
155         * Return a static icon.
156         */
157        @Override public Icon getIcon() {
158            return ImageProvider.get("layer", "marker_small");
159        }
160    
161        @Override
162        public Color getColor(boolean ignoreCustom)
163        {
164            String name = getName();
165            return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray);
166        }
167    
168        /* for preferences */
169        static public Color getGenericColor()
170        {
171            return Main.pref.getColor(marktr("gps marker"), Color.gray);
172        }
173    
174        @Override public void paint(Graphics2D g, MapView mv, Bounds box) {
175            boolean showTextOrIcon = isTextOrIconShown();
176            g.setColor(getColor(true));
177    
178            if (mousePressed) {
179                boolean mousePressedTmp = mousePressed;
180                Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
181                for (Marker mkr : data) {
182                    if (mousePos != null && mkr.containsPoint(mousePos)) {
183                        mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
184                        mousePressedTmp = false;
185                    }
186                }
187            } else {
188                for (Marker mkr : data) {
189                    mkr.paint(g, mv, false, showTextOrIcon);
190                }
191            }
192        }
193    
194        @Override public String getToolTipText() {
195            return data.size()+" "+trn("marker", "markers", data.size());
196        }
197    
198        @Override public void mergeFrom(Layer from) {
199            MarkerLayer layer = (MarkerLayer)from;
200            data.addAll(layer.data);
201        }
202    
203        @Override public boolean isMergable(Layer other) {
204            return other instanceof MarkerLayer;
205        }
206    
207        @Override public void visitBoundingBox(BoundingXYVisitor v) {
208            for (Marker mkr : data) {
209                v.visit(mkr.getEastNorth());
210            }
211        }
212    
213        @Override public Object getInfoComponent() {
214            return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
215        }
216    
217        @Override public Action[] getMenuEntries() {
218            Collection<Action> components = new ArrayList<Action>();
219            components.add(LayerListDialog.getInstance().createShowHideLayerAction());
220            components.add(new ShowHideMarkerText(this));
221            components.add(LayerListDialog.getInstance().createDeleteLayerAction());
222            components.add(SeparatorLayerAction.INSTANCE);
223            components.add(new CustomizeColor(this));
224            components.add(SeparatorLayerAction.INSTANCE);
225            components.add(new SynchronizeAudio());
226            if (Main.pref.getBoolean("marker.traceaudio", true)) {
227                components.add (new MoveAudio());
228            }
229            components.add(new JumpToNextMarker(this));
230            components.add(new JumpToPreviousMarker(this));
231            components.add(new RenameLayerAction(getAssociatedFile(), this));
232            components.add(SeparatorLayerAction.INSTANCE);
233            components.add(new LayerListPopup.InfoAction(this));
234            return components.toArray(new Action[0]);
235        }
236    
237        public boolean synchronizeAudioMarkers(AudioMarker startMarker) {
238            if (startMarker != null && ! data.contains(startMarker)) {
239                startMarker = null;
240            }
241            if (startMarker == null) {
242                // find the first audioMarker in this layer
243                for (Marker m : data) {
244                    if (m instanceof AudioMarker) {
245                        startMarker = (AudioMarker) m;
246                        break;
247                    }
248                }
249            }
250            if (startMarker == null)
251                return false;
252    
253            // apply adjustment to all subsequent audio markers in the layer
254            double adjustment = AudioPlayer.position() - startMarker.offset; // in seconds
255            boolean seenStart = false;
256            URL url = startMarker.url();
257            for (Marker m : data) {
258                if (m == startMarker) {
259                    seenStart = true;
260                }
261                if (seenStart) {
262                    AudioMarker ma = (AudioMarker) m; // it must be an AudioMarker
263                    if (ma.url().equals(url)) {
264                        ma.adjustOffset(adjustment);
265                    }
266                }
267            }
268            return true;
269        }
270    
271        public AudioMarker addAudioMarker(double time, LatLon coor) {
272            // find first audio marker to get absolute start time
273            double offset = 0.0;
274            AudioMarker am = null;
275            for (Marker m : data) {
276                if (m.getClass() == AudioMarker.class) {
277                    am = (AudioMarker)m;
278                    offset = time - am.time;
279                    break;
280                }
281            }
282            if (am == null) {
283                JOptionPane.showMessageDialog(
284                        Main.parent,
285                        tr("No existing audio markers in this layer to offset from."),
286                        tr("Error"),
287                        JOptionPane.ERROR_MESSAGE
288                        );
289                return null;
290            }
291    
292            // make our new marker
293            AudioMarker newAudioMarker = new AudioMarker(coor,
294                    null, AudioPlayer.url(), this, time, offset);
295    
296            // insert it at the right place in a copy the collection
297            Collection<Marker> newData = new ArrayList<Marker>();
298            am = null;
299            AudioMarker ret = newAudioMarker; // save to have return value
300            for (Marker m : data) {
301                if (m.getClass() == AudioMarker.class) {
302                    am = (AudioMarker) m;
303                    if (newAudioMarker != null && offset < am.offset) {
304                        newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
305                        newData.add(newAudioMarker);
306                        newAudioMarker = null;
307                    }
308                }
309                newData.add(m);
310            }
311    
312            if (newAudioMarker != null) {
313                if (am != null) {
314                    newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
315                }
316                newData.add(newAudioMarker); // insert at end
317            }
318    
319            // replace the collection
320            data.clear();
321            data.addAll(newData);
322            return ret;
323        }
324    
325        public void jumpToNextMarker() {
326            if (currentMarker == null) {
327                currentMarker = data.get(0);
328            } else {
329                boolean foundCurrent = false;
330                for (Marker m: data) {
331                    if (foundCurrent) {
332                        currentMarker = m;
333                        break;
334                    } else if (currentMarker == m) {
335                        foundCurrent = true;
336                    }
337                }
338            }
339            Main.map.mapView.zoomTo(currentMarker.getEastNorth());
340        }
341    
342        public void jumpToPreviousMarker() {
343            if (currentMarker == null) {
344                currentMarker = data.get(data.size() - 1);
345            } else {
346                boolean foundCurrent = false;
347                for (int i=data.size() - 1; i>=0; i--) {
348                    Marker m = data.get(i);
349                    if (foundCurrent) {
350                        currentMarker = m;
351                        break;
352                    } else if (currentMarker == m) {
353                        foundCurrent = true;
354                    }
355                }
356            }
357            Main.map.mapView.zoomTo(currentMarker.getEastNorth());
358        }
359    
360        public static void playAudio() {
361            playAdjacentMarker(null, true);
362        }
363    
364        public static void playNextMarker() {
365            playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
366        }
367    
368        public static void playPreviousMarker() {
369            playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
370        }
371    
372        private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
373            Marker previousMarker = null;
374            boolean nextTime = false;
375            if (layer.getClass() == MarkerLayer.class) {
376                MarkerLayer markerLayer = (MarkerLayer) layer;
377                for (Marker marker : markerLayer.data) {
378                    if (marker == startMarker) {
379                        if (next) {
380                            nextTime = true;
381                        } else {
382                            if (previousMarker == null) {
383                                previousMarker = startMarker; // if no previous one, play the first one again
384                            }
385                            return previousMarker;
386                        }
387                    }
388                    else if (marker.getClass() == AudioMarker.class)
389                    {
390                        if(nextTime || startMarker == null)
391                            return marker;
392                        previousMarker = marker;
393                    }
394                }
395                if (nextTime) // there was no next marker in that layer, so play the last one again
396                    return startMarker;
397            }
398            return null;
399        }
400    
401        private static void playAdjacentMarker(Marker startMarker, boolean next) {
402            Marker m = null;
403            if (Main.map == null || Main.map.mapView == null)
404                return;
405            Layer l = Main.map.mapView.getActiveLayer();
406            if(l != null) {
407                m = getAdjacentMarker(startMarker, next, l);
408            }
409            if(m == null)
410            {
411                for (Layer layer : Main.map.mapView.getAllLayers())
412                {
413                    m = getAdjacentMarker(startMarker, next, layer);
414                    if(m != null) {
415                        break;
416                    }
417                }
418            }
419            if(m != null) {
420                ((AudioMarker)m).play();
421            }
422        }
423    
424        /**
425         * Get state of text display.
426         * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
427         */
428        private boolean isTextOrIconShown() {
429            String current = Main.pref.get("marker.show "+getName(),"show");
430            return "show".equalsIgnoreCase(current);
431        }
432    
433        public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
434            private final MarkerLayer layer;
435    
436            public ShowHideMarkerText(MarkerLayer layer) {
437                super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
438                putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
439                putValue("help", ht("/Action/ShowHideTextIcons"));
440                this.layer = layer;
441            }
442    
443    
444            public void actionPerformed(ActionEvent e) {
445                Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
446                Main.map.mapView.repaint();
447            }
448    
449    
450            @Override
451            public Component createMenuComponent() {
452                JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
453                showMarkerTextItem.setState(layer.isTextOrIconShown());
454                return showMarkerTextItem;
455            }
456    
457            @Override
458            public boolean supportLayers(List<Layer> layers) {
459                return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
460            }
461        }
462    
463    
464        private class SynchronizeAudio extends AbstractAction {
465    
466            public SynchronizeAudio() {
467                super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
468                putValue("help", ht("/Action/SynchronizeAudio"));
469            }
470    
471            @Override
472            public void actionPerformed(ActionEvent e) {
473                if (! AudioPlayer.paused()) {
474                    JOptionPane.showMessageDialog(
475                            Main.parent,
476                            tr("You need to pause audio at the moment when you hear your synchronization cue."),
477                            tr("Warning"),
478                            JOptionPane.WARNING_MESSAGE
479                            );
480                    return;
481                }
482                AudioMarker recent = AudioMarker.recentlyPlayedMarker();
483                if (synchronizeAudioMarkers(recent)) {
484                    JOptionPane.showMessageDialog(
485                            Main.parent,
486                            tr("Audio synchronized at point {0}.", recent.getText()),
487                            tr("Information"),
488                            JOptionPane.INFORMATION_MESSAGE
489                            );
490                } else {
491                    JOptionPane.showMessageDialog(
492                            Main.parent,
493                            tr("Unable to synchronize in layer being played."),
494                            tr("Error"),
495                            JOptionPane.ERROR_MESSAGE
496                            );
497                }
498            }
499        }
500    
501        private class MoveAudio extends AbstractAction {
502    
503            public MoveAudio() {
504                super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
505                putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
506            }
507    
508            @Override
509            public void actionPerformed(ActionEvent e) {
510                if (! AudioPlayer.paused()) {
511                    JOptionPane.showMessageDialog(
512                            Main.parent,
513                            tr("You need to have paused audio at the point on the track where you want the marker."),
514                            tr("Warning"),
515                            JOptionPane.WARNING_MESSAGE
516                            );
517                    return;
518                }
519                PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker;
520                if (playHeadMarker == null)
521                    return;
522                addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
523                Main.map.mapView.repaint();
524            }
525        }
526    
527    }