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