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