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