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.time.Instant;
008import java.util.ArrayList;
009import java.util.Collection;
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.LatLon;
016import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
017import org.openstreetmap.josm.data.osm.AbstractPrimitive;
018import org.openstreetmap.josm.data.osm.Changeset;
019import org.openstreetmap.josm.data.osm.DataSet;
020import org.openstreetmap.josm.data.osm.DownloadPolicy;
021import org.openstreetmap.josm.data.osm.INode;
022import org.openstreetmap.josm.data.osm.IPrimitive;
023import org.openstreetmap.josm.data.osm.IRelation;
024import org.openstreetmap.josm.data.osm.IWay;
025import org.openstreetmap.josm.data.osm.Node;
026import org.openstreetmap.josm.data.osm.OsmPrimitive;
027import org.openstreetmap.josm.data.osm.Relation;
028import org.openstreetmap.josm.data.osm.Tagged;
029import org.openstreetmap.josm.data.osm.UploadPolicy;
030import org.openstreetmap.josm.data.osm.Way;
031import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
032
033/**
034 * Save the dataset into a stream as osm intern xml format. This is not using any xml library for storing.
035 * @author imi
036 * @since 59
037 */
038public class OsmWriter extends XmlWriter implements PrimitiveVisitor {
039
040    /** Default OSM API version */
041    public static final String DEFAULT_API_VERSION = "0.6";
042
043    private final boolean osmConform;
044    private boolean withBody = true;
045    private boolean withVisible = true;
046    private boolean isOsmChange;
047    private String version;
048    private Changeset changeset;
049
050    /**
051     * Constructs a new {@code OsmWriter}.
052     * Do not call this directly. Use {@link OsmWriterFactory} instead.
053     * @param out print writer
054     * @param osmConform if {@code true}, prevents modification attributes to be written to the common part
055     * @param version OSM API version (0.6)
056     */
057    protected OsmWriter(PrintWriter out, boolean osmConform, String version) {
058        super(out);
059        this.osmConform = osmConform;
060        this.version = version == null ? DEFAULT_API_VERSION : version;
061    }
062
063    /**
064     * Sets whether body must be written.
065     * @param wb if {@code true} body will be written.
066     */
067    public void setWithBody(boolean wb) {
068        this.withBody = wb;
069    }
070
071    /**
072     * Sets whether 'visible' attribute must be written.
073     * @param wv if {@code true} 'visible' attribute will be written.
074     * @since 12019
075     */
076    public void setWithVisible(boolean wv) {
077        this.withVisible = wv;
078    }
079
080    public void setIsOsmChange(boolean isOsmChange) {
081        this.isOsmChange = isOsmChange;
082    }
083
084    public void setChangeset(Changeset cs) {
085        this.changeset = cs;
086    }
087
088    public void setVersion(String v) {
089        this.version = v;
090    }
091
092    /**
093     * Writes OSM header with normal download and upload policies.
094     */
095    public void header() {
096        header(DownloadPolicy.NORMAL, UploadPolicy.NORMAL);
097    }
098
099    /**
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}