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