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