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