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