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.Comparator;
010import java.util.Date;
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;
032import org.openstreetmap.josm.tools.date.DateUtils;
033
034/**
035 * Save the dataset into a stream as osm intern xml format. This is not using any xml library for storing.
036 * @author imi
037 * @since 59
038 */
039public class OsmWriter extends XmlWriter implements PrimitiveVisitor {
040
041    /** Default OSM API version */
042    public static final String DEFAULT_API_VERSION = "0.6";
043
044    private final boolean osmConform;
045    private boolean withBody = true;
046    private boolean withVisible = true;
047    private boolean isOsmChange;
048    private String version;
049    private Changeset changeset;
050
051    /**
052     * Constructs a new {@code OsmWriter}.
053     * Do not call this directly. Use {@link OsmWriterFactory} instead.
054     * @param out print writer
055     * @param osmConform if {@code true}, prevents modification attributes to be written to the common part
056     * @param version OSM API version (0.6)
057     */
058    protected OsmWriter(PrintWriter out, boolean osmConform, String version) {
059        super(out);
060        this.osmConform = osmConform;
061        this.version = version == null ? DEFAULT_API_VERSION : version;
062    }
063
064    /**
065     * Sets whether body must be written.
066     * @param wb if {@code true} body will be written.
067     */
068    public void setWithBody(boolean wb) {
069        this.withBody = wb;
070    }
071
072    /**
073     * Sets whether 'visible' attribute must be written.
074     * @param wv if {@code true} 'visible' attribute will be written.
075     * @since 12019
076     */
077    public void setWithVisible(boolean wv) {
078        this.withVisible = wv;
079    }
080
081    public void setIsOsmChange(boolean isOsmChange) {
082        this.isOsmChange = isOsmChange;
083    }
084
085    public void setChangeset(Changeset cs) {
086        this.changeset = cs;
087    }
088
089    public void setVersion(String v) {
090        this.version = v;
091    }
092
093    /**
094     * Writes OSM header with normal download and upload policies.
095     */
096    public void header() {
097        header(DownloadPolicy.NORMAL, UploadPolicy.NORMAL);
098    }
099
100    /**
101     * Writes OSM header with normal download policy and given upload policy.
102     * @deprecated Use {@link #header(DownloadPolicy, UploadPolicy)} instead
103     * @param upload upload policy
104     */
105    @Deprecated
106    public void header(UploadPolicy upload) {
107        header(DownloadPolicy.NORMAL, upload);
108    }
109
110    /**
111     * Writes OSM header with given download upload policies.
112     * @param download download policy
113     * @param upload upload policy
114     * @since 13485
115     */
116    public void header(DownloadPolicy download, UploadPolicy upload) {
117        header(download, upload, false);
118    }
119
120    private void header(DownloadPolicy download, UploadPolicy upload, boolean locked) {
121        out.println("<?xml version='1.0' encoding='UTF-8'?>");
122        out.print("<osm version='");
123        out.print(version);
124        if (download != null && download != DownloadPolicy.NORMAL) {
125            out.print("' download='");
126            out.print(download.getXmlFlag());
127        }
128        if (upload != null && upload != UploadPolicy.NORMAL) {
129            out.print("' upload='");
130            out.print(upload.getXmlFlag());
131        }
132        if (locked) {
133            out.print("' locked=true");
134        }
135        out.println("' generator='JOSM'>");
136    }
137
138    /**
139     * Writes OSM footer.
140     */
141    public void footer() {
142        out.println("</osm>");
143    }
144
145    /**
146     * Sorts {@code -1} &rarr; {@code -infinity}, then {@code +1} &rarr; {@code +infinity}
147     */
148    protected static final Comparator<AbstractPrimitive> byIdComparator = (o1, o2) -> {
149        final long i1 = o1.getUniqueId();
150        final long i2 = o2.getUniqueId();
151        if (i1 < 0 && i2 < 0) {
152            return Long.compare(i2, i1);
153        } else {
154            return Long.compare(i1, i2);
155        }
156    };
157
158    protected <T extends OsmPrimitive> Collection<T> sortById(Collection<T> primitives) {
159        List<T> result = new ArrayList<>(primitives.size());
160        result.addAll(primitives);
161        result.sort(byIdComparator);
162        return result;
163    }
164
165    /**
166     * Writes the full OSM file for the given data set (header, data sources, osm data, footer).
167     * @param data OSM data set
168     * @since 12800
169     */
170    public void write(DataSet data) {
171        header(data.getDownloadPolicy(), data.getUploadPolicy(), data.isLocked());
172        writeDataSources(data);
173        writeContent(data);
174        footer();
175    }
176
177    /**
178     * Writes the contents of the given dataset (nodes, then ways, then relations)
179     * @param ds The dataset to write
180     */
181    public void writeContent(DataSet ds) {
182        setWithVisible(UploadPolicy.NORMAL.equals(ds.getUploadPolicy()));
183        writeNodes(ds.getNodes());
184        writeWays(ds.getWays());
185        writeRelations(ds.getRelations());
186    }
187
188    /**
189     * Writes the given nodes sorted by id
190     * @param nodes The nodes to write
191     * @since 5737
192     */
193    public void writeNodes(Collection<Node> nodes) {
194        for (Node n : sortById(nodes)) {
195            if (shouldWrite(n)) {
196                visit(n);
197            }
198        }
199    }
200
201    /**
202     * Writes the given ways sorted by id
203     * @param ways The ways to write
204     * @since 5737
205     */
206    public void writeWays(Collection<Way> ways) {
207        for (Way w : sortById(ways)) {
208            if (shouldWrite(w)) {
209                visit(w);
210            }
211        }
212    }
213
214    /**
215     * Writes the given relations sorted by id
216     * @param relations The relations to write
217     * @since 5737
218     */
219    public void writeRelations(Collection<Relation> relations) {
220        for (Relation r : sortById(relations)) {
221            if (shouldWrite(r)) {
222                visit(r);
223            }
224        }
225    }
226
227    protected boolean shouldWrite(OsmPrimitive osm) {
228        return !osm.isNewOrUndeleted() || !osm.isDeleted();
229    }
230
231    /**
232     * Writes data sources with their respective bounds.
233     * @param ds data set
234     */
235    public void writeDataSources(DataSet ds) {
236        for (DataSource s : ds.getDataSources()) {
237            out.println("  <bounds minlat='"
238                    + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMin())
239                    +"' minlon='"
240                    + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMin())
241                    +"' maxlat='"
242                    + DecimalDegreesCoordinateFormat.INSTANCE.latToString(s.bounds.getMax())
243                    +"' maxlon='"
244                    + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(s.bounds.getMax())
245                    +"' origin='"+XmlWriter.encode(s.origin)+"' />");
246        }
247    }
248
249    void writeLatLon(LatLon ll) {
250        if (ll != null) {
251            out.print(" lat='"+LatLon.cDdHighPecisionFormatter.format(ll.lat())+
252                     "' lon='"+LatLon.cDdHighPecisionFormatter.format(ll.lon())+'\'');
253        }
254    }
255
256    @Override
257    public void visit(INode n) {
258        if (n.isIncomplete()) return;
259        addCommon(n, "node");
260        if (!withBody) {
261            out.println("/>");
262        } else {
263            writeLatLon(n.getCoor());
264            addTags(n, "node", true);
265        }
266    }
267
268    @Override
269    public void visit(IWay<?> w) {
270        if (w.isIncomplete()) return;
271        addCommon(w, "way");
272        if (!withBody) {
273            out.println("/>");
274        } else {
275            out.println(">");
276            for (int i = 0; i < w.getNodesCount(); ++i) {
277                out.println("    <nd ref='"+w.getNodeId(i) +"' />");
278            }
279            addTags(w, "way", false);
280        }
281    }
282
283    @Override
284    public void visit(IRelation<?> e) {
285        if (e.isIncomplete()) return;
286        addCommon(e, "relation");
287        if (!withBody) {
288            out.println("/>");
289        } else {
290            out.println(">");
291            for (int i = 0; i < e.getMembersCount(); ++i) {
292                out.print("    <member type='");
293                out.print(e.getMemberType(i).getAPIName());
294                out.println("' ref='"+e.getMemberId(i)+"' role='" +
295                        XmlWriter.encode(e.getRole(i)) + "' />");
296            }
297            addTags(e, "relation", false);
298        }
299    }
300
301    /**
302     * Visiting call for changesets.
303     * @param cs changeset
304     */
305    public void visit(Changeset cs) {
306        out.print("  <changeset id='"+cs.getId()+'\'');
307        if (cs.getUser() != null) {
308            out.print(" user='"+ XmlWriter.encode(cs.getUser().getName()) +'\'');
309            out.print(" uid='"+cs.getUser().getId() +'\'');
310        }
311        Date createdDate = cs.getCreatedAt();
312        if (createdDate != null) {
313            out.print(" created_at='"+DateUtils.fromDate(createdDate) +'\'');
314        }
315        Date closedDate = cs.getClosedAt();
316        if (closedDate != null) {
317            out.print(" closed_at='"+DateUtils.fromDate(closedDate) +'\'');
318        }
319        out.print(" open='"+ (cs.isOpen() ? "true" : "false") +'\'');
320        if (cs.getMin() != null) {
321            out.print(" min_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\'');
322            out.print(" min_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\'');
323        }
324        if (cs.getMax() != null) {
325            out.print(" max_lon='"+ DecimalDegreesCoordinateFormat.INSTANCE.lonToString(cs.getMin()) +'\'');
326            out.print(" max_lat='"+ DecimalDegreesCoordinateFormat.INSTANCE.latToString(cs.getMin()) +'\'');
327        }
328        out.println(">");
329        addTags(cs, "changeset", false); // also writes closing </changeset>
330    }
331
332    protected static final Comparator<Entry<String, String>> byKeyComparator = (o1, o2) -> o1.getKey().compareTo(o2.getKey());
333
334    protected void addTags(Tagged osm, String tagname, boolean tagOpen) {
335        if (osm.hasKeys()) {
336            if (tagOpen) {
337                out.println(">");
338            }
339            List<Entry<String, String>> entries = new ArrayList<>(osm.getKeys().entrySet());
340            entries.sort(byKeyComparator);
341            for (Entry<String, String> e : entries) {
342                out.println("    <tag k='"+ XmlWriter.encode(e.getKey()) +
343                        "' v='"+XmlWriter.encode(e.getValue())+ "' />");
344            }
345            out.println("  </" + tagname + '>');
346        } else if (tagOpen) {
347            out.println(" />");
348        } else {
349            out.println("  </" + tagname + '>');
350        }
351    }
352
353    /**
354     * Add the common part as the form of the tag as well as the XML attributes
355     * id, action, user, and visible.
356     * @param osm osm primitive
357     * @param tagname XML tag matching osm primitive (node, way, relation)
358     */
359    protected void addCommon(IPrimitive osm, String tagname) {
360        out.print("  <"+tagname);
361        if (osm.getUniqueId() != 0) {
362            out.print(" id='"+ osm.getUniqueId()+'\'');
363        } else
364            throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found"));
365        if (!isOsmChange) {
366            if (!osmConform) {
367                String action = null;
368                if (osm.isDeleted()) {
369                    action = "delete";
370                } else if (osm.isModified()) {
371                    action = "modify";
372                }
373                if (action != null) {
374                    out.print(" action='"+action+'\'');
375                }
376            }
377            if (!osm.isTimestampEmpty()) {
378                out.print(" timestamp='"+DateUtils.fromTimestamp(osm.getRawTimestamp())+'\'');
379            }
380            // user and visible added with 0.4 API
381            if (osm.getUser() != null) {
382                if (osm.getUser().isLocalUser()) {
383                    out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\'');
384                } else if (osm.getUser().isOsmUser()) {
385                    // uid added with 0.6
386                    out.print(" uid='"+ osm.getUser().getId()+'\'');
387                    out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\'');
388                }
389            }
390            if (withVisible) {
391                out.print(" visible='"+osm.isVisible()+'\'');
392            }
393        }
394        if (osm.getVersion() != 0) {
395            out.print(" version='"+osm.getVersion()+'\'');
396        }
397        if (this.changeset != null && this.changeset.getId() != 0) {
398            out.print(" changeset='"+this.changeset.getId()+'\'');
399        } else if (osm.getChangesetId() > 0 && !osm.isNew()) {
400            out.print(" changeset='"+osm.getChangesetId()+'\'');
401        }
402    }
403}