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