001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.relation;
003
004import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.FROM_FIRST_MEMBER;
005import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_FILE;
006import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_LAYER;
007import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
008import static org.openstreetmap.josm.tools.I18n.tr;
009
010import java.awt.event.ActionEvent;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.EnumSet;
016import java.util.HashMap;
017import java.util.Iterator;
018import java.util.List;
019import java.util.ListIterator;
020import java.util.Map;
021import java.util.Stack;
022
023import org.openstreetmap.josm.actions.GpxExportAction;
024import org.openstreetmap.josm.actions.OsmPrimitiveAction;
025import org.openstreetmap.josm.data.gpx.GpxData;
026import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
027import org.openstreetmap.josm.data.gpx.WayPoint;
028import org.openstreetmap.josm.data.osm.Node;
029import org.openstreetmap.josm.data.osm.OsmPrimitive;
030import org.openstreetmap.josm.data.osm.Relation;
031import org.openstreetmap.josm.data.osm.RelationMember;
032import org.openstreetmap.josm.data.osm.Way;
033import org.openstreetmap.josm.gui.MainApplication;
034import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
035import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
036import org.openstreetmap.josm.gui.layer.GpxLayer;
037import org.openstreetmap.josm.gui.layer.Layer;
038import org.openstreetmap.josm.gui.layer.OsmDataLayer;
039import org.openstreetmap.josm.tools.SubclassFilteredCollection;
040
041/**
042 * Exports the current relation to a single GPX track,
043 * currently for type=route and type=superroute relations only.
044 *
045 * @since 13210
046 */
047public class ExportRelationToGpxAction extends GpxExportAction
048    implements OsmPrimitiveAction {
049
050    /** Enumeration of export variants */
051    public enum Mode {
052        /** concatenate members from first to last element */
053        FROM_FIRST_MEMBER,
054        /** concatenate members from last to first element */
055        FROM_LAST_MEMBER,
056        /** export to GPX layer and add to LayerManager */
057        TO_LAYER,
058        /** export to GPX file and open FileChooser */
059        TO_FILE
060    }
061
062    /** Mode of this ExportToGpxAction */
063    protected final EnumSet<Mode> mode;
064
065    /** Primitives this action works on */
066    protected Collection<Relation> relations = Collections.<Relation>emptySet();
067
068    /** Construct a new ExportRelationToGpxAction with default mode */
069    public ExportRelationToGpxAction() {
070        this(EnumSet.of(FROM_FIRST_MEMBER, TO_FILE));
071    }
072
073    /**
074     * Constructs a new {@code ExportRelationToGpxAction}
075     *
076     * @param mode which mode to use, see {@code ExportRelationToGpxAction.Mode}
077     */
078    public ExportRelationToGpxAction(EnumSet<Mode> mode) {
079        super(name(mode), mode.contains(TO_FILE) ? "exportgpx" : "dialogs/layerlist", tooltip(mode),
080                null, false, null, false);
081        putValue("help", ht("/Action/ExportRelationToGpx"));
082        this.mode = mode;
083    }
084
085    private static String name(EnumSet<Mode> mode) {
086        if (mode.contains(TO_FILE)) {
087            if (mode.contains(FROM_FIRST_MEMBER)) {
088                return tr("Export GPX file starting from first member");
089            } else {
090                return tr("Export GPX file starting from last member");
091            }
092        } else {
093            if (mode.contains(FROM_FIRST_MEMBER)) {
094                return tr("Convert to GPX layer starting from first member");
095            } else {
096                return tr("Convert to GPX layer starting from last member");
097            }
098        }
099    }
100
101    private static String tooltip(EnumSet<Mode> mode) {
102        if (mode.contains(FROM_FIRST_MEMBER)) {
103            return tr("Flatten this relation to a single gpx track recursively, " +
104                    "starting with the first member, successively continuing to the last.");
105        } else {
106            return tr("Flatten this relation to a single gpx track recursively, " +
107                    "starting with the last member, successively continuing to the first.");
108        }
109    }
110
111    private static final class BidiIterableList {
112        private final List<RelationMember> l;
113
114        private BidiIterableList(List<RelationMember> l) {
115            this.l = l;
116        }
117
118        public Iterator<RelationMember> iterator() {
119            return l.iterator();
120        }
121
122        public Iterator<RelationMember> reverseIterator() {
123            ListIterator<RelationMember> li = l.listIterator(l.size());
124            return new Iterator<RelationMember>() {
125                @Override
126                public boolean hasNext() {
127                    return li.hasPrevious();
128                }
129
130                @Override
131                public RelationMember next() {
132                    return li.previous();
133                }
134
135                @Override
136                public void remove() {
137                    li.remove();
138                }
139            };
140        }
141    }
142
143    @Override
144    protected Layer getLayer() {
145        List<RelationMember> flat = new ArrayList<>();
146
147        List<RelationMember> init = new ArrayList<>();
148        relations.forEach(t -> init.add(new RelationMember("", t)));
149        BidiIterableList l = new BidiIterableList(init);
150
151        Stack<Iterator<RelationMember>> stack = new Stack<>();
152        stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator());
153
154        List<Relation> relsFound = new ArrayList<>();
155        do {
156            Iterator<RelationMember> i = stack.peek();
157            if (!i.hasNext())
158                stack.pop();
159            while (i.hasNext()) {
160                RelationMember m = i.next();
161                if (m.isRelation() && !m.getRelation().isIncomplete()) {
162                    l = new BidiIterableList(m.getRelation().getMembers());
163                    stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator());
164                    relsFound.add(m.getRelation());
165                    break;
166                }
167                if (m.isWay()) {
168                    flat.add(m);
169                }
170            }
171        } while (!stack.isEmpty());
172
173        GpxData gpxData = new GpxData();
174        String layerName = " (GPX export)";
175        long time = System.currentTimeMillis()-24*3600*1000;
176
177        if (!flat.isEmpty()) {
178            Map<String, Object> trkAttr = new HashMap<>();
179            Collection<Collection<WayPoint>> trk = new ArrayList<>();
180            List<WayPoint> trkseg = new ArrayList<>();
181            trk.add(trkseg);
182
183            List<WayConnectionType> wct = new WayConnectionTypeCalculator().updateLinks(flat);
184            final HashMap<String, Integer> names = new HashMap<>();
185            for (int i = 0; i < flat.size(); i++) {
186                if (!wct.get(i).isOnewayLoopBackwardPart) {
187                    if (!wct.get(i).direction.isRoundabout()) {
188                        if (!wct.get(i).linkPrev && !trkseg.isEmpty()) {
189                            gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
190                            trkAttr.clear();
191                            trk.clear();
192                            trkseg.clear();
193                            trk.add(trkseg);
194                        }
195                        if (trkAttr.isEmpty()) {
196                            Relation r = Way.getParentRelations(Arrays.asList(flat.get(i).getWay()))
197                                    .stream().filter(relsFound::contains).findFirst().orElseGet(null);
198                            if (r != null)
199                                trkAttr.put("name", r.getName() != null ? r.getName() : r.getId());
200                            GpxData.ensureUniqueName(trkAttr, names);
201                        }
202                        List<Node> ln = flat.get(i).getWay().getNodes();
203                        if (wct.get(i).direction == WayConnectionType.Direction.BACKWARD)
204                            Collections.reverse(ln);
205                        for (Node n: ln) {
206                            trkseg.add(OsmDataLayer.nodeToWayPoint(n, time));
207                            time += 1000;
208                        }
209                    }
210                }
211            }
212            gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
213
214            String lprefix = relations.iterator().next().getName();
215            if (lprefix == null || relations.size() > 1)
216                lprefix = tr("Selected Relations");
217            layerName = lprefix + layerName;
218        }
219
220        return new GpxLayer(gpxData, layerName, true);
221    }
222
223    /**
224     *
225     * @param e the ActionEvent
226     */
227    @Override
228    public void actionPerformed(ActionEvent e) {
229        if (mode.contains(TO_LAYER))
230            MainApplication.getLayerManager().addLayer(getLayer());
231        if (mode.contains(TO_FILE))
232            super.actionPerformed(e);
233    }
234
235    @Override
236    public void setPrimitives(Collection<? extends OsmPrimitive> primitives) {
237        relations = Collections.<Relation>emptySet();
238        if (primitives != null && !primitives.isEmpty()) {
239            relations = new SubclassFilteredCollection<>(primitives,
240                r -> r instanceof Relation && r.hasTag("type", Arrays.asList("route", "superroute")));
241        }
242        updateEnabledState();
243    }
244
245    @Override
246    protected void updateEnabledState() {
247        setEnabled(!relations.isEmpty());
248    }
249}