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