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

Last change on this file since 18399 was 18399, checked in by stoecker, 2 years ago

fix #21922 - patch by Bjoeni - GPX file marked as modified when saving session

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