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.waypoints) { 189 wayPoint(pnt, WAY_POINT); 190 } 191 } 192 193 private void writeRoutes() { 194 for (GpxRoute rte : data.routes) { 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.tracks) { 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}