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