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