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.PrintWriter; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.Comparator; 010import java.util.Date; 011import java.util.List; 012import java.util.Map.Entry; 013 014import org.openstreetmap.josm.data.DataSource; 015import org.openstreetmap.josm.data.coor.LatLon; 016import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat; 017import org.openstreetmap.josm.data.osm.AbstractPrimitive; 018import org.openstreetmap.josm.data.osm.Changeset; 019import org.openstreetmap.josm.data.osm.DataSet; 020import org.openstreetmap.josm.data.osm.DownloadPolicy; 021import org.openstreetmap.josm.data.osm.INode; 022import org.openstreetmap.josm.data.osm.IPrimitive; 023import org.openstreetmap.josm.data.osm.IRelation; 024import org.openstreetmap.josm.data.osm.IWay; 025import org.openstreetmap.josm.data.osm.Node; 026import org.openstreetmap.josm.data.osm.OsmPrimitive; 027import org.openstreetmap.josm.data.osm.Relation; 028import org.openstreetmap.josm.data.osm.Tagged; 029import org.openstreetmap.josm.data.osm.UploadPolicy; 030import org.openstreetmap.josm.data.osm.Way; 031import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 032import org.openstreetmap.josm.tools.date.DateUtils; 033 034/** 035 * Save the dataset into a stream as osm intern xml format. This is not using any xml library for storing. 036 * @author imi 037 * @since 59 038 */ 039public class OsmWriter extends XmlWriter implements PrimitiveVisitor { 040 041 /** Default OSM API version */ 042 public static final String DEFAULT_API_VERSION = "0.6"; 043 044 private final boolean osmConform; 045 private boolean withBody = true; 046 private boolean withVisible = true; 047 private boolean isOsmChange; 048 private String version; 049 private Changeset changeset; 050 051 /** 052 * Constructs a new {@code OsmWriter}. 053 * Do not call this directly. Use {@link OsmWriterFactory} instead. 054 * @param out print writer 055 * @param osmConform if {@code true}, prevents modification attributes to be written to the common part 056 * @param version OSM API version (0.6) 057 */ 058 protected OsmWriter(PrintWriter out, boolean osmConform, String version) { 059 super(out); 060 this.osmConform = osmConform; 061 this.version = version == null ? DEFAULT_API_VERSION : version; 062 } 063 064 /** 065 * Sets whether body must be written. 066 * @param wb if {@code true} body will be written. 067 */ 068 public void setWithBody(boolean wb) { 069 this.withBody = wb; 070 } 071 072 /** 073 * Sets whether 'visible' attribute must be written. 074 * @param wv if {@code true} 'visible' attribute will be written. 075 * @since 12019 076 */ 077 public void setWithVisible(boolean wv) { 078 this.withVisible = wv; 079 } 080 081 public void setIsOsmChange(boolean isOsmChange) { 082 this.isOsmChange = isOsmChange; 083 } 084 085 public void setChangeset(Changeset cs) { 086 this.changeset = cs; 087 } 088 089 public void setVersion(String v) { 090 this.version = v; 091 } 092 093 /** 094 * Writes OSM header with normal download and upload policies. 095 */ 096 public void header() { 097 header(DownloadPolicy.NORMAL, UploadPolicy.NORMAL); 098 } 099 100 /** 101 * Writes OSM header with normal download policy and given upload policy. 102 * @deprecated Use {@link #header(DownloadPolicy, UploadPolicy)} instead 103 * @param upload upload policy 104 */ 105 @Deprecated 106 public void header(UploadPolicy upload) { 107 header(DownloadPolicy.NORMAL, upload); 108 } 109 110 /** 111 * Writes OSM header with given download upload policies. 112 * @param download download policy 113 * @param upload upload policy 114 * @since 13485 115 */ 116 public void header(DownloadPolicy download, UploadPolicy upload) { 117 header(download, upload, false); 118 } 119 120 private void header(DownloadPolicy download, UploadPolicy upload, boolean locked) { 121 out.println("<?xml version='1.0' encoding='UTF-8'?>"); 122 out.print("<osm version='"); 123 out.print(version); 124 if (download != null && download != DownloadPolicy.NORMAL) { 125 out.print("' download='"); 126 out.print(download.getXmlFlag()); 127 } 128 if (upload != null && upload != UploadPolicy.NORMAL) { 129 out.print("' upload='"); 130 out.print(upload.getXmlFlag()); 131 } 132 if (locked) { 133 out.print("' locked=true"); 134 } 135 out.println("' generator='JOSM'>"); 136 } 137 138 /** 139 * Writes OSM footer. 140 */ 141 public void footer() { 142 out.println("</osm>"); 143 } 144 145 /** 146 * Sorts {@code -1} → {@code -infinity}, then {@code +1} → {@code +infinity} 147 */ 148 protected static final Comparator<AbstractPrimitive> byIdComparator = (o1, o2) -> { 149 final long i1 = o1.getUniqueId(); 150 final long i2 = o2.getUniqueId(); 151 if (i1 < 0 && i2 < 0) { 152 return Long.compare(i2, i1); 153 } else { 154 return Long.compare(i1, i2); 155 } 156 }; 157 158 protected <T extends OsmPrimitive> Collection<T> sortById(Collection<T> primitives) { 159 List<T> result = new ArrayList<>(primitives.size()); 160 result.addAll(primitives); 161 result.sort(byIdComparator); 162 return result; 163 } 164 165 /** 166 * Writes the full OSM file for the given data set (header, data sources, osm data, footer). 167 * @param data OSM data set 168 * @since 12800 169 */ 170 public void write(DataSet data) { 171 header(data.getDownloadPolicy(), data.getUploadPolicy(), data.isLocked()); 172 writeDataSources(data); 173 writeContent(data); 174 footer(); 175 } 176 177 /** 178 * Writes the contents of the given dataset (nodes, then ways, then relations) 179 * @param ds The dataset to write 180 */ 181 public void writeContent(DataSet ds) { 182 setWithVisible(UploadPolicy.NORMAL.equals(ds.getUploadPolicy())); 183 writeNodes(ds.getNodes()); 184 writeWays(ds.getWays()); 185 writeRelations(ds.getRelations()); 186 } 187 188 /** 189 * Writes the given nodes sorted by id 190 * @param nodes The nodes to write 191 * @since 5737 192 */ 193 public void writeNodes(Collection<Node> nodes) { 194 for (Node n : sortById(nodes)) { 195 if (shouldWrite(n)) { 196 visit(n); 197 } 198 } 199 } 200 201 /** 202 * Writes the given ways sorted by id 203 * @param ways The ways to write 204 * @since 5737 205 */ 206 public void writeWays(Collection<Way> ways) { 207 for (Way w : sortById(ways)) { 208 if (shouldWrite(w)) { 209 visit(w); 210 } 211 } 212 } 213 214 /** 215 * Writes the given relations sorted by id 216 * @param relations The relations to write 217 * @since 5737 218 */ 219 public void writeRelations(Collection<Relation> relations) { 220 for (Relation r : sortById(relations)) { 221 if (shouldWrite(r)) { 222 visit(r); 223 } 224 } 225 } 226 227 protected boolean shouldWrite(OsmPrimitive osm) { 228 return !osm.isNewOrUndeleted() || !osm.isDeleted(); 229 } 230 231 /** 232 * Writes data sources with their respective bounds. 233 * @param ds data set 234 */ 235 public void writeDataSources(DataSet ds) { 236 for (DataSource s : ds.getDataSources()) { 237 out.println(" <bounds minlat='" 238 + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMin()) 239 +"' minlon='" 240 + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMin()) 241 +"' maxlat='" 242 + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMax()) 243 +"' maxlon='" 244 + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMax()) 245 +"' origin='"+XmlWriter.encode(s.origin)+"' />"); 246 } 247 } 248 249 void writeLatLon(LatLon ll) { 250 if (ll != null) { 251 out.print(" lat='"+LatLon.cDdHighPecisionFormatter.format(ll.lat())+ 252 "' lon='"+LatLon.cDdHighPecisionFormatter.format(ll.lon())+'\''); 253 } 254 } 255 256 @Override 257 public void visit(INode n) { 258 if (n.isIncomplete()) return; 259 addCommon(n, "node"); 260 if (!withBody) { 261 out.println("/>"); 262 } else { 263 writeLatLon(n.getCoor()); 264 addTags(n, "node", true); 265 } 266 } 267 268 @Override 269 public void visit(IWay<?> w) { 270 if (w.isIncomplete()) return; 271 addCommon(w, "way"); 272 if (!withBody) { 273 out.println("/>"); 274 } else { 275 out.println(">"); 276 for (int i = 0; i < w.getNodesCount(); ++i) { 277 out.println(" <nd ref='"+w.getNodeId(i) +"' />"); 278 } 279 addTags(w, "way", false); 280 } 281 } 282 283 @Override 284 public void visit(IRelation<?> e) { 285 if (e.isIncomplete()) return; 286 addCommon(e, "relation"); 287 if (!withBody) { 288 out.println("/>"); 289 } else { 290 out.println(">"); 291 for (int i = 0; i < e.getMembersCount(); ++i) { 292 out.print(" <member type='"); 293 out.print(e.getMemberType(i).getAPIName()); 294 out.println("' ref='"+e.getMemberId(i)+"' role='" + 295 XmlWriter.encode(e.getRole(i)) + "' />"); 296 } 297 addTags(e, "relation", false); 298 } 299 } 300 301 /** 302 * Visiting call for changesets. 303 * @param cs changeset 304 */ 305 public void visit(Changeset cs) { 306 out.print(" <changeset id='"+cs.getId()+'\''); 307 if (cs.getUser() != null) { 308 out.print(" user='"+ XmlWriter.encode(cs.getUser().getName()) +'\''); 309 out.print(" uid='"+cs.getUser().getId() +'\''); 310 } 311 Date createdDate = cs.getCreatedAt(); 312 if (createdDate != null) { 313 out.print(" created_at='"+DateUtils.fromDate(createdDate) +'\''); 314 } 315 Date closedDate = cs.getClosedAt(); 316 if (closedDate != null) { 317 out.print(" closed_at='"+DateUtils.fromDate(closedDate) +'\''); 318 } 319 out.print(" open='"+ (cs.isOpen() ? "true" : "false") +'\''); 320 if (cs.getMin() != null) { 321 out.print(" min_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\''); 322 out.print(" min_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\''); 323 } 324 if (cs.getMax() != null) { 325 out.print(" max_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\''); 326 out.print(" max_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\''); 327 } 328 out.println(">"); 329 addTags(cs, "changeset", false); // also writes closing </changeset> 330 } 331 332 protected static final Comparator<Entry<String, String>> byKeyComparator = (o1, o2) -> o1.getKey().compareTo(o2.getKey()); 333 334 protected void addTags(Tagged osm, String tagname, boolean tagOpen) { 335 if (osm.hasKeys()) { 336 if (tagOpen) { 337 out.println(">"); 338 } 339 List<Entry<String, String>> entries = new ArrayList<>(osm.getKeys().entrySet()); 340 entries.sort(byKeyComparator); 341 for (Entry<String, String> e : entries) { 342 out.println(" <tag k='"+ XmlWriter.encode(e.getKey()) + 343 "' v='"+XmlWriter.encode(e.getValue())+ "' />"); 344 } 345 out.println(" </" + tagname + '>'); 346 } else if (tagOpen) { 347 out.println(" />"); 348 } else { 349 out.println(" </" + tagname + '>'); 350 } 351 } 352 353 /** 354 * Add the common part as the form of the tag as well as the XML attributes 355 * id, action, user, and visible. 356 * @param osm osm primitive 357 * @param tagname XML tag matching osm primitive (node, way, relation) 358 */ 359 protected void addCommon(IPrimitive osm, String tagname) { 360 out.print(" <"+tagname); 361 if (osm.getUniqueId() != 0) { 362 out.print(" id='"+ osm.getUniqueId()+'\''); 363 } else 364 throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found")); 365 if (!isOsmChange) { 366 if (!osmConform) { 367 String action = null; 368 if (osm.isDeleted()) { 369 action = "delete"; 370 } else if (osm.isModified()) { 371 action = "modify"; 372 } 373 if (action != null) { 374 out.print(" action='"+action+'\''); 375 } 376 } 377 if (!osm.isTimestampEmpty()) { 378 out.print(" timestamp='"+DateUtils.fromTimestamp(osm.getRawTimestamp())+'\''); 379 } 380 // user and visible added with 0.4 API 381 if (osm.getUser() != null) { 382 if (osm.getUser().isLocalUser()) { 383 out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\''); 384 } else if (osm.getUser().isOsmUser()) { 385 // uid added with 0.6 386 out.print(" uid='"+ osm.getUser().getId()+'\''); 387 out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\''); 388 } 389 } 390 if (withVisible) { 391 out.print(" visible='"+osm.isVisible()+'\''); 392 } 393 } 394 if (osm.getVersion() != 0) { 395 out.print(" version='"+osm.getVersion()+'\''); 396 } 397 if (this.changeset != null && this.changeset.getId() != 0) { 398 out.print(" changeset='"+this.changeset.getId()+'\''); 399 } else if (osm.getChangesetId() > 0 && !osm.isNew()) { 400 out.print(" changeset='"+osm.getChangesetId()+'\''); 401 } 402 } 403}