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