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}