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