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.ArrayList; 012import java.util.Collection; 013import java.util.Date; 014import java.util.List; 015import java.util.Map; 016import java.util.Objects; 017import java.util.stream.Collectors; 018 019import javax.xml.XMLConstants; 020 021import org.openstreetmap.josm.data.Bounds; 022import org.openstreetmap.josm.data.coor.LatLon; 023import org.openstreetmap.josm.data.gpx.GpxConstants; 024import org.openstreetmap.josm.data.gpx.GpxData; 025import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace; 026import org.openstreetmap.josm.data.gpx.GpxExtension; 027import org.openstreetmap.josm.data.gpx.GpxExtensionCollection; 028import org.openstreetmap.josm.data.gpx.GpxLink; 029import org.openstreetmap.josm.data.gpx.GpxRoute; 030import org.openstreetmap.josm.data.gpx.GpxTrack; 031import org.openstreetmap.josm.data.gpx.IGpxTrack; 032import org.openstreetmap.josm.data.gpx.IGpxTrackSegment; 033import org.openstreetmap.josm.data.gpx.IWithAttributes; 034import org.openstreetmap.josm.data.gpx.WayPoint; 035import org.openstreetmap.josm.tools.JosmRuntimeException; 036import org.openstreetmap.josm.tools.Logging; 037import org.openstreetmap.josm.tools.date.DateUtils; 038 039/** 040 * Writes GPX files from GPX data or OSM data. 041 */ 042public class GpxWriter extends XmlWriter implements GpxConstants { 043 044 /** 045 * Constructs a new {@code GpxWriter}. 046 * @param out The output writer 047 */ 048 public GpxWriter(PrintWriter out) { 049 super(out); 050 } 051 052 /** 053 * Constructs a new {@code GpxWriter}. 054 * @param out The output stream 055 */ 056 public GpxWriter(OutputStream out) { 057 super(new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)))); 058 } 059 060 private GpxData data; 061 private String indent = ""; 062 private List<String> validprefixes; 063 064 private static final int WAY_POINT = 0; 065 private static final int ROUTE_POINT = 1; 066 private static final int TRACK_POINT = 2; 067 068 /** 069 * Writes the given GPX data. 070 * @param data The data to write 071 */ 072 public void write(GpxData data) { 073 write(data, ColorFormat.GPXD, true); 074 } 075 076 /** 077 * Writes the given GPX data. 078 * 079 * @param data The data to write 080 * @param colorFormat determines if colors are saved and which extension is to be used 081 * @param savePrefs whether layer specific preferences are saved 082 */ 083 public void write(GpxData data, ColorFormat colorFormat, boolean savePrefs) { 084 this.data = data; 085 086 //Prepare extensions for writing 087 data.beginUpdate(); 088 data.getTracks().stream() 089 .filter(GpxTrack.class::isInstance).map(GpxTrack.class::cast) 090 .forEach(trk -> trk.convertColor(colorFormat)); 091 data.getExtensions().removeAllWithPrefix("josm"); 092 if (data.fromServer) { 093 data.getExtensions().add("josm", "from-server", "true"); 094 } 095 if (savePrefs && !data.getLayerPrefs().isEmpty()) { 096 GpxExtensionCollection layerExts = data.getExtensions().add("josm", "layerPreferences").getExtensions(); 097 data.getLayerPrefs().entrySet() 098 .stream() 099 .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) 100 .forEach(entry -> { 101 GpxExtension e = layerExts.add("josm", "entry"); 102 e.put("key", entry.getKey()); 103 e.put("value", entry.getValue()); 104 }); 105 } 106 data.endUpdate(); 107 108 Collection<IWithAttributes> all = new ArrayList<>(); 109 110 all.add(data); 111 all.addAll(data.getWaypoints()); 112 all.addAll(data.getRoutes()); 113 all.addAll(data.getTracks()); 114 all.addAll(data.getTrackSegmentsStream().collect(Collectors.toList())); 115 116 List<XMLNamespace> namespaces = all 117 .stream() 118 .flatMap(w -> w.getExtensions().getPrefixesStream()) 119 .distinct() 120 .map(p -> data.getNamespaces() 121 .stream() 122 .filter(s -> s.getPrefix().equals(p)) 123 .findAny() 124 .orElse(GpxExtension.findNamespace(p))) 125 .filter(Objects::nonNull) 126 .collect(Collectors.toList()); 127 128 validprefixes = namespaces.stream().map(n -> n.getPrefix()).collect(Collectors.toList()); 129 130 out.println("<?xml version='1.0' encoding='UTF-8'?>"); 131 out.println("<gpx version=\"1.1\" creator=\"JOSM GPX export\" xmlns=\"http://www.topografix.com/GPX/1/1\""); 132 133 StringBuilder schemaLocations = new StringBuilder("http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"); 134 135 for (XMLNamespace n : namespaces) { 136 if (n.getURI() != null && n.getPrefix() != null && !n.getPrefix().isEmpty()) { 137 out.println(String.format(" xmlns:%s=\"%s\"", n.getPrefix(), n.getURI())); 138 if (n.getLocation() != null) { 139 schemaLocations.append(' ').append(n.getURI()).append(' ').append(n.getLocation()); 140 } 141 } 142 } 143 144 out.println(" xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\""); 145 out.println(String.format(" xsi:schemaLocation=\"%s\">", schemaLocations)); 146 indent = " "; 147 writeMetaData(); 148 writeWayPoints(); 149 writeRoutes(); 150 writeTracks(); 151 out.print("</gpx>"); 152 out.flush(); 153 } 154 155 private void writeAttr(IWithAttributes obj, List<String> keys) { 156 for (String key : keys) { 157 if (META_LINKS.equals(key)) { 158 Collection<GpxLink> lValue = obj.<GpxLink>getCollection(key); 159 if (lValue != null) { 160 for (GpxLink link : lValue) { 161 gpxLink(link); 162 } 163 } 164 } else { 165 String value = obj.getString(key); 166 if (value != null) { 167 simpleTag(key, value); 168 } else { 169 Object val = obj.get(key); 170 if (val instanceof Date) { 171 simpleTag(key, DateUtils.fromDate((Date) val)); 172 } else if (val instanceof Number) { 173 simpleTag(key, val.toString()); 174 } else if (val != null) { 175 Logging.warn("GPX attribute '"+key+"' not managed: " + val); 176 } 177 } 178 } 179 } 180 } 181 182 private void writeMetaData() { 183 Map<String, Object> attr = data.attr; 184 openln("metadata"); 185 186 // write the description 187 if (attr.containsKey(META_DESC)) { 188 simpleTag("desc", data.getString(META_DESC)); 189 } 190 191 // write the author details 192 if (attr.containsKey(META_AUTHOR_NAME) 193 || attr.containsKey(META_AUTHOR_EMAIL)) { 194 openln("author"); 195 // write the name 196 simpleTag("name", data.getString(META_AUTHOR_NAME)); 197 // write the email address 198 if (attr.containsKey(META_AUTHOR_EMAIL)) { 199 String[] tmp = data.getString(META_AUTHOR_EMAIL).split("@"); 200 if (tmp.length == 2) { 201 inline("email", "id=\"" + encode(tmp[0]) + "\" domain=\"" + encode(tmp[1]) +'\"'); 202 } 203 } 204 // write the author link 205 gpxLink((GpxLink) data.get(META_AUTHOR_LINK)); 206 closeln("author"); 207 } 208 209 // write the copyright details 210 if (attr.containsKey(META_COPYRIGHT_LICENSE) 211 || attr.containsKey(META_COPYRIGHT_YEAR)) { 212 openln("copyright", "author=\""+ encode(data.get(META_COPYRIGHT_AUTHOR).toString()) +'\"'); 213 if (attr.containsKey(META_COPYRIGHT_YEAR)) { 214 simpleTag("year", (String) data.get(META_COPYRIGHT_YEAR)); 215 } 216 if (attr.containsKey(META_COPYRIGHT_LICENSE)) { 217 simpleTag("license", encode((String) data.get(META_COPYRIGHT_LICENSE))); 218 } 219 closeln("copyright"); 220 } 221 222 // write links 223 if (attr.containsKey(META_LINKS)) { 224 for (GpxLink link : data.<GpxLink>getCollection(META_LINKS)) { 225 gpxLink(link); 226 } 227 } 228 229 // write keywords 230 if (attr.containsKey(META_KEYWORDS)) { 231 simpleTag("keywords", data.getString(META_KEYWORDS)); 232 } 233 234 Bounds bounds = data.recalculateBounds(); 235 if (bounds != null) { 236 String b = "minlat=\"" + bounds.getMinLat() + "\" minlon=\"" + bounds.getMinLon() + 237 "\" maxlat=\"" + bounds.getMaxLat() + "\" maxlon=\"" + bounds.getMaxLon() + '\"'; 238 inline("bounds", b); 239 } 240 241 gpxExtensions(data.getExtensions()); 242 closeln("metadata"); 243 } 244 245 private void writeWayPoints() { 246 for (WayPoint pnt : data.getWaypoints()) { 247 wayPoint(pnt, WAY_POINT); 248 } 249 } 250 251 private void writeRoutes() { 252 for (GpxRoute rte : data.getRoutes()) { 253 openln("rte"); 254 writeAttr(rte, RTE_TRK_KEYS); 255 gpxExtensions(rte.getExtensions()); 256 for (WayPoint pnt : rte.routePoints) { 257 wayPoint(pnt, ROUTE_POINT); 258 } 259 closeln("rte"); 260 } 261 } 262 263 private void writeTracks() { 264 for (IGpxTrack trk : data.getTracks()) { 265 openln("trk"); 266 writeAttr(trk, RTE_TRK_KEYS); 267 gpxExtensions(trk.getExtensions()); 268 for (IGpxTrackSegment seg : trk.getSegments()) { 269 openln("trkseg"); 270 gpxExtensions(seg.getExtensions()); 271 for (WayPoint pnt : seg.getWayPoints()) { 272 wayPoint(pnt, TRACK_POINT); 273 } 274 closeln("trkseg"); 275 } 276 closeln("trk"); 277 } 278 } 279 280 private void openln(String tag) { 281 open(tag); 282 out.println(); 283 } 284 285 private void openln(String tag, String attributes) { 286 open(tag, attributes); 287 out.println(); 288 } 289 290 private void open(String tag) { 291 out.print(indent + '<' + tag + '>'); 292 indent += " "; 293 } 294 295 private void open(String tag, String attributes) { 296 out.print(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + '>'); 297 indent += " "; 298 } 299 300 private void inline(String tag, String attributes) { 301 out.println(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + "/>"); 302 } 303 304 private void close(String tag) { 305 indent = indent.substring(2); 306 out.print(indent + "</" + tag + '>'); 307 } 308 309 private void closeln(String tag) { 310 close(tag); 311 out.println(); 312 } 313 314 /** 315 * if content not null, open tag, write encoded content, and close tag 316 * else do nothing. 317 * @param tag GPX tag 318 * @param content content 319 */ 320 private void simpleTag(String tag, String content) { 321 if (content != null && !content.isEmpty()) { 322 open(tag); 323 out.print(encode(content)); 324 out.println("</" + tag + '>'); 325 indent = indent.substring(2); 326 } 327 } 328 329 private void simpleTag(String tag, String content, String attributes) { 330 if (content != null && !content.isEmpty()) { 331 open(tag, attributes); 332 out.print(encode(content)); 333 out.println("</" + tag + '>'); 334 indent = indent.substring(2); 335 } 336 } 337 338 /** 339 * output link 340 * @param link link 341 */ 342 private void gpxLink(GpxLink link) { 343 if (link != null) { 344 openln("link", "href=\"" + encode(link.uri) + '\"'); 345 simpleTag("text", link.text); 346 simpleTag("type", link.type); 347 closeln("link"); 348 } 349 } 350 351 /** 352 * output a point 353 * @param pnt waypoint 354 * @param mode {@code WAY_POINT} for {@code wpt}, {@code ROUTE_POINT} for {@code rtept}, {@code TRACK_POINT} for {@code trkpt} 355 */ 356 private void wayPoint(WayPoint pnt, int mode) { 357 String type; 358 switch(mode) { 359 case WAY_POINT: 360 type = "wpt"; 361 break; 362 case ROUTE_POINT: 363 type = "rtept"; 364 break; 365 case TRACK_POINT: 366 type = "trkpt"; 367 break; 368 default: 369 throw new JosmRuntimeException(tr("Unknown mode {0}.", mode)); 370 } 371 if (pnt != null) { 372 LatLon c = pnt.getCoor(); 373 String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"'; 374 if (pnt.attr.isEmpty() && pnt.getExtensions().isEmpty()) { 375 inline(type, coordAttr); 376 } else { 377 openln(type, coordAttr); 378 writeAttr(pnt, WPT_KEYS); 379 gpxExtensions(pnt.getExtensions()); 380 closeln(type); 381 } 382 } 383 } 384 385 private void gpxExtensions(GpxExtensionCollection allExtensions) { 386 if (allExtensions.isVisible()) { 387 openln("extensions"); 388 writeExtension(allExtensions); 389 closeln("extensions"); 390 } 391 } 392 393 private void writeExtension(List<GpxExtension> extensions) { 394 for (GpxExtension e : extensions) { 395 if (validprefixes.contains(e.getPrefix()) && e.isVisible()) { 396 // this might lead to loss of an unknown extension *after* the file was saved as .osm, 397 // but otherwise the file is invalid and can't even be parsed by SAX anymore 398 String k = (e.getPrefix().isEmpty() ? "" : e.getPrefix() + ":") + e.getKey(); 399 String attr = e.getAttributes().entrySet().stream() 400 .map(a -> encode(a.getKey()) + "=\"" + encode(a.getValue().toString()) + "\"") 401 .sorted() 402 .collect(Collectors.joining(" ")); 403 if (e.getValue() == null && e.getExtensions().isEmpty()) { 404 inline(k, attr); 405 } else if (e.getExtensions().isEmpty()) { 406 simpleTag(k, e.getValue(), attr); 407 } else { 408 openln(k, attr); 409 if (e.getValue() != null) { 410 out.print(encode(e.getValue())); 411 } 412 writeExtension(e.getExtensions()); 413 closeln(k); 414 } 415 } 416 } 417 } 418}