001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import java.io.StringWriter; 005import java.math.BigDecimal; 006import java.math.RoundingMode; 007import java.util.HashMap; 008import java.util.Iterator; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.stream.Stream; 013 014import javax.json.Json; 015import javax.json.JsonArrayBuilder; 016import javax.json.JsonObject; 017import javax.json.JsonObjectBuilder; 018import javax.json.JsonValue; 019import javax.json.JsonWriter; 020import javax.json.stream.JsonGenerator; 021 022import org.openstreetmap.josm.data.Bounds; 023import org.openstreetmap.josm.data.coor.EastNorth; 024import org.openstreetmap.josm.data.coor.LatLon; 025import org.openstreetmap.josm.data.osm.DataSet; 026import org.openstreetmap.josm.data.osm.MultipolygonBuilder; 027import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; 028import org.openstreetmap.josm.data.osm.Node; 029import org.openstreetmap.josm.data.osm.OsmPrimitive; 030import org.openstreetmap.josm.data.osm.Relation; 031import org.openstreetmap.josm.data.osm.Way; 032import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 033import org.openstreetmap.josm.data.projection.Projection; 034import org.openstreetmap.josm.data.projection.Projections; 035import org.openstreetmap.josm.gui.mappaint.ElemStyles; 036import org.openstreetmap.josm.tools.Logging; 037import org.openstreetmap.josm.tools.Pair; 038 039/** 040 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). 041 * <p> 042 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a> 043 */ 044public class GeoJSONWriter { 045 046 private final DataSet data; 047 private final Projection projection; 048 private static final boolean SKIP_EMPTY_NODES = true; 049 050 /** 051 * Constructs a new {@code GeoJSONWriter}. 052 * @param ds The OSM data set to save 053 * @since 12806 054 */ 055 public GeoJSONWriter(DataSet ds) { 056 this.data = ds; 057 this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 058 } 059 060 /** 061 * Writes OSM data as a GeoJSON string (prettified). 062 * @return The GeoJSON data 063 */ 064 public String write() { 065 return write(true); 066 } 067 068 /** 069 * Writes OSM data as a GeoJSON string (prettified or not). 070 * @param pretty {@code true} to have pretty output, {@code false} otherwise 071 * @return The GeoJSON data 072 * @since 6756 073 */ 074 public String write(boolean pretty) { 075 StringWriter stringWriter = new StringWriter(); 076 Map<String, Object> config = new HashMap<>(1); 077 config.put(JsonGenerator.PRETTY_PRINTING, pretty); 078 try (JsonWriter writer = Json.createWriterFactory(config).createWriter(stringWriter)) { 079 JsonObjectBuilder object = Json.createObjectBuilder() 080 .add("type", "FeatureCollection") 081 .add("generator", "JOSM"); 082 appendLayerBounds(data, object); 083 appendLayerFeatures(data, object); 084 writer.writeObject(object.build()); 085 return stringWriter.toString(); 086 } 087 } 088 089 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor { 090 091 private final JsonObjectBuilder geomObj; 092 093 GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) { 094 this.geomObj = geomObj; 095 } 096 097 @Override 098 public void visit(Node n) { 099 geomObj.add("type", "Point"); 100 LatLon ll = n.getCoor(); 101 if (ll != null) { 102 geomObj.add("coordinates", getCoorArray(null, n.getCoor())); 103 } 104 } 105 106 @Override 107 public void visit(Way w) { 108 if (w != null) { 109 final JsonArrayBuilder array = getCoorsArray(w.getNodes()); 110 if (w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) { 111 final JsonArrayBuilder container = Json.createArrayBuilder().add(array); 112 geomObj.add("type", "Polygon"); 113 geomObj.add("coordinates", container); 114 } else { 115 geomObj.add("type", "LineString"); 116 geomObj.add("coordinates", array); 117 } 118 } 119 } 120 121 @Override 122 public void visit(Relation r) { 123 if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) { 124 return; 125 } 126 try { 127 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r); 128 final JsonArrayBuilder polygon = Json.createArrayBuilder(); 129 Stream.concat(mp.a.stream(), mp.b.stream()) 130 .map(p -> getCoorsArray(p.getNodes()) 131 // since first node is not duplicated as last node 132 .add(getCoorArray(null, p.getNodes().get(0).getCoor()))) 133 .forEach(polygon::add); 134 geomObj.add("type", "MultiPolygon"); 135 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon); 136 geomObj.add("coordinates", multiPolygon); 137 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) { 138 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId()); 139 Logging.warn(ex); 140 } 141 } 142 } 143 144 private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) { 145 return getCoorArray(builder, projection.latlon2eastNorth(c)); 146 } 147 148 private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) { 149 return (builder != null ? builder : Json.createArrayBuilder()) 150 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP)) 151 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP)); 152 } 153 154 private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) { 155 final JsonArrayBuilder builder = Json.createArrayBuilder(); 156 for (Node n : nodes) { 157 LatLon ll = n.getCoor(); 158 if (ll != null) { 159 builder.add(getCoorArray(null, ll)); 160 } 161 } 162 return builder; 163 } 164 165 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) { 166 if (p.isIncomplete()) { 167 return; 168 } else if (SKIP_EMPTY_NODES && p instanceof Node && p.getKeys().isEmpty()) { 169 return; 170 } 171 172 // Properties 173 final JsonObjectBuilder propObj = Json.createObjectBuilder(); 174 for (Entry<String, String> t : p.getKeys().entrySet()) { 175 propObj.add(t.getKey(), t.getValue()); 176 } 177 final JsonObject prop = propObj.build(); 178 179 // Geometry 180 final JsonObjectBuilder geomObj = Json.createObjectBuilder(); 181 p.accept(new GeometryPrimitiveVisitor(geomObj)); 182 final JsonObject geom = geomObj.build(); 183 184 // Build primitive JSON object 185 array.add(Json.createObjectBuilder() 186 .add("type", "Feature") 187 .add("properties", prop.isEmpty() ? JsonValue.NULL : prop) 188 .add("geometry", geom.isEmpty() ? JsonValue.NULL : geom)); 189 } 190 191 protected void appendLayerBounds(DataSet ds, JsonObjectBuilder object) { 192 if (ds != null) { 193 Iterator<Bounds> it = ds.getDataSourceBounds().iterator(); 194 if (it.hasNext()) { 195 Bounds b = new Bounds(it.next()); 196 while (it.hasNext()) { 197 b.extend(it.next()); 198 } 199 appendBounds(b, object); 200 } 201 } 202 } 203 204 protected void appendBounds(Bounds b, JsonObjectBuilder object) { 205 if (b != null) { 206 JsonArrayBuilder builder = Json.createArrayBuilder(); 207 getCoorArray(builder, b.getMin()); 208 getCoorArray(builder, b.getMax()); 209 object.add("bbox", builder); 210 } 211 } 212 213 protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) { 214 JsonArrayBuilder array = Json.createArrayBuilder(); 215 if (ds != null) { 216 ds.allNonDeletedPrimitives().forEach(p -> appendPrimitive(p, array)); 217 } 218 object.add("features", array); 219 } 220}