| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.io;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 5 |
|
|---|
| 6 | import java.io.PrintWriter;
|
|---|
| 7 | import java.time.Instant;
|
|---|
| 8 | import java.util.ArrayList;
|
|---|
| 9 | import java.util.Collection;
|
|---|
| 10 | import java.util.Comparator;
|
|---|
| 11 | import java.util.List;
|
|---|
| 12 | import java.util.Map.Entry;
|
|---|
| 13 |
|
|---|
| 14 | import org.openstreetmap.josm.data.DataSource;
|
|---|
| 15 | import org.openstreetmap.josm.data.coor.LatLon;
|
|---|
| 16 | import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
|
|---|
| 17 | import org.openstreetmap.josm.data.osm.AbstractPrimitive;
|
|---|
| 18 | import org.openstreetmap.josm.data.osm.Changeset;
|
|---|
| 19 | import org.openstreetmap.josm.data.osm.DataSet;
|
|---|
| 20 | import org.openstreetmap.josm.data.osm.DownloadPolicy;
|
|---|
| 21 | import org.openstreetmap.josm.data.osm.INode;
|
|---|
| 22 | import org.openstreetmap.josm.data.osm.IPrimitive;
|
|---|
| 23 | import org.openstreetmap.josm.data.osm.IRelation;
|
|---|
| 24 | import org.openstreetmap.josm.data.osm.IWay;
|
|---|
| 25 | import org.openstreetmap.josm.data.osm.Node;
|
|---|
| 26 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
|---|
| 27 | import org.openstreetmap.josm.data.osm.Relation;
|
|---|
| 28 | import org.openstreetmap.josm.data.osm.Tagged;
|
|---|
| 29 | import org.openstreetmap.josm.data.osm.UploadPolicy;
|
|---|
| 30 | import org.openstreetmap.josm.data.osm.Way;
|
|---|
| 31 | import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
|
|---|
| 32 |
|
|---|
| 33 | /**
|
|---|
| 34 | * Save the dataset into a stream as osm intern xml format. This is not using any xml library for storing.
|
|---|
| 35 | * @author imi
|
|---|
| 36 | * @since 59
|
|---|
| 37 | */
|
|---|
| 38 | public class OsmWriter extends XmlWriter implements PrimitiveVisitor {
|
|---|
| 39 |
|
|---|
| 40 | /** Default OSM API version */
|
|---|
| 41 | public static final String DEFAULT_API_VERSION = "0.6";
|
|---|
| 42 |
|
|---|
| 43 | private final boolean osmConform;
|
|---|
| 44 | private boolean withBody = true;
|
|---|
| 45 | private boolean withVisible = true;
|
|---|
| 46 | private boolean isOsmChange;
|
|---|
| 47 | private String version;
|
|---|
| 48 | private Changeset changeset;
|
|---|
| 49 |
|
|---|
| 50 | /**
|
|---|
| 51 | * Constructs a new {@code OsmWriter}.
|
|---|
| 52 | * Do not call this directly. Use {@link OsmWriterFactory} instead.
|
|---|
| 53 | * @param out print writer
|
|---|
| 54 | * @param osmConform if {@code true}, prevents modification attributes to be written to the common part
|
|---|
| 55 | * @param version OSM API version (0.6)
|
|---|
| 56 | */
|
|---|
| 57 | protected OsmWriter(PrintWriter out, boolean osmConform, String version) {
|
|---|
| 58 | super(out);
|
|---|
| 59 | this.osmConform = osmConform;
|
|---|
| 60 | this.version = version == null ? DEFAULT_API_VERSION : version;
|
|---|
| 61 | }
|
|---|
| 62 |
|
|---|
| 63 | /**
|
|---|
| 64 | * Sets whether body must be written.
|
|---|
| 65 | * @param wb if {@code true} body will be written.
|
|---|
| 66 | */
|
|---|
| 67 | public void setWithBody(boolean wb) {
|
|---|
| 68 | this.withBody = wb;
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | /**
|
|---|
| 72 | * Sets whether 'visible' attribute must be written.
|
|---|
| 73 | * @param wv if {@code true} 'visible' attribute will be written.
|
|---|
| 74 | * @since 12019
|
|---|
| 75 | */
|
|---|
| 76 | public void setWithVisible(boolean wv) {
|
|---|
| 77 | this.withVisible = wv;
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | public void setIsOsmChange(boolean isOsmChange) {
|
|---|
| 81 | this.isOsmChange = isOsmChange;
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | public void setChangeset(Changeset cs) {
|
|---|
| 85 | this.changeset = cs;
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | public void setVersion(String v) {
|
|---|
| 89 | this.version = v;
|
|---|
| 90 | }
|
|---|
| 91 |
|
|---|
| 92 | /**
|
|---|
| 93 | * Writes OSM header with normal download and upload policies.
|
|---|
| 94 | */
|
|---|
| 95 | public void header() {
|
|---|
| 96 | header(DownloadPolicy.NORMAL, UploadPolicy.NORMAL);
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | /**
|
|---|
| 100 | * Writes OSM header with given download upload policies.
|
|---|
| 101 | * @param download download policy
|
|---|
| 102 | * @param upload upload policy
|
|---|
| 103 | * @since 13485
|
|---|
| 104 | */
|
|---|
| 105 | public void header(DownloadPolicy download, UploadPolicy upload) {
|
|---|
| 106 | header(download, upload, false);
|
|---|
| 107 | }
|
|---|
| 108 |
|
|---|
| 109 | private void header(DownloadPolicy download, UploadPolicy upload, boolean locked) {
|
|---|
| 110 | out.println("<?xml version='1.0' encoding='UTF-8'?>");
|
|---|
| 111 | out.print("<osm version='");
|
|---|
| 112 | out.print(version);
|
|---|
| 113 | if (download != null && download != DownloadPolicy.NORMAL) {
|
|---|
| 114 | out.print("' download='");
|
|---|
| 115 | out.print(download.getXmlFlag());
|
|---|
| 116 | }
|
|---|
| 117 | if (upload != null && upload != UploadPolicy.NORMAL) {
|
|---|
| 118 | out.print("' upload='");
|
|---|
| 119 | out.print(upload.getXmlFlag());
|
|---|
| 120 | }
|
|---|
| 121 | if (locked) {
|
|---|
| 122 | out.print("' locked='true");
|
|---|
| 123 | }
|
|---|
| 124 | out.println("' generator='JOSM'>");
|
|---|
| 125 | }
|
|---|
| 126 |
|
|---|
| 127 | /**
|
|---|
| 128 | * Writes OSM footer.
|
|---|
| 129 | */
|
|---|
| 130 | public void footer() {
|
|---|
| 131 | out.println("</osm>");
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | /**
|
|---|
| 135 | * Sorts {@code -1} → {@code -infinity}, then {@code +1} → {@code +infinity}
|
|---|
| 136 | */
|
|---|
| 137 | protected static final Comparator<AbstractPrimitive> byIdComparator = (o1, o2) -> {
|
|---|
| 138 | final long i1 = o1.getUniqueId();
|
|---|
| 139 | final long i2 = o2.getUniqueId();
|
|---|
| 140 | if (i1 < 0 && i2 < 0) {
|
|---|
| 141 | return Long.compare(i2, i1);
|
|---|
| 142 | } else {
|
|---|
| 143 | return Long.compare(i1, i2);
|
|---|
| 144 | }
|
|---|
| 145 | };
|
|---|
| 146 |
|
|---|
| 147 | protected <T extends OsmPrimitive> Collection<T> sortById(Collection<T> primitives) {
|
|---|
| 148 | List<T> result = new ArrayList<>(primitives.size());
|
|---|
| 149 | result.addAll(primitives);
|
|---|
| 150 | result.sort(byIdComparator);
|
|---|
| 151 | return result;
|
|---|
| 152 | }
|
|---|
| 153 |
|
|---|
| 154 | /**
|
|---|
| 155 | * Writes the full OSM file for the given data set (header, data sources, osm data, footer).
|
|---|
| 156 | * @param data OSM data set
|
|---|
| 157 | * @since 12800
|
|---|
| 158 | */
|
|---|
| 159 | public void write(DataSet data) {
|
|---|
| 160 | header(data.getDownloadPolicy(), data.getUploadPolicy(), data.isLocked());
|
|---|
| 161 | writeDataSources(data);
|
|---|
| 162 | writeContent(data);
|
|---|
| 163 | footer();
|
|---|
| 164 | }
|
|---|
| 165 |
|
|---|
| 166 | /**
|
|---|
| 167 | * Writes the contents of the given dataset (nodes, then ways, then relations)
|
|---|
| 168 | * @param ds The dataset to write
|
|---|
| 169 | */
|
|---|
| 170 | public void writeContent(DataSet ds) {
|
|---|
| 171 | setWithVisible(UploadPolicy.NORMAL == ds.getUploadPolicy());
|
|---|
| 172 | writeNodes(ds.getNodes());
|
|---|
| 173 | writeWays(ds.getWays());
|
|---|
| 174 | writeRelations(ds.getRelations());
|
|---|
| 175 | }
|
|---|
| 176 |
|
|---|
| 177 | /**
|
|---|
| 178 | * Writes the given nodes sorted by id
|
|---|
| 179 | * @param nodes The nodes to write
|
|---|
| 180 | * @since 5737
|
|---|
| 181 | */
|
|---|
| 182 | public void writeNodes(Collection<Node> nodes) {
|
|---|
| 183 | for (Node n : sortById(nodes)) {
|
|---|
| 184 | if (shouldWrite(n)) {
|
|---|
| 185 | visit(n);
|
|---|
| 186 | }
|
|---|
| 187 | }
|
|---|
| 188 | }
|
|---|
| 189 |
|
|---|
| 190 | /**
|
|---|
| 191 | * Writes the given ways sorted by id
|
|---|
| 192 | * @param ways The ways to write
|
|---|
| 193 | * @since 5737
|
|---|
| 194 | */
|
|---|
| 195 | public void writeWays(Collection<Way> ways) {
|
|---|
| 196 | for (Way w : sortById(ways)) {
|
|---|
| 197 | if (shouldWrite(w)) {
|
|---|
| 198 | visit(w);
|
|---|
| 199 | }
|
|---|
| 200 | }
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | /**
|
|---|
| 204 | * Writes the given relations sorted by id
|
|---|
| 205 | * @param relations The relations to write
|
|---|
| 206 | * @since 5737
|
|---|
| 207 | */
|
|---|
| 208 | public void writeRelations(Collection<Relation> relations) {
|
|---|
| 209 | for (Relation r : sortById(relations)) {
|
|---|
| 210 | if (shouldWrite(r)) {
|
|---|
| 211 | visit(r);
|
|---|
| 212 | }
|
|---|
| 213 | }
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | protected boolean shouldWrite(OsmPrimitive osm) {
|
|---|
| 217 | return !osm.isNewOrUndeleted() || !osm.isDeleted();
|
|---|
| 218 | }
|
|---|
| 219 |
|
|---|
| 220 | /**
|
|---|
| 221 | * Writes data sources with their respective bounds.
|
|---|
| 222 | * @param ds data set
|
|---|
| 223 | */
|
|---|
| 224 | public void writeDataSources(DataSet ds) {
|
|---|
| 225 | for (DataSource s : ds.getDataSources()) {
|
|---|
| 226 | out.append(" <bounds minlat='").append(DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMin()));
|
|---|
| 227 | out.append("' minlon='").append(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMin()));
|
|---|
| 228 | out.append("' maxlat='").append(DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMax()));
|
|---|
| 229 | out.append("' maxlon='").append(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMax()));
|
|---|
| 230 | out.append("' origin='").append(XmlWriter.encode(s.origin)).append("' />");
|
|---|
| 231 | out.println();
|
|---|
| 232 | }
|
|---|
| 233 | }
|
|---|
| 234 |
|
|---|
| 235 | void writeLatLon(LatLon ll) {
|
|---|
| 236 | if (ll != null) {
|
|---|
| 237 | out.append(" lat='").append(LatLon.cDdHighPrecisionFormatter.format(ll.lat())).append("'");
|
|---|
| 238 | out.append(" lon='").append(LatLon.cDdHighPrecisionFormatter.format(ll.lon())).append("'");
|
|---|
| 239 | }
|
|---|
| 240 | }
|
|---|
| 241 |
|
|---|
| 242 | @Override
|
|---|
| 243 | public void visit(INode n) {
|
|---|
| 244 | if (n.isIncomplete()) return;
|
|---|
| 245 | addCommon(n, "node");
|
|---|
| 246 | if (!withBody) {
|
|---|
| 247 | out.println("/>");
|
|---|
| 248 | } else {
|
|---|
| 249 | writeLatLon(n.getCoor());
|
|---|
| 250 | addTags(n, "node", true);
|
|---|
| 251 | }
|
|---|
| 252 | }
|
|---|
| 253 |
|
|---|
| 254 | @Override
|
|---|
| 255 | public void visit(IWay<?> w) {
|
|---|
| 256 | if (w.isIncomplete()) return;
|
|---|
| 257 | addCommon(w, "way");
|
|---|
| 258 | if (!withBody) {
|
|---|
| 259 | out.println("/>");
|
|---|
| 260 | } else {
|
|---|
| 261 | out.println(">");
|
|---|
| 262 | for (int i = 0; i < w.getNodesCount(); ++i) {
|
|---|
| 263 | out.append(" <nd ref='").append(String.valueOf(w.getNodeId(i))).append("' />");
|
|---|
| 264 | out.println();
|
|---|
| 265 | }
|
|---|
| 266 | addTags(w, "way", false);
|
|---|
| 267 | }
|
|---|
| 268 | }
|
|---|
| 269 |
|
|---|
| 270 | @Override
|
|---|
| 271 | public void visit(IRelation<?> e) {
|
|---|
| 272 | if (e.isIncomplete()) return;
|
|---|
| 273 | addCommon(e, "relation");
|
|---|
| 274 | if (!withBody) {
|
|---|
| 275 | out.println("/>");
|
|---|
| 276 | } else {
|
|---|
| 277 | out.println(">");
|
|---|
| 278 | for (int i = 0; i < e.getMembersCount(); ++i) {
|
|---|
| 279 | out.print(" <member type='");
|
|---|
| 280 | out.print(e.getMemberType(i).getAPIName());
|
|---|
| 281 | out.append("' ref='").append(String.valueOf(e.getMemberId(i)));
|
|---|
| 282 | out.append("' role='").append(XmlWriter.encode(e.getRole(i))).append("' />");
|
|---|
| 283 | out.println();
|
|---|
| 284 | }
|
|---|
| 285 | addTags(e, "relation", false);
|
|---|
| 286 | }
|
|---|
| 287 | }
|
|---|
| 288 |
|
|---|
| 289 | /**
|
|---|
| 290 | * Visiting call for changesets.
|
|---|
| 291 | * @param cs changeset
|
|---|
| 292 | */
|
|---|
| 293 | public void visit(Changeset cs) {
|
|---|
| 294 | out.append(" <changeset id='").append(String.valueOf(cs.getId())).append("'");
|
|---|
| 295 | if (cs.getUser() != null) {
|
|---|
| 296 | out.append(" user='").append(XmlWriter.encode(cs.getUser().getName())).append("'");
|
|---|
| 297 | out.append(" uid='").append(String.valueOf(cs.getUser().getId())).append("'");
|
|---|
| 298 | }
|
|---|
| 299 | Instant createdDate = cs.getCreatedAt();
|
|---|
| 300 | if (createdDate != null) {
|
|---|
| 301 | out.append(" created_at='").append(String.valueOf(createdDate)).append("'");
|
|---|
| 302 | }
|
|---|
| 303 | Instant closedDate = cs.getClosedAt();
|
|---|
| 304 | if (closedDate != null) {
|
|---|
| 305 | out.append(" closed_at='").append(String.valueOf(closedDate)).append("'");
|
|---|
| 306 | }
|
|---|
| 307 | out.append(" open='").append(cs.isOpen() ? "true" : "false").append("'");
|
|---|
| 308 | if (cs.getMin() != null) {
|
|---|
| 309 | out.append(" min_lon='").append(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin())).append("'");
|
|---|
| 310 | out.append(" min_lat='").append(DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin())).append("'");
|
|---|
| 311 | }
|
|---|
| 312 | if (cs.getMax() != null) {
|
|---|
| 313 | out.append(" max_lon='").append(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMax())).append("'");
|
|---|
| 314 | out.append(" max_lat='").append(DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMax())).append("'");
|
|---|
| 315 | }
|
|---|
| 316 | out.println(">");
|
|---|
| 317 | addTags(cs, "changeset", false); // also writes closing </changeset>
|
|---|
| 318 | }
|
|---|
| 319 |
|
|---|
| 320 | protected static final Comparator<Entry<String, String>> byKeyComparator = Entry.comparingByKey();
|
|---|
| 321 |
|
|---|
| 322 | protected void addTags(Tagged osm, String tagname, boolean tagOpen) {
|
|---|
| 323 | if (osm.hasKeys()) {
|
|---|
| 324 | if (tagOpen) {
|
|---|
| 325 | out.println(">");
|
|---|
| 326 | }
|
|---|
| 327 | List<Entry<String, String>> entries = new ArrayList<>(osm.getKeys().entrySet());
|
|---|
| 328 | entries.sort(byKeyComparator);
|
|---|
| 329 | for (Entry<String, String> e : entries) {
|
|---|
| 330 | out.append(" <tag k='").append(XmlWriter.encode(e.getKey()));
|
|---|
| 331 | out.append("' v='").append(XmlWriter.encode(e.getValue())).append("' />");
|
|---|
| 332 | out.println();
|
|---|
| 333 | }
|
|---|
| 334 | out.println(" </" + tagname + '>');
|
|---|
| 335 | } else if (tagOpen) {
|
|---|
| 336 | out.println(" />");
|
|---|
| 337 | } else {
|
|---|
| 338 | out.println(" </" + tagname + '>');
|
|---|
| 339 | }
|
|---|
| 340 | }
|
|---|
| 341 |
|
|---|
| 342 | /**
|
|---|
| 343 | * Add the common part as the form of the tag as well as the XML attributes
|
|---|
| 344 | * id, action, user, and visible.
|
|---|
| 345 | * @param osm osm primitive
|
|---|
| 346 | * @param tagname XML tag matching osm primitive (node, way, relation)
|
|---|
| 347 | */
|
|---|
| 348 | protected void addCommon(IPrimitive osm, String tagname) {
|
|---|
| 349 | out.append(" <").append(tagname);
|
|---|
| 350 | if (osm.getUniqueId() != 0) {
|
|---|
| 351 | out.append(" id='").append(String.valueOf(osm.getUniqueId())).append("'");
|
|---|
| 352 | } else
|
|---|
| 353 | throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found"));
|
|---|
| 354 | if (!isOsmChange) {
|
|---|
| 355 | if (!osmConform) {
|
|---|
| 356 | String action = null;
|
|---|
| 357 | if (osm.isDeleted()) {
|
|---|
| 358 | action = "delete";
|
|---|
| 359 | } else if (osm.isModified()) {
|
|---|
| 360 | action = "modify";
|
|---|
| 361 | }
|
|---|
| 362 | if (action != null) {
|
|---|
| 363 | out.append(" action='").append(action).append("'");
|
|---|
| 364 | }
|
|---|
| 365 | }
|
|---|
| 366 | if (!osm.isTimestampEmpty()) {
|
|---|
| 367 | out.append(" timestamp='").append(String.valueOf(osm.getInstant())).append("'");
|
|---|
| 368 | }
|
|---|
| 369 | // user and visible added with 0.4 API
|
|---|
| 370 | if (osm.getUser() != null) {
|
|---|
| 371 | if (osm.getUser().isLocalUser()) {
|
|---|
| 372 | out.append(" user='").append(XmlWriter.encode(osm.getUser().getName())).append("'");
|
|---|
| 373 | } else if (osm.getUser().isOsmUser()) {
|
|---|
| 374 | // uid added with 0.6
|
|---|
| 375 | out.append(" uid='").append(String.valueOf(osm.getUser().getId())).append("'");
|
|---|
| 376 | out.append(" user='").append(XmlWriter.encode(osm.getUser().getName())).append("'");
|
|---|
| 377 | }
|
|---|
| 378 | }
|
|---|
| 379 | if (withVisible) {
|
|---|
| 380 | out.append(" visible='").append(String.valueOf(osm.isVisible())).append("'");
|
|---|
| 381 | }
|
|---|
| 382 | }
|
|---|
| 383 | if (osm.getVersion() != 0) {
|
|---|
| 384 | out.append(" version='").append(String.valueOf(osm.getVersion())).append("'");
|
|---|
| 385 | }
|
|---|
| 386 | if (this.changeset != null && this.changeset.getId() != 0) {
|
|---|
| 387 | out.append(" changeset='").append(String.valueOf(this.changeset.getId())).append("'");
|
|---|
| 388 | } else if (osm.getChangesetId() > 0 && !osm.isNew()) {
|
|---|
| 389 | out.append(" changeset='").append(String.valueOf(osm.getChangesetId())).append("'");
|
|---|
| 390 | }
|
|---|
| 391 | }
|
|---|
| 392 | }
|
|---|