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.UploadPolicy; 022import org.openstreetmap.josm.data.osm.INode; 023import org.openstreetmap.josm.data.osm.IPrimitive; 024import org.openstreetmap.josm.data.osm.IRelation; 025import org.openstreetmap.josm.data.osm.IWay; 026import org.openstreetmap.josm.data.osm.Node; 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.data.osm.Relation; 029import org.openstreetmap.josm.data.osm.Tagged; 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 public void writeDataSources(DataSet ds) { 232 for (DataSource s : ds.getDataSources()) { 233 out.println(" <bounds minlat='" 234 + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMin()) 235 +"' minlon='" 236 + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMin()) 237 +"' maxlat='" 238 + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMax()) 239 +"' maxlon='" 240 + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMax()) 241 +"' origin='"+XmlWriter.encode(s.origin)+"' />"); 242 } 243 } 244 245 void writeLatLon(LatLon ll) { 246 if (ll != null) { 247 out.print(" lat='"+LatLon.cDdHighPecisionFormatter.format(ll.lat())+ 248 "' lon='"+LatLon.cDdHighPecisionFormatter.format(ll.lon())+'\''); 249 } 250 } 251 252 @Override 253 public void visit(INode n) { 254 if (n.isIncomplete()) return; 255 addCommon(n, "node"); 256 if (!withBody) { 257 out.println("/>"); 258 } else { 259 writeLatLon(n.getCoor()); 260 addTags(n, "node", true); 261 } 262 } 263 264 @Override 265 public void visit(IWay w) { 266 if (w.isIncomplete()) return; 267 addCommon(w, "way"); 268 if (!withBody) { 269 out.println("/>"); 270 } else { 271 out.println(">"); 272 for (int i = 0; i < w.getNodesCount(); ++i) { 273 out.println(" <nd ref='"+w.getNodeId(i) +"' />"); 274 } 275 addTags(w, "way", false); 276 } 277 } 278 279 @Override 280 public void visit(IRelation e) { 281 if (e.isIncomplete()) return; 282 addCommon(e, "relation"); 283 if (!withBody) { 284 out.println("/>"); 285 } else { 286 out.println(">"); 287 for (int i = 0; i < e.getMembersCount(); ++i) { 288 out.print(" <member type='"); 289 out.print(e.getMemberType(i).getAPIName()); 290 out.println("' ref='"+e.getMemberId(i)+"' role='" + 291 XmlWriter.encode(e.getRole(i)) + "' />"); 292 } 293 addTags(e, "relation", false); 294 } 295 } 296 297 public void visit(Changeset cs) { 298 out.print(" <changeset id='"+cs.getId()+'\''); 299 if (cs.getUser() != null) { 300 out.print(" user='"+ XmlWriter.encode(cs.getUser().getName()) +'\''); 301 out.print(" uid='"+cs.getUser().getId() +'\''); 302 } 303 Date createdDate = cs.getCreatedAt(); 304 if (createdDate != null) { 305 out.print(" created_at='"+DateUtils.fromDate(createdDate) +'\''); 306 } 307 Date closedDate = cs.getClosedAt(); 308 if (closedDate != null) { 309 out.print(" closed_at='"+DateUtils.fromDate(closedDate) +'\''); 310 } 311 out.print(" open='"+ (cs.isOpen() ? "true" : "false") +'\''); 312 if (cs.getMin() != null) { 313 out.print(" min_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\''); 314 out.print(" min_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\''); 315 } 316 if (cs.getMax() != null) { 317 out.print(" max_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\''); 318 out.print(" max_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\''); 319 } 320 out.println(">"); 321 addTags(cs, "changeset", false); // also writes closing </changeset> 322 } 323 324 protected static final Comparator<Entry<String, String>> byKeyComparator = (o1, o2) -> o1.getKey().compareTo(o2.getKey()); 325 326 protected void addTags(Tagged osm, String tagname, boolean tagOpen) { 327 if (osm.hasKeys()) { 328 if (tagOpen) { 329 out.println(">"); 330 } 331 List<Entry<String, String>> entries = new ArrayList<>(osm.getKeys().entrySet()); 332 entries.sort(byKeyComparator); 333 for (Entry<String, String> e : entries) { 334 out.println(" <tag k='"+ XmlWriter.encode(e.getKey()) + 335 "' v='"+XmlWriter.encode(e.getValue())+ "' />"); 336 } 337 out.println(" </" + tagname + '>'); 338 } else if (tagOpen) { 339 out.println(" />"); 340 } else { 341 out.println(" </" + tagname + '>'); 342 } 343 } 344 345 /** 346 * Add the common part as the form of the tag as well as the XML attributes 347 * id, action, user, and visible. 348 * @param osm osm primitive 349 * @param tagname XML tag matching osm primitive (node, way, relation) 350 */ 351 protected void addCommon(IPrimitive osm, String tagname) { 352 out.print(" <"+tagname); 353 if (osm.getUniqueId() != 0) { 354 out.print(" id='"+ osm.getUniqueId()+'\''); 355 } else 356 throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found")); 357 if (!isOsmChange) { 358 if (!osmConform) { 359 String action = null; 360 if (osm.isDeleted()) { 361 action = "delete"; 362 } else if (osm.isModified()) { 363 action = "modify"; 364 } 365 if (action != null) { 366 out.print(" action='"+action+'\''); 367 } 368 } 369 if (!osm.isTimestampEmpty()) { 370 out.print(" timestamp='"+DateUtils.fromTimestamp(osm.getRawTimestamp())+'\''); 371 } 372 // user and visible added with 0.4 API 373 if (osm.getUser() != null) { 374 if (osm.getUser().isLocalUser()) { 375 out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\''); 376 } else if (osm.getUser().isOsmUser()) { 377 // uid added with 0.6 378 out.print(" uid='"+ osm.getUser().getId()+'\''); 379 out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\''); 380 } 381 } 382 if (withVisible) { 383 out.print(" visible='"+osm.isVisible()+'\''); 384 } 385 } 386 if (osm.getVersion() != 0) { 387 out.print(" version='"+osm.getVersion()+'\''); 388 } 389 if (this.changeset != null && this.changeset.getId() != 0) { 390 out.print(" changeset='"+this.changeset.getId()+'\''); 391 } else if (osm.getChangesetId() > 0 && !osm.isNew()) { 392 out.print(" changeset='"+osm.getChangesetId()+'\''); 393 } 394 } 395}