Ticket #15606: josm-export-route-relations-to-GPX-file_or_convert-to-GPX-layer.patch

File josm-export-route-relations-to-GPX-file_or_convert-to-GPX-layer.patch, 36.7 KB (added by cmuelle8, 9 years ago)

rework layer and segment naming, generate uniq names, generate single-segmented tracks by default (multiple tracks should generally only occur, if the relations are broken, unsorted or have wrong/incomplete forward/backward roles)

  • src/org/openstreetmap/josm/actions/GpxExportAction.java

     
    3939    }
    4040
    4141    /**
     42     * Deferring constructor for child classes.
     43     *
     44     * @param name see {@code DiskAccessAction}
     45     * @param iconName see {@code DiskAccessAction}
     46     * @param tooltip see {@code DiskAccessAction}
     47     * @param shortcut see {@code DiskAccessAction}
     48     * @param register see {@code DiskAccessAction}
     49     * @param toolbarId see {@code DiskAccessAction}
     50     * @param installAdapters see {@code DiskAccessAction}
     51     *
     52     * @since xxx
     53     */
     54    protected GpxExportAction(String name, String iconName, String tooltip, Shortcut shortcut,
     55            boolean register, String toolbarId, boolean installAdapters) {
     56        super(name, iconName, tooltip, shortcut, register, toolbarId, installAdapters);
     57    }
     58
     59    /**
    4260     * Get the layer to export.
    4361     * @return The layer to export, either a {@link GpxLayer} or {@link OsmDataLayer}.
    4462     */
  • src/org/openstreetmap/josm/actions/relation/ExportRelationToGpxAction.java

     
     1// License: GPL.
     2package org.openstreetmap.josm.actions.relation;
     3
     4import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.FROM_FIRST_MEMBER;
     5import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_FILE;
     6import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_LAYER;
     7import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
     8import static org.openstreetmap.josm.tools.I18n.tr;
     9
     10import java.awt.event.ActionEvent;
     11import java.util.ArrayList;
     12import java.util.Arrays;
     13import java.util.Collection;
     14import java.util.Collections;
     15import java.util.EnumSet;
     16import java.util.HashMap;
     17import java.util.Iterator;
     18import java.util.List;
     19import java.util.ListIterator;
     20import java.util.Map;
     21import java.util.Stack;
     22
     23import org.openstreetmap.josm.actions.GpxExportAction;
     24import org.openstreetmap.josm.actions.OsmPrimitiveAction;
     25import org.openstreetmap.josm.data.gpx.GpxData;
     26import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     27import org.openstreetmap.josm.data.gpx.WayPoint;
     28import org.openstreetmap.josm.data.osm.Node;
     29import org.openstreetmap.josm.data.osm.OsmPrimitive;
     30import org.openstreetmap.josm.data.osm.Relation;
     31import org.openstreetmap.josm.data.osm.RelationMember;
     32import org.openstreetmap.josm.data.osm.Way;
     33import org.openstreetmap.josm.gui.MainApplication;
     34import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
     35import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
     36import org.openstreetmap.josm.gui.layer.GpxLayer;
     37import org.openstreetmap.josm.gui.layer.Layer;
     38import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     39import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     40
     41/**
     42 * Exports the current relation to a single GPX track,
     43 * currently for type=route and type=superroute relations only.
     44 *
     45 * @since xxx
     46 */
     47public class ExportRelationToGpxAction extends GpxExportAction
     48    implements OsmPrimitiveAction {
     49
     50    /** Enumeration of export variants */
     51    public enum Mode {
     52        /** concatenate members from first to last element */
     53        FROM_FIRST_MEMBER,
     54        /** concatenate members from last to first element */
     55        FROM_LAST_MEMBER,
     56        /** export to GPX layer and add to LayerManager */
     57        TO_LAYER,
     58        /** export to GPX file and open FileChooser */
     59        TO_FILE
     60    }
     61
     62    /** Mode of this ExportToGpxAction */
     63    protected final EnumSet<Mode> mode;
     64
     65    /** Primitives this action works on */
     66    protected Collection<Relation> relations = Collections.<Relation>emptySet();
     67
     68    /** Construct a new ExportRelationToGpxAction with default mode */
     69    public ExportRelationToGpxAction() {
     70        this(EnumSet.of(FROM_FIRST_MEMBER, TO_FILE));
     71    }
     72
     73    /**
     74     * Constructs a new {@code ExportRelationToGpxAction}
     75     *
     76     * @param mode which mode to use, see {@code ExportRelationToGpxAction.Mode}
     77     */
     78    public ExportRelationToGpxAction(EnumSet<Mode> mode) {
     79        super(tr("{0} starting from {1} member",
     80                 mode.contains(TO_FILE) ? tr("Export GPX file") : tr("Convert to GPX layer"),
     81                 mode.contains(FROM_FIRST_MEMBER) ? tr("first") : tr("last")),
     82              mode.contains(TO_FILE) ? "exportgpx" : "dialogs/layerlist",
     83              tr("Flatten this relation to a single gpx track "+
     84                 "recursively, starting with the {0} member(s), "+
     85                 "successively continuing to the {1}.",
     86                 mode.contains(FROM_FIRST_MEMBER) ? tr("first") : tr("last"),
     87                 mode.contains(FROM_FIRST_MEMBER) ? tr("last") : tr("first")),
     88              null, false, null, false);
     89        putValue("help", ht("/Action/ExportRelationToGpx"));
     90        this.mode = mode;
     91    }
     92
     93    private static class BidiIterableList {
     94        private final List<RelationMember> l;
     95
     96        private BidiIterableList(List<RelationMember> l) {
     97            this.l = l;
     98        }
     99
     100        public Iterator<RelationMember> iterator() {
     101            return l.iterator();
     102        }
     103
     104        public Iterator<RelationMember> reverseIterator() {
     105            ListIterator<RelationMember> li = l.listIterator(l.size());
     106            return new Iterator<RelationMember>() {
     107                @Override
     108                public boolean hasNext() {
     109                    return li.hasPrevious();
     110                }
     111
     112                @Override
     113                public RelationMember next() {
     114                    return li.previous();
     115                }
     116
     117                @Override
     118                public void remove() {
     119                    li.remove();
     120                }
     121            };
     122        }
     123    }
     124
     125    @Override
     126    protected Layer getLayer() {
     127        List<RelationMember> flat = new ArrayList<>();
     128
     129        List<RelationMember> init = new ArrayList<>();
     130        relations.forEach(t -> init.add(new RelationMember("", t)));
     131        BidiIterableList l = new BidiIterableList(init);
     132
     133        Stack<Iterator<RelationMember>> stack = new Stack<>();
     134        stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator());
     135
     136        List<Relation> relsFound = new ArrayList<>();
     137        do {
     138            Iterator<RelationMember> i = stack.peek();
     139            if (!i.hasNext())
     140                stack.pop();
     141            while (i.hasNext()) {
     142                RelationMember m = i.next();
     143                if (m.isRelation() && !m.getRelation().isIncomplete()) {
     144                    l = new BidiIterableList(m.getRelation().getMembers());
     145                    stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator());
     146                    relsFound.add(m.getRelation());
     147                    break;
     148                }
     149                if (m.isWay()) {
     150                    flat.add(m);
     151                }
     152            }
     153        } while (!stack.isEmpty());
     154
     155        GpxData gpxData = new GpxData();
     156        String layerName = " (GPX export)";
     157        long time = System.currentTimeMillis()-24*3600*1000;
     158
     159        if (!flat.isEmpty()) {
     160            Map<String, Object> trkAttr = new HashMap<>();
     161            Collection<Collection<WayPoint>> trk = new ArrayList<>();
     162            List<WayPoint> trkseg = new ArrayList<>();
     163            trk.add(trkseg);
     164
     165            //RelationSorter.sortMembersByConnectivity(defaultMembers); // preserve given order
     166            List<WayConnectionType> wct = new WayConnectionTypeCalculator().updateLinks(flat);
     167            final HashMap<String, Integer> names = new HashMap<>();
     168            for (int i=0; i<flat.size(); i++) {
     169                if (!wct.get(i).isOnewayLoopBackwardPart) {
     170                    if (!wct.get(i).direction.isRoundabout()) {
     171                        if (!wct.get(i).linkPrev && trkseg.size() > 0) {
     172                            gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
     173                            trkAttr.clear();
     174                            trk.clear();
     175                            trkseg.clear();
     176                            trk.add(trkseg);
     177                        }
     178                        if (trkAttr.isEmpty()/* && (!wct.get(i).linkNext || (i+1 == flat.size()))*/) {
     179                            Relation r = Way.getParentRelations(Arrays.asList(flat.get(i).getWay()))
     180                                    .stream().filter(relsFound::contains).findFirst().orElseGet(null);
     181                            if (r != null)
     182                                trkAttr.put("name", r.getName()!=null ? r.getName() : r.getId());
     183                            GpxData.ensureUniqueName(trkAttr, names);
     184                        }
     185                        List<Node> ln = flat.get(i).getWay().getNodes();
     186                        if (wct.get(i).direction == WayConnectionType.Direction.BACKWARD)
     187                            Collections.reverse(ln);
     188                        for (Node n: ln) {
     189                            trkseg.add(OsmDataLayer.nodeToWayPoint(n,time));
     190                            time += 1000;
     191                        }
     192                    }
     193                }
     194            }
     195            gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
     196
     197            String lprefix = relations.iterator().next().getName();
     198            if (lprefix==null || relations.size()>1)
     199                lprefix = tr("Selected Relations");
     200            layerName = lprefix + layerName;
     201        }
     202
     203        //Relation debug = new Relation(); debug.setMembers(flat);
     204        //MainApplication.getLayerManager().getEditDataSet().addPrimitive(debug);
     205        return new GpxLayer(gpxData, layerName, true);
     206    }
     207
     208    /**
     209     *
     210     * @param e the ActionEvent
     211     */
     212    @Override
     213    public void actionPerformed(ActionEvent e) {
     214        if(mode.contains(TO_LAYER))
     215            MainApplication.getLayerManager().addLayer(getLayer());
     216        if(mode.contains(TO_FILE))
     217            super.actionPerformed(e);
     218    }
     219
     220    @Override
     221    public void setPrimitives(Collection<? extends OsmPrimitive> primitives) {
     222        relations = Collections.<Relation>emptySet();
     223        if (primitives != null && !primitives.isEmpty()) {
     224            relations = new SubclassFilteredCollection<>(primitives,
     225                r -> r instanceof Relation && r.hasTag("type", Arrays.asList("route", "superroute")));
     226        }
     227        updateEnabledState();
     228    }
     229
     230    @Override
     231    protected void updateEnabledState() {
     232        setEnabled(!relations.isEmpty());
     233    }
     234}
  • src/org/openstreetmap/josm/data/gpx/GpxData.java

     
    44import java.io.File;
    55import java.text.MessageFormat;
    66import java.util.ArrayList;
     7import java.util.Arrays;
    78import java.util.Collection;
    89import java.util.Collections;
    910import java.util.Date;
    1011import java.util.DoubleSummaryStatistics;
     12import java.util.HashMap;
    1113import java.util.HashSet;
    1214import java.util.Iterator;
     15import java.util.List;
    1316import java.util.Map;
    1417import java.util.NoSuchElementException;
    1518import java.util.Set;
     19import java.util.stream.Collectors;
    1620import java.util.stream.Stream;
    1721
    1822import org.openstreetmap.josm.Main;
     
    2125import org.openstreetmap.josm.data.DataSource;
    2226import org.openstreetmap.josm.data.coor.EastNorth;
    2327import org.openstreetmap.josm.data.gpx.GpxTrack.GpxTrackChangeListener;
     28import org.openstreetmap.josm.gui.MainApplication;
     29import org.openstreetmap.josm.gui.layer.GpxLayer;
    2430import org.openstreetmap.josm.tools.ListenerList;
    2531import org.openstreetmap.josm.tools.ListeningCollection;
    2632
     
    140146    }
    141147
    142148    /**
     149     * Get Stream<> of track segments as introduced in Java 8.
     150     * @return {@code Stream<GPXTrack>}
     151     */
     152    private synchronized Stream<GpxTrackSegment> getTrackSegmentsStream() {
     153        return getTracks().stream().flatMap(trk -> trk.getSegments().stream());
     154    }
     155
     156    /**
     157     * Clear all tracks, empties the current privateTracks container,
     158     * helper method for some gpx manipulations.
     159     */
     160    private synchronized void clearTracks() {
     161        privateTracks.forEach(t -> { t.removeListener(proxy); });
     162        privateTracks.clear();
     163    }
     164
     165    /**
    143166     * Add a new track
    144167     * @param track The new track
    145168     * @since 12156
     
    167190    }
    168191
    169192    /**
     193     * Combine tracks into a single, segmented track.
     194     * The attributes of the first track are used, the rest discarded.
     195     *
     196     * @since xxx
     197     */
     198    public synchronized void combineTracksToSegmentedTrack() {
     199        List<GpxTrackSegment> segs = getTrackSegmentsStream()
     200                .collect(Collectors.toCollection(ArrayList<GpxTrackSegment>::new));
     201        Map<String, Object> attrs = new HashMap<>(privateTracks.get(0).getAttributes());
     202
     203        // do not let the name grow if split / combine operations are called iteratively
     204        attrs.put("name", attrs.get("name").toString().replaceFirst(" #\\d+$", ""));
     205
     206        clearTracks();
     207        addTrack(new ImmutableGpxTrack(segs, attrs));
     208    }
     209
     210    /**
     211     * @param attrs attributes of/for an gpx track, written to if the name appeared previously in {@code counts}.
     212     * @param counts a {@code HashMap} of previously seen names, associated with their count.
     213     * @return the unique name for the gpx track.
     214     *
     215     * @since xxx
     216     */
     217    public static String ensureUniqueName(Map<String, Object> attrs, Map<String, Integer> counts) {
     218        String name = attrs.getOrDefault("name", "GPX split result").toString();
     219        Integer count = counts.getOrDefault(name, 0) + 1;
     220        counts.put(name, count);
     221
     222        attrs.put("name", MessageFormat.format("{0}{1}", name, (count>1) ? " #"+count : ""));
     223        return attrs.get("name").toString();
     224    }
     225
     226    /**
     227     * Split tracks so that only single-segment tracks remain.
     228     * Each segment will make up one individual track after this operation.
     229     *
     230     * @since xxx
     231     */
     232    public synchronized void splitTrackSegmentsToTracks() {
     233        final HashMap<String, Integer> counts = new HashMap<>();
     234
     235        List<GpxTrack> trks = getTracks().stream()
     236            .flatMap(trk -> {
     237                return trk.getSegments().stream().map(seg -> {
     238                    HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
     239                    ensureUniqueName(attrs, counts);
     240                    return new ImmutableGpxTrack(Arrays.asList(seg), attrs);
     241                });
     242            })
     243            .collect(Collectors.toCollection(ArrayList<GpxTrack>::new));
     244
     245        clearTracks();
     246        trks.stream().forEachOrdered(trk -> addTrack(trk));
     247    }
     248
     249    /**
     250     * Split tracks into layers, the result is one layer for each track.
     251     * If this layer currently has only one GpxTrack this is a no-operation.
     252     *
     253     * The new GpxLayers are added to the LayerManager, the original GpxLayer
     254     * is untouched as to preserve potential route or wpt parts.
     255     *
     256     * @since xxx
     257     */
     258    public synchronized void splitTracksToLayers() {
     259        final HashMap<String, Integer> counts = new HashMap<>();
     260
     261        getTracks().stream()
     262            .filter(trk -> privateTracks.size() > 1)
     263            .map(trk -> {
     264                HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
     265                GpxData d = new GpxData();
     266                d.addTrack(trk);
     267                return new GpxLayer(d, ensureUniqueName(attrs, counts)); })
     268            .forEachOrdered(layer -> MainApplication.getLayerManager().addLayer(layer));
     269    }
     270
     271    /**
     272     * Replies the current number of tracks in this GpxData
     273     * @return track count
     274     * @since xxx
     275     */
     276    public synchronized int getTrackCount() {
     277        return privateTracks.size();
     278    }
     279
     280    /**
     281     * Replies the accumulated total of all track segments,
     282     * the sum of segment counts for each track present.
     283     * @return track segments count
     284     * @since xxx
     285     */
     286    public synchronized int getTrackSegsCount() {
     287        return privateTracks.stream().collect(Collectors.summingInt(t -> t.getSegments().size()));
     288    }
     289
     290    /**
    170291     * Gets the list of all routes defined in this data set.
    171292     * @return The routes
    172293     * @since 12156
  • src/org/openstreetmap/josm/data/gpx/ImmutableGpxTrack.java

     
    3838        this.bounds = calculateBounds();
    3939    }
    4040
     41    /**
     42     * Constructs a new {@code ImmutableGpxTrack} from {@code GpxTrackSegment} objects.
     43     * @param segments The segments to build the track from.  Input is not deep-copied,
     44     *                 which means the caller may reuse the same segments to build
     45     *                 multiple ImmutableGpxTrack instances from.  This should not be
     46     *                 a problem, since this object cannot modify {@code this.segments}.
     47     * @param attributes Attributes for the GpxTrack, the input map is copied.
     48     */
     49    public ImmutableGpxTrack(List<GpxTrackSegment> segments, Map<String, Object> attributes) {
     50        this.attr = Collections.unmodifiableMap(new HashMap<>(attributes));
     51        this.segments = Collections.unmodifiableList(segments);
     52        this.length = calculateLength();
     53        this.bounds = calculateBounds();
     54    }
     55
    4156    private double calculateLength() {
    4257        double result = 0.0; // in meters
    4358
  • src/org/openstreetmap/josm/data/gpx/WayPoint.java

     
    145145    }
    146146
    147147    /**
     148     * Set the the time stamp of the waypoint into seconds from the epoch,
     149     * @param time millisecond from the epoch
     150     */
     151    public void setTime(long time) {
     152        this.time = time / 1000.;
     153    }
     154
     155    /**
    148156     * Convert the time stamp of the waypoint into seconds from the epoch
    149157     * @return The parsed time if successful, or {@code null}
    150158     * @since 9383
  • src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java

     
    1212import java.util.Arrays;
    1313import java.util.Collection;
    1414import java.util.Collections;
     15import java.util.EnumSet;
    1516import java.util.HashSet;
    1617import java.util.List;
    1718import java.util.Set;
     
    2829import javax.swing.JScrollPane;
    2930import javax.swing.KeyStroke;
    3031import javax.swing.ListSelectionModel;
     32import javax.swing.event.PopupMenuEvent;
     33import javax.swing.event.PopupMenuListener;
    3134
    3235import org.openstreetmap.josm.Main;
    3336import org.openstreetmap.josm.actions.ExpertToggleAction;
     37import org.openstreetmap.josm.actions.OsmPrimitiveAction;
    3438import org.openstreetmap.josm.actions.relation.AddSelectionToRelations;
    3539import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
    3640import org.openstreetmap.josm.actions.relation.DownloadMembersAction;
    3741import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
    3842import org.openstreetmap.josm.actions.relation.DuplicateRelationAction;
    3943import org.openstreetmap.josm.actions.relation.EditRelationAction;
     44import org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction;
     45import org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode;
    4046import org.openstreetmap.josm.actions.relation.RecentRelationsAction;
    4147import org.openstreetmap.josm.actions.relation.SelectMembersAction;
    4248import org.openstreetmap.josm.actions.relation.SelectRelationAction;
     
    121127    private final AddSelectionToRelations addSelectionToRelations = new AddSelectionToRelations();
    122128    private transient JMenuItem addSelectionToRelationMenuItem;
    123129
     130    /** export relation to GPX track action */
     131    private final ExportRelationToGpxAction exportRelationFromFirstAction =
     132            new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_FIRST_MEMBER, Mode.TO_FILE));
     133    private final ExportRelationToGpxAction exportRelationFromLastAction =
     134            new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_LAST_MEMBER, Mode.TO_FILE));
     135    private final ExportRelationToGpxAction exportRelationFromFirstToLayerAction =
     136            new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_FIRST_MEMBER, Mode.TO_LAYER));
     137    private final ExportRelationToGpxAction exportRelationFromLastToLayerAction =
     138            new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_LAST_MEMBER, Mode.TO_LAYER));
     139
    124140    private final transient HighlightHelper highlightHelper = new HighlightHelper();
    125141    private final boolean highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);
    126142    private final transient RecentRelationsAction recentRelationsAction;
     
    602618    }
    603619
    604620    private void setupPopupMenuHandler() {
     621        List<JMenuItem> checkDisabled = new ArrayList<>();
    605622
    606623        // -- select action
    607624        popupMenuHandler.addAction(selectRelationAction);
     
    611628        popupMenuHandler.addAction(selectMembersAction);
    612629        popupMenuHandler.addAction(addMembersToSelectionAction);
    613630
    614         popupMenuHandler.addSeparator();
    615631        // -- download members action
     632        popupMenuHandler.addSeparator();
    616633        popupMenuHandler.addAction(downloadMembersAction);
    617 
    618         // -- download incomplete members action
    619634        popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction);
    620635
     636        // -- export relation to gpx action
     637        popupMenuHandler.addSeparator();
     638        checkDisabled.add(popupMenuHandler.addAction(exportRelationFromFirstAction));
     639        checkDisabled.add(popupMenuHandler.addAction(exportRelationFromLastAction));
     640        popupMenuHandler.addSeparator();
     641        checkDisabled.add(popupMenuHandler.addAction(exportRelationFromFirstToLayerAction));
     642        checkDisabled.add(popupMenuHandler.addAction(exportRelationFromLastToLayerAction));
     643
    621644        popupMenuHandler.addSeparator();
    622645        popupMenuHandler.addAction(editAction).setVisible(false);
    623646        popupMenuHandler.addAction(duplicateAction).setVisible(false);
    624647        popupMenuHandler.addAction(deleteRelationsAction).setVisible(false);
    625648
    626649        addSelectionToRelationMenuItem = popupMenuHandler.addAction(addSelectionToRelations);
     650
     651        popupMenuHandler.addListener(new PopupMenuListener() {
     652            @Override
     653            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
     654                for (JMenuItem mi: checkDisabled) {
     655                    mi.setVisible(((OsmPrimitiveAction) mi.getAction()).isEnabled());
     656
     657                    Component sep = popupMenu.getComponent(
     658                            Math.max(0, popupMenu.getComponentIndex(mi)-1));
     659                    if (!(sep instanceof JMenuItem)) {
     660                        sep.setVisible(mi.isVisible());
     661                    }
     662                }
     663            }
     664
     665            @Override
     666            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
     667            }
     668
     669            @Override
     670            public void popupMenuCanceled(PopupMenuEvent e) {
     671            }
     672        });
    627673    }
    628674
    629675    /* ---------------------------------------------------------------------------------- */
  • src/org/openstreetmap/josm/gui/layer/GpxLayer.java

     
    66
    77import java.awt.Dimension;
    88import java.awt.Graphics2D;
     9import java.awt.event.ActionEvent;
    910import java.io.File;
    1011import java.text.DateFormat;
     12import java.util.ArrayList;
    1113import java.util.Arrays;
    1214import java.util.Date;
     15import java.util.List;
    1316
     17import javax.swing.AbstractAction;
    1418import javax.swing.Action;
    1519import javax.swing.Icon;
    1620import javax.swing.JScrollPane;
    1721import javax.swing.SwingUtilities;
    1822
     23import org.openstreetmap.josm.actions.ExpertToggleAction;
     24import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener;
    1925import org.openstreetmap.josm.actions.RenameLayerAction;
    2026import org.openstreetmap.josm.actions.SaveActionBase;
    2127import org.openstreetmap.josm.data.Bounds;
     
    4753/**
    4854 * A layer that displays data from a Gpx file / the OSM gpx downloads.
    4955 */
    50 public class GpxLayer extends Layer {
     56public class GpxLayer extends Layer implements ExpertModeChangeListener {
    5157
    5258    /** GPX data */
    5359    public GpxData data;
    5460    private final boolean isLocalFile;
     61    private boolean isExpertMode;
    5562    /**
    5663     * used by {@link ChooseTrackVisibilityAction} to determine which tracks to show/hide
    5764     *
     
    8390    }
    8491
    8592    /**
    86      * Constructs a new {@code GpxLayer} with a given name, thah can be attached to a local file.
     93     * Constructs a new {@code GpxLayer} with a given name, that can be attached to a local file.
    8794     * @param d GPX data
    8895     * @param name layer name
    8996     * @param isLocal whether data is attached to a local file
     
    96103        Arrays.fill(trackVisibility, true);
    97104        setName(name);
    98105        isLocalFile = isLocal;
     106        ExpertToggleAction.addExpertModeChangeListener(this, true);
    99107    }
    100108
    101109    @Override
     
    138146
    139147    @Override
    140148    public Object getInfoComponent() {
    141         StringBuilder info = new StringBuilder(48).append("<html>");
     149        StringBuilder info = new StringBuilder(48)
     150                .append("<html><head><style>")
     151                .append("td { padding: 4px 16px; }")
     152                .append("</style></head><body>");
    142153
    143154        if (data.attr.containsKey("name")) {
    144155            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
     
    150161
    151162        if (!data.getTracks().isEmpty()) {
    152163            info.append("<table><thead align='center'><tr><td colspan='5'>")
    153                 .append(trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size()))
    154                 .append("</td></tr><tr align='center'><td>").append(tr("Name")).append("</td><td>")
    155                 .append(tr("Description")).append("</td><td>").append(tr("Timespan"))
    156                 .append("</td><td>").append(tr("Length")).append("</td><td>").append(tr("URL"))
     164                .append(trn("{0} track, {1} track segments", "{0} tracks, {1} track segments",
     165                        data.getTrackCount(), data.getTrackCount(),
     166                        data.getTrackSegsCount(), data.getTrackSegsCount()))
     167                .append("</td></tr><tr align='center'><td>").append(tr("Name"))
     168                .append("</td><td>").append(tr("Description"))
     169                .append("</td><td>").append(tr("Timespan"))
     170                .append("</td><td>").append(tr("Length"))
     171                .append("</td><td>").append(tr("Number of<br/>Segments"))
     172                .append("</td><td>").append(tr("URL"))
    157173                .append("</td></tr></thead>");
    158174
    159175            for (GpxTrack trk : data.getTracks()) {
     
    170186                info.append("</td><td>");
    171187                info.append(SystemOfMeasurement.getSystemOfMeasurement().getDistText(trk.length()));
    172188                info.append("</td><td>");
     189                info.append(trk.getSegments().size());
     190                info.append("</td><td>");
    173191                if (trk.getAttributes().containsKey("url")) {
    174192                    info.append(trk.get("url"));
    175193                }
     
    180198
    181199        info.append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))).append("<br>")
    182200            .append(trn("{0} route, ", "{0} routes, ", data.getRoutes().size(), data.getRoutes().size()))
    183             .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size())).append("<br></html>");
     201            .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size()))
     202            .append("<br></body></html>");
    184203
    185204        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()));
    186205        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370));
     
    195214
    196215    @Override
    197216    public Action[] getMenuEntries() {
    198         return new Action[] {
     217        List<Action> entries = new ArrayList<>(Arrays.asList(
    199218                LayerListDialog.getInstance().createShowHideLayerAction(),
    200219                LayerListDialog.getInstance().createDeleteLayerAction(),
    201220                LayerListDialog.getInstance().createMergeLayerAction(this),
     
    212231                new DownloadWmsAlongTrackAction(data),
    213232                SeparatorLayerAction.INSTANCE,
    214233                new ChooseTrackVisibilityAction(this),
    215                 new RenameLayerAction(getAssociatedFile(), this),
    216                 SeparatorLayerAction.INSTANCE,
    217                 new LayerListPopup.InfoAction(this) };
     234                new RenameLayerAction(getAssociatedFile(), this)));
     235
     236        List<Action> expert = Arrays.asList(
     237                new CombineTracksToSegmentedTrackAction(this),
     238                new SplitTrackSegementsToTracksAction(this),
     239                new SplitTracksToLayersAction(this));
     240
     241        if (isExpertMode && expert.stream().anyMatch(t -> t.isEnabled())) {
     242            entries.add(SeparatorLayerAction.INSTANCE);
     243            expert.stream().filter(t -> t.isEnabled()).forEach(t -> entries.add(t));
     244        }
     245
     246        entries.add(SeparatorLayerAction.INSTANCE);
     247        entries.add(new LayerListPopup.InfoAction(this));
     248        return entries.toArray(new Action[0]);
    218249    }
    219250
    220251    /**
     
    237268            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
    238269        }
    239270
    240         info.append(trn("{0} track, ", "{0} tracks, ", data.getTracks().size(), data.getTracks().size()))
     271        info.append(trn("{0} track", "{0} tracks", data.getTrackCount(), data.getTrackCount()))
     272            .append(trn(" ({0} segment)", " ({0} segments)", data.getTrackSegsCount(), data.getTrackSegsCount()))
     273            .append(", ")
    241274            .append(trn("{0} route, ", "{0} routes, ", data.getRoutes().size(), data.getRoutes().size()))
    242275            .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size())).append("<br>")
    243276            .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())))
     
    329362    protected LayerPainter createMapViewPainter(MapViewEvent event) {
    330363        return new GpxDrawHelper(this);
    331364    }
     365
     366    /**
     367     * Action to merge tracks into a single segmented track
     368     *
     369     * @since xxx
     370     */
     371    public static class CombineTracksToSegmentedTrackAction extends AbstractAction {
     372        private final transient GpxLayer layer;
     373
     374        /**
     375         * Create a new CombineTracksToSegmentedTrackAction
     376         * @param layer The layer with the data to work on.
     377         */
     378        public CombineTracksToSegmentedTrackAction(GpxLayer layer) {
     379            // FIXME: icon missing, create a new icon for this action
     380            //new ImageProvider("gpx_tracks_to_segmented_track").getResource().attachImageIcon(this, true);
     381            putValue(SHORT_DESCRIPTION, tr("Collect segments of all tracks and combine in a single track."));
     382            putValue(NAME, tr("Combine tracks of this layer"));
     383            this.layer = layer;
     384        }
     385
     386        @Override
     387        public void actionPerformed(ActionEvent e) {
     388            layer.data.combineTracksToSegmentedTrack();
     389            layer.invalidate();
     390        }
     391
     392        @Override
     393        public boolean isEnabled() {
     394            return layer.data.getTrackCount() > 1;
     395        }
     396    }
     397
     398    /**
     399     * Action to split track segments into a multiple tracks with one segment each
     400     *
     401     * @since xxx
     402     */
     403    public static class SplitTrackSegementsToTracksAction extends AbstractAction {
     404        private final transient GpxLayer layer;
     405
     406        /**
     407         * Create a new SplitTrackSegementsToTracksAction
     408         * @param layer The layer with the data to work on.
     409         */
     410        public SplitTrackSegementsToTracksAction(GpxLayer layer) {
     411            // FIXME: icon missing, create a new icon for this action
     412            //new ImageProvider("gpx_segmented_track_to_tracks").getResource().attachImageIcon(this, true);
     413            putValue(SHORT_DESCRIPTION, tr("Split multiple track segments of one track into multiple tracks."));
     414            putValue(NAME, tr("Split track segments to tracks"));
     415            this.layer = layer;
     416        }
     417
     418        @Override
     419        public void actionPerformed(ActionEvent e) {
     420            layer.data.splitTrackSegmentsToTracks();
     421            layer.invalidate();
     422        }
     423
     424        @Override
     425        public boolean isEnabled() {
     426            return layer.data.getTrackSegsCount() > layer.data.getTrackCount();
     427        }
     428    }
     429
     430    /**
     431     * Action to split tracks of one gpx layer into multiple gpx layers,
     432     * the result is one GPX track per gpx layer.
     433     *
     434     * @since xxx
     435     */
     436    public static class SplitTracksToLayersAction extends AbstractAction {
     437        private final transient GpxLayer layer;
     438
     439        /**
     440         * Create a new SplitTrackSegementsToTracksAction
     441         * @param layer The layer with the data to work on.
     442         */
     443        public SplitTracksToLayersAction(GpxLayer layer) {
     444            // FIXME: icon missing, create a new icon for this action
     445            //new ImageProvider("gpx_split_tracks_to_layers").getResource().attachImageIcon(this, true);
     446            putValue(SHORT_DESCRIPTION, tr("Split the tracks of this layer to one new layer each."));
     447            putValue(NAME, tr("Split tracks to new layers"));
     448            this.layer = layer;
     449        }
     450
     451        @Override
     452        public void actionPerformed(ActionEvent e) {
     453            layer.data.splitTracksToLayers();
     454            // layer is not modified by this action
     455            //layer.invalidate();
     456        }
     457
     458        @Override
     459        public boolean isEnabled() {
     460            return layer.data.getTrackCount() > 1;
     461        }
     462    }
     463
     464    @Override
     465    public void expertChanged(boolean isExpert) {
     466        this.isExpertMode = isExpert;
     467    }
    332468}
  • src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

     
    739739        });
    740740    }
    741741
    742     private static WayPoint nodeToWayPoint(Node n) {
     742    /**
     743     * @param n the {@code Node} to convert
     744     * @return {@code WayPoint} object
     745     */
     746    public static WayPoint nodeToWayPoint(Node n) {
     747        return nodeToWayPoint(n, 0);
     748    }
     749
     750    /**
     751     * @param n the {@code Node} to convert
     752     * @param time a time value in milliseconds from the epoch.
     753     * @return {@code WayPoint} object
     754     */
     755    public static WayPoint nodeToWayPoint(Node n, long time) {
    743756        WayPoint wpt = new WayPoint(n.getCoor());
    744757
    745758        // Position info
    746759
    747760        addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
    748761
    749         if (!n.isTimestampEmpty()) {
     762        if (time > 0) {
     763            wpt.setTime(time);
     764        } else if (!n.isTimestampEmpty()) {
    750765            wpt.put("time", DateUtils.fromTimestamp(n.getRawTimestamp()));
    751766            wpt.setTime();
    752767        }
  • test/unit/org/openstreetmap/josm/data/gpx/GpxDataTest.java

     
    99
    1010import java.util.ArrayList;
    1111import java.util.Arrays;
     12import java.util.Collection;
    1213import java.util.Collections;
    1314import java.util.Date;
    1415import java.util.List;
     
    423424    }
    424425
    425426    private static ImmutableGpxTrack emptyGpxTrack() {
    426         return new ImmutableGpxTrack(Collections.emptyList(), Collections.emptyMap());
     427        return new ImmutableGpxTrack(Collections.<Collection<WayPoint>>emptyList(), Collections.emptyMap());
    427428    }
    428429
    429430    private static ImmutableGpxTrack singleWaypointGpxTrack() {