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