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