source: josm/trunk/src/org/openstreetmap/josm/io/GpxWriter.java@ 18207

Last change on this file since 18207 was 18207, checked in by Don-vip, 3 years ago

fix #21257 - sort tracks chronologically when writing GPX file

  • Property svn:eol-style set to native
File size: 14.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedWriter;
7import java.io.OutputStream;
8import java.io.OutputStreamWriter;
9import java.io.PrintWriter;
10import java.nio.charset.StandardCharsets;
11import java.time.Instant;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.Date;
15import java.util.List;
16import java.util.Map;
17import java.util.Objects;
18import java.util.stream.Collectors;
19
20import javax.xml.XMLConstants;
21
22import org.openstreetmap.josm.data.Bounds;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.gpx.GpxConstants;
25import org.openstreetmap.josm.data.gpx.GpxData;
26import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
27import org.openstreetmap.josm.data.gpx.GpxExtension;
28import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
29import org.openstreetmap.josm.data.gpx.GpxLink;
30import org.openstreetmap.josm.data.gpx.GpxRoute;
31import org.openstreetmap.josm.data.gpx.GpxTrack;
32import org.openstreetmap.josm.data.gpx.IGpxTrack;
33import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
34import org.openstreetmap.josm.data.gpx.IWithAttributes;
35import org.openstreetmap.josm.data.gpx.WayPoint;
36import org.openstreetmap.josm.tools.JosmRuntimeException;
37import org.openstreetmap.josm.tools.Logging;
38
39/**
40 * Writes GPX files from GPX data or OSM data.
41 */
42public class GpxWriter extends XmlWriter implements GpxConstants {
43
44 /**
45 * Constructs a new {@code GpxWriter}.
46 * @param out The output writer
47 */
48 public GpxWriter(PrintWriter out) {
49 super(out);
50 }
51
52 /**
53 * Constructs a new {@code GpxWriter}.
54 * @param out The output stream
55 */
56 public GpxWriter(OutputStream out) {
57 super(new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))));
58 }
59
60 private GpxData data;
61 private String indent = "";
62 private List<String> validprefixes;
63
64 private static final int WAY_POINT = 0;
65 private static final int ROUTE_POINT = 1;
66 private static final int TRACK_POINT = 2;
67
68 /**
69 * Writes the given GPX data.
70 * @param data The data to write
71 */
72 public void write(GpxData data) {
73 write(data, ColorFormat.GPXD, true);
74 }
75
76 /**
77 * Writes the given GPX data.
78 *
79 * @param data The data to write
80 * @param colorFormat determines if colors are saved and which extension is to be used
81 * @param savePrefs whether layer specific preferences are saved
82 */
83 public void write(GpxData data, ColorFormat colorFormat, boolean savePrefs) {
84 this.data = data;
85
86 //Prepare extensions for writing
87 data.beginUpdate();
88 data.getTracks().stream()
89 .filter(GpxTrack.class::isInstance).map(GpxTrack.class::cast)
90 .forEach(trk -> trk.convertColor(colorFormat));
91 data.getExtensions().removeAllWithPrefix("josm");
92 if (data.fromServer) {
93 data.getExtensions().add("josm", "from-server", "true");
94 }
95 if (savePrefs && !data.getLayerPrefs().isEmpty()) {
96 GpxExtensionCollection layerExts = data.getExtensions().add("josm", "layerPreferences").getExtensions();
97 data.getLayerPrefs().entrySet()
98 .stream()
99 .sorted(Map.Entry.comparingByKey())
100 .forEach(entry -> {
101 GpxExtension e = layerExts.add("josm", "entry");
102 e.put("key", entry.getKey());
103 e.put("value", entry.getValue());
104 });
105 }
106 data.endUpdate();
107
108 Collection<IWithAttributes> all = new ArrayList<>();
109
110 all.add(data);
111 all.addAll(data.getWaypoints());
112 all.addAll(data.getRoutes());
113 all.addAll(data.getTracks());
114 all.addAll(data.getTrackSegmentsStream().collect(Collectors.toList()));
115
116 List<XMLNamespace> namespaces = all
117 .stream()
118 .flatMap(w -> w.getExtensions().getPrefixesStream())
119 .distinct()
120 .map(p -> data.getNamespaces()
121 .stream()
122 .filter(s -> s.getPrefix().equals(p))
123 .findAny()
124 .orElse(GpxExtension.findNamespace(p)))
125 .filter(Objects::nonNull)
126 .collect(Collectors.toList());
127
128 validprefixes = namespaces.stream().map(n -> n.getPrefix()).collect(Collectors.toList());
129
130 out.println("<?xml version='1.0' encoding='UTF-8'?>");
131 out.println("<gpx version=\"1.1\" creator=\"JOSM GPX export\" xmlns=\"http://www.topografix.com/GPX/1/1\"");
132
133 StringBuilder schemaLocations = new StringBuilder("http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
134
135 for (XMLNamespace n : namespaces) {
136 if (n.getURI() != null && n.getPrefix() != null && !n.getPrefix().isEmpty()) {
137 out.println(String.format(" xmlns:%s=\"%s\"", n.getPrefix(), n.getURI()));
138 if (n.getLocation() != null) {
139 schemaLocations.append(' ').append(n.getURI()).append(' ').append(n.getLocation());
140 }
141 }
142 }
143
144 out.println(" xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
145 out.println(String.format(" xsi:schemaLocation=\"%s\">", schemaLocations));
146 indent = " ";
147 writeMetaData();
148 writeWayPoints();
149 writeRoutes();
150 writeTracks();
151 out.print("</gpx>");
152 out.flush();
153 }
154
155 private void writeAttr(IWithAttributes obj, List<String> keys) {
156 for (String key : keys) {
157 if (META_LINKS.equals(key)) {
158 Collection<GpxLink> lValue = obj.<GpxLink>getCollection(key);
159 if (lValue != null) {
160 for (GpxLink link : lValue) {
161 gpxLink(link);
162 }
163 }
164 } else {
165 String value = obj.getString(key);
166 if (value != null) {
167 simpleTag(key, value);
168 } else {
169 Object val = obj.get(key);
170 if (val instanceof Date) {
171 throw new IllegalStateException();
172 } else if (val instanceof Instant) {
173 simpleTag(key, String.valueOf(val));
174 } else if (val instanceof Number) {
175 simpleTag(key, val.toString());
176 } else if (val != null) {
177 Logging.warn("GPX attribute '"+key+"' not managed: " + val);
178 }
179 }
180 }
181 }
182 }
183
184 private void writeMetaData() {
185 Map<String, Object> attr = data.attr;
186 openln("metadata");
187
188 // write the description
189 if (attr.containsKey(META_DESC)) {
190 simpleTag("desc", data.getString(META_DESC));
191 }
192
193 // write the author details
194 if (attr.containsKey(META_AUTHOR_NAME)
195 || attr.containsKey(META_AUTHOR_EMAIL)) {
196 openln("author");
197 // write the name
198 simpleTag("name", data.getString(META_AUTHOR_NAME));
199 // write the email address
200 if (attr.containsKey(META_AUTHOR_EMAIL)) {
201 String[] tmp = data.getString(META_AUTHOR_EMAIL).split("@", -1);
202 if (tmp.length == 2) {
203 inline("email", "id=\"" + encode(tmp[0]) + "\" domain=\"" + encode(tmp[1]) +'\"');
204 }
205 }
206 // write the author link
207 gpxLink((GpxLink) data.get(META_AUTHOR_LINK));
208 closeln("author");
209 }
210
211 // write the copyright details
212 if (attr.containsKey(META_COPYRIGHT_LICENSE)
213 || attr.containsKey(META_COPYRIGHT_YEAR)) {
214 openln("copyright", "author=\""+ encode(data.get(META_COPYRIGHT_AUTHOR).toString()) +'\"');
215 if (attr.containsKey(META_COPYRIGHT_YEAR)) {
216 simpleTag("year", (String) data.get(META_COPYRIGHT_YEAR));
217 }
218 if (attr.containsKey(META_COPYRIGHT_LICENSE)) {
219 simpleTag("license", encode((String) data.get(META_COPYRIGHT_LICENSE)));
220 }
221 closeln("copyright");
222 }
223
224 // write links
225 if (attr.containsKey(META_LINKS)) {
226 for (GpxLink link : data.<GpxLink>getCollection(META_LINKS)) {
227 gpxLink(link);
228 }
229 }
230
231 // write keywords
232 if (attr.containsKey(META_KEYWORDS)) {
233 simpleTag("keywords", data.getString(META_KEYWORDS));
234 }
235
236 simpleTag("time", Instant.now().toString());
237
238 Bounds bounds = data.recalculateBounds();
239 if (bounds != null) {
240 String b = "minlat=\"" + bounds.getMinLat() + "\" minlon=\"" + bounds.getMinLon() +
241 "\" maxlat=\"" + bounds.getMaxLat() + "\" maxlon=\"" + bounds.getMaxLon() + '\"';
242 inline("bounds", b);
243 }
244
245 gpxExtensions(data.getExtensions());
246 closeln("metadata");
247 }
248
249 private void writeWayPoints() {
250 for (WayPoint pnt : data.getWaypoints()) {
251 wayPoint(pnt, WAY_POINT);
252 }
253 }
254
255 private void writeRoutes() {
256 for (GpxRoute rte : data.getRoutes()) {
257 openln("rte");
258 writeAttr(rte, RTE_TRK_KEYS);
259 gpxExtensions(rte.getExtensions());
260 for (WayPoint pnt : rte.routePoints) {
261 wayPoint(pnt, ROUTE_POINT);
262 }
263 closeln("rte");
264 }
265 }
266
267 private void writeTracks() {
268 for (IGpxTrack trk : data.getOrderedTracks()) {
269 openln("trk");
270 writeAttr(trk, RTE_TRK_KEYS);
271 gpxExtensions(trk.getExtensions());
272 for (IGpxTrackSegment seg : trk.getSegments()) {
273 openln("trkseg");
274 gpxExtensions(seg.getExtensions());
275 for (WayPoint pnt : seg.getWayPoints()) {
276 wayPoint(pnt, TRACK_POINT);
277 }
278 closeln("trkseg");
279 }
280 closeln("trk");
281 }
282 }
283
284 private void openln(String tag) {
285 open(tag);
286 out.println();
287 }
288
289 private void openln(String tag, String attributes) {
290 open(tag, attributes);
291 out.println();
292 }
293
294 private void open(String tag) {
295 out.print(indent + '<' + tag + '>');
296 indent += " ";
297 }
298
299 private void open(String tag, String attributes) {
300 out.print(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + '>');
301 indent += " ";
302 }
303
304 private void inline(String tag, String attributes) {
305 out.println(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + "/>");
306 }
307
308 private void close(String tag) {
309 indent = indent.substring(2);
310 out.print(indent + "</" + tag + '>');
311 }
312
313 private void closeln(String tag) {
314 close(tag);
315 out.println();
316 }
317
318 /**
319 * if content not null, open tag, write encoded content, and close tag
320 * else do nothing.
321 * @param tag GPX tag
322 * @param content content
323 */
324 private void simpleTag(String tag, String content) {
325 if (content != null && !content.isEmpty()) {
326 open(tag);
327 out.print(encode(content));
328 out.println("</" + tag + '>');
329 indent = indent.substring(2);
330 }
331 }
332
333 private void simpleTag(String tag, String content, String attributes) {
334 if (content != null && !content.isEmpty()) {
335 open(tag, attributes);
336 out.print(encode(content));
337 out.println("</" + tag + '>');
338 indent = indent.substring(2);
339 }
340 }
341
342 /**
343 * output link
344 * @param link link
345 */
346 private void gpxLink(GpxLink link) {
347 if (link != null) {
348 openln("link", "href=\"" + encode(link.uri) + '\"');
349 simpleTag("text", link.text);
350 simpleTag("type", link.type);
351 closeln("link");
352 }
353 }
354
355 /**
356 * output a point
357 * @param pnt waypoint
358 * @param mode {@code WAY_POINT} for {@code wpt}, {@code ROUTE_POINT} for {@code rtept}, {@code TRACK_POINT} for {@code trkpt}
359 */
360 private void wayPoint(WayPoint pnt, int mode) {
361 String type;
362 switch(mode) {
363 case WAY_POINT:
364 type = "wpt";
365 break;
366 case ROUTE_POINT:
367 type = "rtept";
368 break;
369 case TRACK_POINT:
370 type = "trkpt";
371 break;
372 default:
373 throw new JosmRuntimeException(tr("Unknown mode {0}.", mode));
374 }
375 if (pnt != null) {
376 LatLon c = pnt.getCoor();
377 String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"';
378 if (pnt.attr.isEmpty() && pnt.getExtensions().isEmpty()) {
379 inline(type, coordAttr);
380 } else {
381 openln(type, coordAttr);
382 writeAttr(pnt, WPT_KEYS);
383 gpxExtensions(pnt.getExtensions());
384 closeln(type);
385 }
386 }
387 }
388
389 private void gpxExtensions(GpxExtensionCollection allExtensions) {
390 if (allExtensions.isVisible()) {
391 openln("extensions");
392 writeExtension(allExtensions);
393 closeln("extensions");
394 }
395 }
396
397 private void writeExtension(List<GpxExtension> extensions) {
398 for (GpxExtension e : extensions) {
399 if (validprefixes.contains(e.getPrefix()) && e.isVisible()) {
400 // this might lead to loss of an unknown extension *after* the file was saved as .osm,
401 // but otherwise the file is invalid and can't even be parsed by SAX anymore
402 String k = (e.getPrefix().isEmpty() ? "" : e.getPrefix() + ":") + e.getKey();
403 String attr = e.getAttributes().entrySet().stream()
404 .map(a -> encode(a.getKey()) + "=\"" + encode(a.getValue().toString()) + "\"")
405 .sorted()
406 .collect(Collectors.joining(" "));
407 if (e.getValue() == null && e.getExtensions().isEmpty()) {
408 inline(k, attr);
409 } else if (e.getExtensions().isEmpty()) {
410 simpleTag(k, e.getValue(), attr);
411 } else {
412 openln(k, attr);
413 if (e.getValue() != null) {
414 out.print(encode(e.getValue()));
415 }
416 writeExtension(e.getExtensions());
417 closeln(k);
418 }
419 }
420 }
421 }
422}
Note: See TracBrowser for help on using the repository browser.