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