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