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     */
251    private void simpleTag(String tag, String content) {
252        if (content != null && !content.isEmpty()) {
253            open(tag);
254            out.print(encode(content));
255            out.println("</" + tag + '>');
256            indent = indent.substring(2);
257        }
258    }
259
260    /**
261     * output link
262     */
263    private void gpxLink(GpxLink link) {
264        if (link != null) {
265            openAtt("link", "href=\"" + link.uri + '\"');
266            simpleTag("text", link.text);
267            simpleTag("type", link.type);
268            closeln("link");
269        }
270    }
271
272    /**
273     * output a point
274     */
275    private void wayPoint(WayPoint pnt, int mode) {
276        String type;
277        switch(mode) {
278        case WAY_POINT:
279            type = "wpt";
280            break;
281        case ROUTE_POINT:
282            type = "rtept";
283            break;
284        case TRACK_POINT:
285            type = "trkpt";
286            break;
287        default:
288            throw new RuntimeException(tr("Unknown mode {0}.", mode));
289        }
290        if (pnt != null) {
291            LatLon c = pnt.getCoor();
292            String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"';
293            if (pnt.attr.isEmpty()) {
294                inline(type, coordAttr);
295            } else {
296                openAtt(type, coordAttr);
297                writeAttr(pnt, WPT_KEYS);
298                closeln(type);
299            }
300        }
301    }
302
303    private void gpxExtensions(Extensions extensions) {
304        if (extensions != null && !extensions.isEmpty()) {
305            openln("extensions");
306            for (Entry<String, String> e : extensions.entrySet()) {
307                simpleTag("josm:" + e.getKey(), e.getValue());
308            }
309            closeln("extensions");
310        }
311    }
312}