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