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