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