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.preferences.BooleanProperty; 034import org.openstreetmap.josm.data.projection.Projection; 035import org.openstreetmap.josm.data.projection.Projections; 036import org.openstreetmap.josm.gui.mappaint.ElemStyles; 037import org.openstreetmap.josm.tools.Logging; 038import org.openstreetmap.josm.tools.Pair; 039 040/** 041 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). 042 * <p> 043 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a> 044 */ 045public class GeoJSONWriter { 046 047 private final DataSet data; 048 private final Projection projection; 049 private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true); 050 051 /** 052 * Constructs a new {@code GeoJSONWriter}. 053 * @param ds The OSM data set to save 054 * @since 12806 055 */ 056 public GeoJSONWriter(DataSet ds) { 057 this.data = ds; 058 this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 059 } 060 061 /** 062 * Writes OSM data as a GeoJSON string (prettified). 063 * @return The GeoJSON data 064 */ 065 public String write() { 066 return write(true); 067 } 068 069 /** 070 * Writes OSM data as a GeoJSON string (prettified or not). 071 * @param pretty {@code true} to have pretty output, {@code false} otherwise 072 * @return The GeoJSON data 073 * @since 6756 074 */ 075 public String write(boolean pretty) { 076 StringWriter stringWriter = new StringWriter(); 077 Map<String, Object> config = new HashMap<>(1); 078 config.put(JsonGenerator.PRETTY_PRINTING, pretty); 079 try (JsonWriter writer = Json.createWriterFactory(config).createWriter(stringWriter)) { 080 JsonObjectBuilder object = Json.createObjectBuilder() 081 .add("type", "FeatureCollection") 082 .add("generator", "JOSM"); 083 appendLayerBounds(data, object); 084 appendLayerFeatures(data, object); 085 writer.writeObject(object.build()); 086 return stringWriter.toString(); 087 } 088 } 089 090 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor { 091 092 private final JsonObjectBuilder geomObj; 093 094 GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) { 095 this.geomObj = geomObj; 096 } 097 098 @Override 099 public void visit(Node n) { 100 geomObj.add("type", "Point"); 101 LatLon ll = n.getCoor(); 102 if (ll != null) { 103 geomObj.add("coordinates", getCoorArray(null, n.getCoor())); 104 } 105 } 106 107 @Override 108 public void visit(Way w) { 109 if (w != null) { 110 final JsonArrayBuilder array = getCoorsArray(w.getNodes()); 111 if (w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) { 112 final JsonArrayBuilder container = Json.createArrayBuilder().add(array); 113 geomObj.add("type", "Polygon"); 114 geomObj.add("coordinates", container); 115 } else { 116 geomObj.add("type", "LineString"); 117 geomObj.add("coordinates", array); 118 } 119 } 120 } 121 122 @Override 123 public void visit(Relation r) { 124 if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) { 125 return; 126 } 127 try { 128 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r); 129 final JsonArrayBuilder polygon = Json.createArrayBuilder(); 130 Stream.concat(mp.a.stream(), mp.b.stream()) 131 .map(p -> getCoorsArray(p.getNodes()) 132 // since first node is not duplicated as last node 133 .add(getCoorArray(null, p.getNodes().get(0).getCoor()))) 134 .forEach(polygon::add); 135 geomObj.add("type", "MultiPolygon"); 136 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon); 137 geomObj.add("coordinates", multiPolygon); 138 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) { 139 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId()); 140 Logging.warn(ex); 141 } 142 } 143 } 144 145 private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) { 146 return getCoorArray(builder, projection.latlon2eastNorth(c)); 147 } 148 149 private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) { 150 return (builder != null ? builder : Json.createArrayBuilder()) 151 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP)) 152 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP)); 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 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) { 167 if (p.isIncomplete() || 168 (SKIP_EMPTY_NODES.get() && 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}