001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import java.io.StringReader; 005import java.io.StringWriter; 006import java.io.Writer; 007import java.math.BigDecimal; 008import java.math.RoundingMode; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashSet; 012import java.util.Iterator; 013import java.util.List; 014import java.util.Map; 015import java.util.Map.Entry; 016import java.util.Set; 017import java.util.stream.Stream; 018 019import javax.json.Json; 020import javax.json.JsonArrayBuilder; 021import javax.json.JsonObject; 022import javax.json.JsonObjectBuilder; 023import javax.json.JsonValue; 024import javax.json.JsonWriter; 025import javax.json.stream.JsonGenerator; 026import javax.json.stream.JsonParser; 027import javax.json.stream.JsonParsingException; 028 029import org.openstreetmap.josm.data.Bounds; 030import org.openstreetmap.josm.data.coor.EastNorth; 031import org.openstreetmap.josm.data.coor.LatLon; 032import org.openstreetmap.josm.data.osm.DataSet; 033import org.openstreetmap.josm.data.osm.MultipolygonBuilder; 034import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; 035import org.openstreetmap.josm.data.osm.Node; 036import org.openstreetmap.josm.data.osm.OsmPrimitive; 037import org.openstreetmap.josm.data.osm.Relation; 038import org.openstreetmap.josm.data.osm.Way; 039import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 040import org.openstreetmap.josm.data.preferences.BooleanProperty; 041import org.openstreetmap.josm.data.projection.Projection; 042import org.openstreetmap.josm.data.projection.Projections; 043import org.openstreetmap.josm.gui.mappaint.ElemStyles; 044import org.openstreetmap.josm.tools.Logging; 045import org.openstreetmap.josm.tools.Pair; 046 047/** 048 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). 049 * <p> 050 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a> 051 */ 052public class GeoJSONWriter { 053 054 private final DataSet data; 055 private final Projection projection; 056 private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true); 057 private static final BooleanProperty UNTAGGED_CLOSED_IS_POLYGON = new BooleanProperty("geojson.export.untagged-closed-is-polygon", false); 058 private static final Set<Way> processedMultipolygonWays = new HashSet<>(); 059 060 /** 061 * This is used to determine that a tag should be interpreted as a json 062 * object or array. The tag should have both {@link #JSON_VALUE_START_MARKER} 063 * and {@link #JSON_VALUE_END_MARKER}. 064 */ 065 static final String JSON_VALUE_START_MARKER = "{"; 066 /** 067 * This is used to determine that a tag should be interpreted as a json 068 * object or array. The tag should have both {@link #JSON_VALUE_START_MARKER} 069 * and {@link #JSON_VALUE_END_MARKER}. 070 */ 071 static final String JSON_VALUE_END_MARKER = "}"; 072 073 /** 074 * Constructs a new {@code GeoJSONWriter}. 075 * @param ds The OSM data set to save 076 * @since 12806 077 */ 078 public GeoJSONWriter(DataSet ds) { 079 this.data = ds; 080 this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 081 } 082 083 /** 084 * Writes OSM data as a GeoJSON string (prettified). 085 * @return The GeoJSON data 086 */ 087 public String write() { 088 return write(true); 089 } 090 091 /** 092 * Writes OSM data as a GeoJSON string (prettified or not). 093 * @param pretty {@code true} to have pretty output, {@code false} otherwise 094 * @return The GeoJSON data 095 * @since 6756 096 */ 097 public String write(boolean pretty) { 098 StringWriter stringWriter = new StringWriter(); 099 write(pretty, stringWriter); 100 return stringWriter.toString(); 101 } 102 103 /** 104 * Writes OSM data as a GeoJSON string (prettified or not). 105 * @param pretty {@code true} to have pretty output, {@code false} otherwise 106 * @param writer The writer used to write results 107 */ 108 public void write(boolean pretty, Writer writer) { 109 Map<String, Object> config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, pretty); 110 try (JsonWriter jsonWriter = Json.createWriterFactory(config).createWriter(writer)) { 111 JsonObjectBuilder object = Json.createObjectBuilder() 112 .add("type", "FeatureCollection") 113 .add("generator", "JOSM"); 114 appendLayerBounds(data, object); 115 appendLayerFeatures(data, object); 116 jsonWriter.writeObject(object.build()); 117 } 118 } 119 120 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor { 121 122 private final JsonObjectBuilder geomObj; 123 124 GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) { 125 this.geomObj = geomObj; 126 } 127 128 @Override 129 public void visit(Node n) { 130 geomObj.add("type", "Point"); 131 LatLon ll = n.getCoor(); 132 if (ll != null) { 133 geomObj.add("coordinates", getCoorArray(null, ll)); 134 } 135 } 136 137 @Override 138 public void visit(Way w) { 139 if (w != null) { 140 if (!w.isTagged() && processedMultipolygonWays.contains(w)) { 141 // no need to write this object again 142 return; 143 } 144 final JsonArrayBuilder array = getCoorsArray(w.getNodes()); 145 boolean writeAsPolygon = w.isClosed() && ((!w.isTagged() && UNTAGGED_CLOSED_IS_POLYGON.get()) 146 || ElemStyles.hasAreaElemStyle(w, false)); 147 if (writeAsPolygon) { 148 geomObj.add("type", "Polygon"); 149 geomObj.add("coordinates", Json.createArrayBuilder().add(array)); 150 } else { 151 geomObj.add("type", "LineString"); 152 geomObj.add("coordinates", array); 153 } 154 } 155 } 156 157 @Override 158 public void visit(Relation r) { 159 if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) { 160 return; 161 } 162 try { 163 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r); 164 final JsonArrayBuilder polygon = Json.createArrayBuilder(); 165 Stream.concat(mp.a.stream(), mp.b.stream()) 166 .map(p -> { 167 JsonArrayBuilder array = getCoorsArray(p.getNodes()); 168 LatLon ll = p.getNodes().get(0).getCoor(); 169 // since first node is not duplicated as last node 170 return ll != null ? array.add(getCoorArray(null, ll)) : array; 171 }) 172 .forEach(polygon::add); 173 geomObj.add("type", "MultiPolygon"); 174 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon); 175 geomObj.add("coordinates", multiPolygon); 176 processedMultipolygonWays.addAll(r.getMemberPrimitives(Way.class)); 177 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) { 178 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId()); 179 Logging.warn(ex); 180 } 181 } 182 183 private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) { 184 final JsonArrayBuilder builder = Json.createArrayBuilder(); 185 for (Node n : nodes) { 186 LatLon ll = n.getCoor(); 187 if (ll != null) { 188 builder.add(getCoorArray(null, ll)); 189 } 190 } 191 return builder; 192 } 193 } 194 195 private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) { 196 return getCoorArray(builder, projection.latlon2eastNorth(c)); 197 } 198 199 private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) { 200 return (builder != null ? builder : Json.createArrayBuilder()) 201 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP)) 202 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP)); 203 } 204 205 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) { 206 if (p.isIncomplete() || 207 (SKIP_EMPTY_NODES.get() && p instanceof Node && p.getKeys().isEmpty())) { 208 return; 209 } 210 211 // Properties 212 final JsonObjectBuilder propObj = Json.createObjectBuilder(); 213 for (Entry<String, String> t : p.getKeys().entrySet()) { 214 propObj.add(t.getKey(), convertValueToJson(t.getValue())); 215 } 216 final JsonObject prop = propObj.build(); 217 218 // Geometry 219 final JsonObjectBuilder geomObj = Json.createObjectBuilder(); 220 p.accept(new GeometryPrimitiveVisitor(geomObj)); 221 final JsonObject geom = geomObj.build(); 222 223 if (!geom.isEmpty()) { 224 // Build primitive JSON object 225 array.add(Json.createObjectBuilder() 226 .add("type", "Feature") 227 .add("properties", prop.isEmpty() ? JsonValue.NULL : prop) 228 .add("geometry", geom.isEmpty() ? JsonValue.NULL : geom)); 229 } 230 } 231 232 private static JsonValue convertValueToJson(String value) { 233 if (value.startsWith(JSON_VALUE_START_MARKER) && value.endsWith(JSON_VALUE_END_MARKER)) { 234 try (JsonParser parser = Json.createParser(new StringReader(value))) { 235 if (parser.hasNext() && parser.next() != null) { 236 return parser.getValue(); 237 } 238 } catch (JsonParsingException e) { 239 Logging.warn(e); 240 } 241 } 242 return Json.createValue(value); 243 } 244 245 protected void appendLayerBounds(DataSet ds, JsonObjectBuilder object) { 246 if (ds != null) { 247 Iterator<Bounds> it = ds.getDataSourceBounds().iterator(); 248 if (it.hasNext()) { 249 Bounds b = new Bounds(it.next()); 250 while (it.hasNext()) { 251 b.extend(it.next()); 252 } 253 appendBounds(b, object); 254 } 255 } 256 } 257 258 protected void appendBounds(Bounds b, JsonObjectBuilder object) { 259 if (b != null) { 260 JsonArrayBuilder builder = Json.createArrayBuilder(); 261 getCoorArray(builder, b.getMin()); 262 getCoorArray(builder, b.getMax()); 263 object.add("bbox", builder); 264 } 265 } 266 267 protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) { 268 JsonArrayBuilder array = Json.createArrayBuilder(); 269 if (ds != null) { 270 processedMultipolygonWays.clear(); 271 Collection<OsmPrimitive> primitives = ds.allNonDeletedPrimitives(); 272 // Relations first 273 for (OsmPrimitive p : primitives) { 274 if (p instanceof Relation) 275 appendPrimitive(p, array); 276 } 277 for (OsmPrimitive p : primitives) { 278 if (!(p instanceof Relation)) 279 appendPrimitive(p, array); 280 } 281 processedMultipolygonWays.clear(); 282 } 283 object.add("features", array); 284 } 285}