source: josm/trunk/src/org/openstreetmap/josm/io/OsmWriter.java

Last change on this file was 17848, checked in by simon04, 3 years ago

see #20829 - Avoid heap allocations in OsmWriter

  • Property svn:eol-style set to native
File size: 13.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.PrintWriter;
7import java.time.Instant;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Comparator;
11import java.util.List;
12import java.util.Map.Entry;
13
14import org.openstreetmap.josm.data.DataSource;
15import org.openstreetmap.josm.data.coor.LatLon;
16import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
17import org.openstreetmap.josm.data.osm.AbstractPrimitive;
18import org.openstreetmap.josm.data.osm.Changeset;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.data.osm.DownloadPolicy;
21import org.openstreetmap.josm.data.osm.INode;
22import org.openstreetmap.josm.data.osm.IPrimitive;
23import org.openstreetmap.josm.data.osm.IRelation;
24import org.openstreetmap.josm.data.osm.IWay;
25import org.openstreetmap.josm.data.osm.Node;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.data.osm.Relation;
28import org.openstreetmap.josm.data.osm.Tagged;
29import org.openstreetmap.josm.data.osm.UploadPolicy;
30import org.openstreetmap.josm.data.osm.Way;
31import 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 */
38public 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} &rarr; {@code -infinity}, then {@code +1} &rarr; {@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}
Note: See TracBrowser for help on using the repository browser.