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