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.Set; 022import java.util.Stack; 023 024import org.openstreetmap.josm.actions.GpxExportAction; 025import org.openstreetmap.josm.actions.IPrimitiveAction; 026import org.openstreetmap.josm.data.gpx.GpxData; 027import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 028import org.openstreetmap.josm.data.gpx.WayPoint; 029import org.openstreetmap.josm.data.osm.IPrimitive; 030import org.openstreetmap.josm.data.osm.Node; 031import org.openstreetmap.josm.data.osm.Relation; 032import org.openstreetmap.josm.data.osm.RelationMember; 033import org.openstreetmap.josm.data.osm.Way; 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 putValue("help", 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 private static final class BidiIterableList { 113 private final List<RelationMember> l; 114 115 private BidiIterableList(List<RelationMember> l) { 116 this.l = l; 117 } 118 119 public Iterator<RelationMember> iterator() { 120 return l.iterator(); 121 } 122 123 public Iterator<RelationMember> reverseIterator() { 124 ListIterator<RelationMember> li = l.listIterator(l.size()); 125 return new Iterator<RelationMember>() { 126 @Override 127 public boolean hasNext() { 128 return li.hasPrevious(); 129 } 130 131 @Override 132 public RelationMember next() { 133 return li.previous(); 134 } 135 136 @Override 137 public void remove() { 138 li.remove(); 139 } 140 }; 141 } 142 } 143 144 @Override 145 protected Layer getLayer() { 146 List<RelationMember> flat = new ArrayList<>(); 147 148 List<RelationMember> init = new ArrayList<>(); 149 relations.forEach(t -> init.add(new RelationMember("", t))); 150 BidiIterableList l = new BidiIterableList(init); 151 152 Stack<Iterator<RelationMember>> stack = new Stack<>(); 153 stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator()); 154 155 List<Relation> relsFound = new ArrayList<>(); 156 do { 157 Iterator<RelationMember> i = stack.peek(); 158 if (!i.hasNext()) 159 stack.pop(); 160 while (i.hasNext()) { 161 RelationMember m = i.next(); 162 if (m.isRelation() && !m.getRelation().isIncomplete()) { 163 l = new BidiIterableList(m.getRelation().getMembers()); 164 stack.push(mode.contains(FROM_FIRST_MEMBER) ? l.iterator() : l.reverseIterator()); 165 relsFound.add(m.getRelation()); 166 break; 167 } 168 if (m.isWay()) { 169 flat.add(m); 170 } 171 } 172 } while (!stack.isEmpty()); 173 174 GpxData gpxData = new GpxData(); 175 String layerName = " (GPX export)"; 176 long time = System.currentTimeMillis()-24*3600*1000; 177 178 if (!flat.isEmpty()) { 179 Map<String, Object> trkAttr = new HashMap<>(); 180 Collection<Collection<WayPoint>> trk = new ArrayList<>(); 181 List<WayPoint> trkseg = new ArrayList<>(); 182 trk.add(trkseg); 183 184 List<WayConnectionType> wct = new WayConnectionTypeCalculator().updateLinks(flat); 185 final HashMap<String, Integer> names = new HashMap<>(); 186 for (int i = 0; i < flat.size(); i++) { 187 if (!wct.get(i).isOnewayLoopBackwardPart) { 188 if (!wct.get(i).direction.isRoundabout()) { 189 if (!wct.get(i).linkPrev && !trkseg.isEmpty()) { 190 gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr)); 191 trkAttr.clear(); 192 trk.clear(); 193 trkseg.clear(); 194 trk.add(trkseg); 195 } 196 if (trkAttr.isEmpty()) { 197 Relation r = Way.getParentRelations(Arrays.asList(flat.get(i).getWay())) 198 .stream().filter(relsFound::contains).findFirst().orElseGet(null); 199 if (r != null) 200 trkAttr.put("name", r.getName() != null ? r.getName() : r.getId()); 201 GpxData.ensureUniqueName(trkAttr, names); 202 } 203 List<Node> ln = flat.get(i).getWay().getNodes(); 204 if (wct.get(i).direction == WayConnectionType.Direction.BACKWARD) 205 Collections.reverse(ln); 206 for (Node n: ln) { 207 trkseg.add(OsmDataLayer.nodeToWayPoint(n, time)); 208 time += 1000; 209 } 210 } 211 } 212 } 213 gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr)); 214 215 String lprefix = relations.iterator().next().getName(); 216 if (lprefix == null || relations.size() > 1) 217 lprefix = tr("Selected Relations"); 218 layerName = lprefix + layerName; 219 } 220 221 return new GpxLayer(gpxData, layerName, true); 222 } 223 224 /** 225 * 226 * @param e the ActionEvent 227 */ 228 @Override 229 public void actionPerformed(ActionEvent e) { 230 if (mode.contains(TO_LAYER)) 231 MainApplication.getLayerManager().addLayer(getLayer()); 232 if (mode.contains(TO_FILE)) 233 super.actionPerformed(e); 234 } 235 236 @Override 237 public void setPrimitives(Collection<? extends IPrimitive> primitives) { 238 relations = Collections.<Relation>emptySet(); 239 if (primitives != null && !primitives.isEmpty()) { 240 relations = new SubclassFilteredCollection<>(primitives, 241 r -> r instanceof Relation && r.hasTag("type", Arrays.asList("route", "superroute"))); 242 } 243 updateEnabledState(); 244 } 245 246 @Override 247 protected void updateEnabledState() { 248 setEnabled(!relations.isEmpty()); 249 } 250}