001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.BufferedWriter;
007import java.io.OutputStream;
008import java.io.OutputStreamWriter;
009import java.io.PrintWriter;
010import java.nio.charset.StandardCharsets;
011import java.time.Instant;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.Date;
015import java.util.List;
016import java.util.Map;
017import java.util.Objects;
018import java.util.stream.Collectors;
019
020import javax.xml.XMLConstants;
021
022import org.openstreetmap.josm.data.Bounds;
023import org.openstreetmap.josm.data.coor.LatLon;
024import org.openstreetmap.josm.data.gpx.GpxConstants;
025import org.openstreetmap.josm.data.gpx.GpxData;
026import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
027import org.openstreetmap.josm.data.gpx.GpxExtension;
028import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
029import org.openstreetmap.josm.data.gpx.GpxLink;
030import org.openstreetmap.josm.data.gpx.GpxRoute;
031import org.openstreetmap.josm.data.gpx.GpxTrack;
032import org.openstreetmap.josm.data.gpx.IGpxTrack;
033import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
034import org.openstreetmap.josm.data.gpx.IWithAttributes;
035import org.openstreetmap.josm.data.gpx.WayPoint;
036import org.openstreetmap.josm.tools.JosmRuntimeException;
037import org.openstreetmap.josm.tools.Logging;
038import org.openstreetmap.josm.tools.Utils;
039
040/**
041 * Writes GPX files from GPX data or OSM data.
042 */
043public class GpxWriter extends XmlWriter implements GpxConstants {
044
045    /**
046     * Constructs a new {@code GpxWriter}.
047     * @param out The output writer
048     */
049    public GpxWriter(PrintWriter out) {
050        super(out);
051    }
052
053    /**
054     * Constructs a new {@code GpxWriter}.
055     * @param out The output stream
056     */
057    public GpxWriter(OutputStream out) {
058        super(new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))));
059    }
060
061    private GpxData data;
062    private String indent = "";
063    private Instant metaTime;
064    private List<String> validprefixes;
065
066    private static final int WAY_POINT = 0;
067    private static final int ROUTE_POINT = 1;
068    private static final int TRACK_POINT = 2;
069
070    /**
071     * Returns the forced metadata time information, if any.
072     * @return the forced metadata time information, or {@code null}
073     * @since 18219
074     */
075    public Instant getMetaTime() {
076        return metaTime;
077    }
078
079    /**
080     * Sets the forced metadata time information.
081     * @param metaTime the forced metadata time information, or {@code null} to use the current time
082     * @since 18219
083     */
084    public void setMetaTime(Instant metaTime) {
085        this.metaTime = metaTime;
086    }
087
088    /**
089     * Writes the given GPX data.
090     * @param data The data to write
091     */
092    public void write(GpxData data) {
093        write(data, ColorFormat.GPXD, true);
094    }
095
096    /**
097     * Writes the given GPX data.
098     *
099     * @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());
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}