// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.io;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import org.openstreetmap.josm.data.DataSource;
import org.openstreetmap.josm.data.coor.CoordinateFormat;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.AbstractPrimitive;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.INode;
import org.openstreetmap.josm.data.osm.IPrimitive;
import org.openstreetmap.josm.data.osm.IRelation;
import org.openstreetmap.josm.data.osm.IWay;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Tagged;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.date.DateUtils;
/**
* Save the dataset into a stream as osm intern xml format. This is not using any
* xml library for storing.
* @author imi
*/
public class OsmWriter extends XmlWriter implements PrimitiveVisitor {
public static final String DEFAULT_API_VERSION = "0.6";
private final boolean osmConform;
private boolean withBody = true;
private boolean isOsmChange;
private String version;
private Changeset changeset;
/**
* Constructs a new {@code OsmWriter}.
* Do not call this directly. Use {@link OsmWriterFactory} instead.
* @param out print writer
* @param osmConform if {@code true}, prevents modification attributes to be written to the common part
* @param version OSM API version (0.6)
*/
protected OsmWriter(PrintWriter out, boolean osmConform, String version) {
super(out);
this.osmConform = osmConform;
this.version = version == null ? DEFAULT_API_VERSION : version;
}
public void setWithBody(boolean wb) {
this.withBody = wb;
}
public void setIsOsmChange(boolean isOsmChange) {
this.isOsmChange = isOsmChange;
}
public void setChangeset(Changeset cs) {
this.changeset = cs;
}
public void setVersion(String v) {
this.version = v;
}
public void header() {
header(null);
}
public void header(Boolean upload) {
out.println("");
out.print("");
}
public void footer() {
out.println("");
}
/**
* Sorts {@code -1} → {@code -infinity}, then {@code +1} → {@code +infinity}
*/
protected static final Comparator byIdComparator = (o1, o2) -> {
final long i1 = o1.getUniqueId();
final long i2 = o2.getUniqueId();
if (i1 < 0 && i2 < 0) {
return Long.compare(i2, i1);
} else {
return Long.compare(i1, i2);
}
};
protected Collection sortById(Collection primitives) {
List result = new ArrayList<>(primitives.size());
result.addAll(primitives);
result.sort(byIdComparator);
return result;
}
public void writeLayer(OsmDataLayer layer) {
header(!layer.isUploadDiscouraged());
writeDataSources(layer.data);
writeContent(layer.data);
footer();
}
/**
* Writes the contents of the given dataset (nodes, then ways, then relations)
* @param ds The dataset to write
*/
public void writeContent(DataSet ds) {
writeNodes(ds.getNodes());
writeWays(ds.getWays());
writeRelations(ds.getRelations());
}
/**
* Writes the given nodes sorted by id
* @param nodes The nodes to write
* @since 5737
*/
public void writeNodes(Collection nodes) {
for (Node n : sortById(nodes)) {
if (shouldWrite(n)) {
visit(n);
}
}
}
/**
* Writes the given ways sorted by id
* @param ways The ways to write
* @since 5737
*/
public void writeWays(Collection ways) {
for (Way w : sortById(ways)) {
if (shouldWrite(w)) {
visit(w);
}
}
}
/**
* Writes the given relations sorted by id
* @param relations The relations to write
* @since 5737
*/
public void writeRelations(Collection relations) {
for (Relation r : sortById(relations)) {
if (shouldWrite(r)) {
visit(r);
}
}
}
protected boolean shouldWrite(OsmPrimitive osm) {
return !osm.isNewOrUndeleted() || !osm.isDeleted();
}
public void writeDataSources(DataSet ds) {
for (DataSource s : ds.dataSources) {
out.println(" ");
}
}
@Override
public void visit(INode n) {
if (n.isIncomplete()) return;
addCommon(n, "node");
if (!withBody) {
out.println("/>");
} else {
if (n.getCoor() != null) {
out.print(" lat='"+LatLon.cDdHighPecisionFormatter.format(n.getCoor().lat())+
"' lon='"+LatLon.cDdHighPecisionFormatter.format(n.getCoor().lon())+'\'');
}
addTags(n, "node", true);
}
}
@Override
public void visit(IWay w) {
if (w.isIncomplete()) return;
addCommon(w, "way");
if (!withBody) {
out.println("/>");
} else {
out.println(">");
for (int i = 0; i < w.getNodesCount(); ++i) {
out.println(" ");
}
addTags(w, "way", false);
}
}
@Override
public void visit(IRelation e) {
if (e.isIncomplete()) return;
addCommon(e, "relation");
if (!withBody) {
out.println("/>");
} else {
out.println(">");
for (int i = 0; i < e.getMembersCount(); ++i) {
out.print(" ");
}
addTags(e, "relation", false);
}
}
public void visit(Changeset cs) {
out.print(" ");
addTags(cs, "changeset", false); // also writes closing
}
protected static final Comparator> byKeyComparator = (o1, o2) -> o1.getKey().compareTo(o2.getKey());
protected void addTags(Tagged osm, String tagname, boolean tagOpen) {
if (osm.hasKeys()) {
if (tagOpen) {
out.println(">");
}
List> entries = new ArrayList<>(osm.getKeys().entrySet());
entries.sort(byKeyComparator);
for (Entry e : entries) {
out.println(" ");
}
out.println(" " + tagname + '>');
} else if (tagOpen) {
out.println(" />");
} else {
out.println(" " + tagname + '>');
}
}
/**
* Add the common part as the form of the tag as well as the XML attributes
* id, action, user, and visible.
* @param osm osm primitive
* @param tagname XML tag matching osm primitive (node, way, relation)
*/
protected void addCommon(IPrimitive osm, String tagname) {
out.print(" <"+tagname);
if (osm.getUniqueId() != 0) {
out.print(" id='"+ osm.getUniqueId()+'\'');
} else
throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found"));
if (!isOsmChange) {
if (!osmConform) {
String action = null;
if (osm.isDeleted()) {
action = "delete";
} else if (osm.isModified()) {
action = "modify";
}
if (action != null) {
out.print(" action='"+action+'\'');
}
}
if (!osm.isTimestampEmpty()) {
out.print(" timestamp='"+DateUtils.fromTimestamp(osm.getRawTimestamp())+'\'');
}
// user and visible added with 0.4 API
if (osm.getUser() != null) {
if (osm.getUser().isLocalUser()) {
out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\'');
} else if (osm.getUser().isOsmUser()) {
// uid added with 0.6
out.print(" uid='"+ osm.getUser().getId()+'\'');
out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+'\'');
}
}
out.print(" visible='"+osm.isVisible()+'\'');
}
if (osm.getVersion() != 0) {
out.print(" version='"+osm.getVersion()+'\'');
}
if (this.changeset != null && this.changeset.getId() != 0) {
out.print(" changeset='"+this.changeset.getId()+'\'');
} else if (osm.getChangesetId() > 0 && !osm.isNew()) {
out.print(" changeset='"+osm.getChangesetId()+'\'');
}
}
}