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