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.util.Collection;
012import java.util.Date;
013import java.util.List;
014import java.util.Map;
015import java.util.Map.Entry;
016
017import javax.xml.XMLConstants;
018
019import org.openstreetmap.josm.data.Bounds;
020import org.openstreetmap.josm.data.coor.LatLon;
021import org.openstreetmap.josm.data.gpx.Extensions;
022import org.openstreetmap.josm.data.gpx.GpxConstants;
023import org.openstreetmap.josm.data.gpx.GpxData;
024import org.openstreetmap.josm.data.gpx.GpxLink;
025import org.openstreetmap.josm.data.gpx.GpxRoute;
026import org.openstreetmap.josm.data.gpx.GpxTrack;
027import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
028import org.openstreetmap.josm.data.gpx.IWithAttributes;
029import org.openstreetmap.josm.data.gpx.WayPoint;
030import org.openstreetmap.josm.tools.JosmRuntimeException;
031import org.openstreetmap.josm.tools.Logging;
032import org.openstreetmap.josm.tools.date.DateUtils;
033
034/**
035 * Writes GPX files from GPX data or OSM data.
036 */
037public class GpxWriter extends XmlWriter implements GpxConstants {
038
039    /**
040     * Constructs a new {@code GpxWriter}.
041     * @param out The output writer
042     */
043    public GpxWriter(PrintWriter out) {
044        super(out);
045    }
046
047    /**
048     * Constructs a new {@code GpxWriter}.
049     * @param out The output stream
050     */
051    public GpxWriter(OutputStream out) {
052        super(new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))));
053    }
054
055    private GpxData data;
056    private String indent = "";
057
058    private static final int WAY_POINT = 0;
059    private static final int ROUTE_POINT = 1;
060    private static final int TRACK_POINT = 2;
061
062    /**
063     * Writes the given GPX data.
064     * @param data The data to write
065     */
066    public void write(GpxData data) {
067        this.data = data;
068        // We write JOSM specific meta information into gpx 'extensions' elements.
069        // In particular it is noted whether the gpx data is from the OSM server
070        // (so the rendering of clouds of anonymous TrackPoints can be improved)
071        // and some extra synchronization info for export of AudioMarkers.
072        // It is checked in advance, if any extensions are used, so we know whether
073        // a namespace declaration is necessary.
074        boolean hasExtensions = data.fromServer;
075        if (!hasExtensions) {
076            for (WayPoint wpt : data.waypoints) {
077                Extensions extensions = (Extensions) wpt.get(META_EXTENSIONS);
078                if (extensions != null && !extensions.isEmpty()) {
079                    hasExtensions = true;
080                    break;
081                }
082            }
083        }
084
085        out.println("<?xml version='1.0' encoding='UTF-8'?>");
086        out.println("<gpx version=\"1.1\" creator=\"JOSM GPX export\" xmlns=\"http://www.topografix.com/GPX/1/1\"");
087        out.println((hasExtensions ? String.format("    xmlns:josm=\"%s\"%n", JOSM_EXTENSIONS_NAMESPACE_URI) : "") +
088                    "    xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
089        out.println("    xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">");
090        indent = "  ";
091        writeMetaData();
092        writeWayPoints();
093        writeRoutes();
094        writeTracks();
095        out.print("</gpx>");
096        out.flush();
097    }
098
099    private void writeAttr(IWithAttributes obj, List<String> keys) {
100        for (String key : keys) {
101            if (META_LINKS.equals(key)) {
102                Collection<GpxLink> lValue = obj.<GpxLink>getCollection(key);
103                if (lValue != null) {
104                    for (GpxLink link : lValue) {
105                        gpxLink(link);
106                    }
107                }
108            } else if (META_EXTENSIONS.equals(key)) {
109                Extensions extensions = (Extensions) obj.get(key);
110                if (extensions != null) {
111                    gpxExtensions(extensions);
112                }
113            } else {
114                String value = obj.getString(key);
115                if (value != null) {
116                    simpleTag(key, value);
117                } else {
118                    Object val = obj.get(key);
119                    if (val instanceof Date) {
120                        simpleTag(key, DateUtils.fromDate((Date) val));
121                    } else if (val instanceof Number) {
122                        simpleTag(key, val.toString());
123                    } else if (val != null) {
124                        Logging.warn("GPX attribute '"+key+"' not managed: " + val);
125                    }
126                }
127            }
128        }
129    }
130
131    private void writeMetaData() {
132        Map<String, Object> attr = data.attr;
133        openln("metadata");
134
135        // write the description
136        if (attr.containsKey(META_DESC)) {
137            simpleTag("desc", data.getString(META_DESC));
138        }
139
140        // write the author details
141        if (attr.containsKey(META_AUTHOR_NAME)
142                || attr.containsKey(META_AUTHOR_EMAIL)) {
143            openln("author");
144            // write the name
145            simpleTag("name", data.getString(META_AUTHOR_NAME));
146            // write the email address
147            if (attr.containsKey(META_AUTHOR_EMAIL)) {
148                String[] tmp = data.getString(META_AUTHOR_EMAIL).split("@");
149                if (tmp.length == 2) {
150                    inline("email", "id=\"" + tmp[0] + "\" domain=\""+tmp[1]+'\"');
151                }
152            }
153            // write the author link
154            gpxLink((GpxLink) data.get(META_AUTHOR_LINK));
155            closeln("author");
156        }
157
158        // write the copyright details
159        if (attr.containsKey(META_COPYRIGHT_LICENSE)
160                || attr.containsKey(META_COPYRIGHT_YEAR)) {
161            openAtt("copyright", "author=\""+ data.get(META_COPYRIGHT_AUTHOR) +'\"');
162            if (attr.containsKey(META_COPYRIGHT_YEAR)) {
163                simpleTag("year", (String) data.get(META_COPYRIGHT_YEAR));
164            }
165            if (attr.containsKey(META_COPYRIGHT_LICENSE)) {
166                simpleTag("license", encode((String) data.get(META_COPYRIGHT_LICENSE)));
167            }
168            closeln("copyright");
169        }
170
171        // write links
172        if (attr.containsKey(META_LINKS)) {
173            for (GpxLink link : data.<GpxLink>getCollection(META_LINKS)) {
174                gpxLink(link);
175            }
176        }
177
178        // write keywords
179        if (attr.containsKey(META_KEYWORDS)) {
180            simpleTag("keywords", data.getString(META_KEYWORDS));
181        }
182
183        Bounds bounds = data.recalculateBounds();
184        if (bounds != null) {
185            String b = "minlat=\"" + bounds.getMinLat() + "\" minlon=\"" + bounds.getMinLon() +
186            "\" maxlat=\"" + bounds.getMaxLat() + "\" maxlon=\"" + bounds.getMaxLon() + '\"';
187            inline("bounds", b);
188        }
189
190        if (data.fromServer) {
191            openln("extensions");
192            simpleTag("josm:from-server", "true");
193            closeln("extensions");
194        }
195
196        closeln("metadata");
197    }
198
199    private void writeWayPoints() {
200        for (WayPoint pnt : data.getWaypoints()) {
201            wayPoint(pnt, WAY_POINT);
202        }
203    }
204
205    private void writeRoutes() {
206        for (GpxRoute rte : data.getRoutes()) {
207            openln("rte");
208            writeAttr(rte, RTE_TRK_KEYS);
209            for (WayPoint pnt : rte.routePoints) {
210                wayPoint(pnt, ROUTE_POINT);
211            }
212            closeln("rte");
213        }
214    }
215
216    private void writeTracks() {
217        for (GpxTrack trk : data.getTracks()) {
218            openln("trk");
219            writeAttr(trk, RTE_TRK_KEYS);
220            for (GpxTrackSegment seg : trk.getSegments()) {
221                openln("trkseg");
222                for (WayPoint pnt : seg.getWayPoints()) {
223                    wayPoint(pnt, TRACK_POINT);
224                }
225                closeln("trkseg");
226            }
227            closeln("trk");
228        }
229    }
230
231    private void openln(String tag) {
232        open(tag);
233        out.println();
234    }
235
236    private void open(String tag) {
237        out.print(indent + '<' + tag + '>');
238        indent += "  ";
239    }
240
241    private void openAtt(String tag, String attributes) {
242        out.println(indent + '<' + tag + ' ' + attributes + '>');
243        indent += "  ";
244    }
245
246    private void inline(String tag, String attributes) {
247        out.println(indent + '<' + tag + ' ' + attributes + "/>");
248    }
249
250    private void close(String tag) {
251        indent = indent.substring(2);
252        out.print(indent + "</" + tag + '>');
253    }
254
255    private void closeln(String tag) {
256        close(tag);
257        out.println();
258    }
259
260    /**
261     * if content not null, open tag, write encoded content, and close tag
262     * else do nothing.
263     * @param tag GPX tag
264     * @param content content
265     */
266    private void simpleTag(String tag, String content) {
267        if (content != null && !content.isEmpty()) {
268            open(tag);
269            out.print(encode(content));
270            out.println("</" + tag + '>');
271            indent = indent.substring(2);
272        }
273    }
274
275    /**
276     * output link
277     * @param link link
278     */
279    private void gpxLink(GpxLink link) {
280        if (link != null) {
281            openAtt("link", "href=\"" + link.uri + '\"');
282            simpleTag("text", link.text);
283            simpleTag("type", link.type);
284            closeln("link");
285        }
286    }
287
288    /**
289     * output a point
290     * @param pnt waypoint
291     * @param mode {@code WAY_POINT} for {@code wpt}, {@code ROUTE_POINT} for {@code rtept}, {@code TRACK_POINT} for {@code trkpt}
292     */
293    private void wayPoint(WayPoint pnt, int mode) {
294        String type;
295        switch(mode) {
296        case WAY_POINT:
297            type = "wpt";
298            break;
299        case ROUTE_POINT:
300            type = "rtept";
301            break;
302        case TRACK_POINT:
303            type = "trkpt";
304            break;
305        default:
306            throw new JosmRuntimeException(tr("Unknown mode {0}.", mode));
307        }
308        if (pnt != null) {
309            LatLon c = pnt.getCoor();
310            String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"';
311            if (pnt.attr.isEmpty()) {
312                inline(type, coordAttr);
313            } else {
314                openAtt(type, coordAttr);
315                writeAttr(pnt, WPT_KEYS);
316                closeln(type);
317            }
318        }
319    }
320
321    private void gpxExtensions(Extensions extensions) {
322        if (extensions != null && !extensions.isEmpty()) {
323            openln("extensions");
324            for (Entry<String, String> e : extensions.entrySet()) {
325                simpleTag("josm:" + e.getKey(), e.getValue());
326            }
327            closeln("extensions");
328        }
329    }
330}