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