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