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