source: josm/trunk/src/org/openstreetmap/josm/io/GeoJSONWriter.java@ 16936

Last change on this file since 16936 was 16936, checked in by simon04, 4 years ago

fix #19632 - GeoJSONWriter: write key={value} as JSON object (patch by taylor.smock, modified)

  • Property svn:eol-style set to native
File size: 10.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import java.io.StringReader;
5import java.io.StringWriter;
6import java.math.BigDecimal;
7import java.math.RoundingMode;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.HashSet;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Map;
14import java.util.Map.Entry;
15import java.util.Set;
16import java.util.stream.Stream;
17
18import javax.json.Json;
19import javax.json.JsonArrayBuilder;
20import javax.json.JsonObject;
21import javax.json.JsonObjectBuilder;
22import javax.json.JsonValue;
23import javax.json.JsonWriter;
24import javax.json.stream.JsonGenerator;
25import javax.json.stream.JsonParser;
26import javax.json.stream.JsonParsingException;
27
28import org.openstreetmap.josm.data.Bounds;
29import org.openstreetmap.josm.data.coor.EastNorth;
30import org.openstreetmap.josm.data.coor.LatLon;
31import org.openstreetmap.josm.data.osm.DataSet;
32import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
33import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
34import org.openstreetmap.josm.data.osm.Node;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.Way;
38import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
39import org.openstreetmap.josm.data.preferences.BooleanProperty;
40import org.openstreetmap.josm.data.projection.Projection;
41import org.openstreetmap.josm.data.projection.Projections;
42import org.openstreetmap.josm.gui.mappaint.ElemStyles;
43import org.openstreetmap.josm.tools.Logging;
44import org.openstreetmap.josm.tools.Pair;
45
46/**
47 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P).
48 * <p>
49 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a>
50 */
51public class GeoJSONWriter {
52
53 private final DataSet data;
54 private final Projection projection;
55 private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true);
56 private static final BooleanProperty UNTAGGED_CLOSED_IS_POLYGON = new BooleanProperty("geojson.export.untagged-closed-is-polygon", false);
57 private static final Set<Way> processedMultipolygonWays = new HashSet<>();
58
59 /**
60 * This is used to determine that a tag should be interpreted as a json
61 * object or array. The tag should have both {@link #JSON_VALUE_START_MARKER}
62 * and {@link #JSON_VALUE_END_MARKER}.
63 */
64 static final String JSON_VALUE_START_MARKER = "{";
65 /**
66 * This is used to determine that a tag should be interpreted as a json
67 * object or array. The tag should have both {@link #JSON_VALUE_START_MARKER}
68 * and {@link #JSON_VALUE_END_MARKER}.
69 */
70 static final String JSON_VALUE_END_MARKER = "}";
71
72 /**
73 * Constructs a new {@code GeoJSONWriter}.
74 * @param ds The OSM data set to save
75 * @since 12806
76 */
77 public GeoJSONWriter(DataSet ds) {
78 this.data = ds;
79 this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
80 }
81
82 /**
83 * Writes OSM data as a GeoJSON string (prettified).
84 * @return The GeoJSON data
85 */
86 public String write() {
87 return write(true);
88 }
89
90 /**
91 * Writes OSM data as a GeoJSON string (prettified or not).
92 * @param pretty {@code true} to have pretty output, {@code false} otherwise
93 * @return The GeoJSON data
94 * @since 6756
95 */
96 public String write(boolean pretty) {
97 StringWriter stringWriter = new StringWriter();
98 Map<String, Object> config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, pretty);
99 try (JsonWriter writer = Json.createWriterFactory(config).createWriter(stringWriter)) {
100 JsonObjectBuilder object = Json.createObjectBuilder()
101 .add("type", "FeatureCollection")
102 .add("generator", "JOSM");
103 appendLayerBounds(data, object);
104 appendLayerFeatures(data, object);
105 writer.writeObject(object.build());
106 return stringWriter.toString();
107 }
108 }
109
110 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor {
111
112 private final JsonObjectBuilder geomObj;
113
114 GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) {
115 this.geomObj = geomObj;
116 }
117
118 @Override
119 public void visit(Node n) {
120 geomObj.add("type", "Point");
121 LatLon ll = n.getCoor();
122 if (ll != null) {
123 geomObj.add("coordinates", getCoorArray(null, n.getCoor()));
124 }
125 }
126
127 @Override
128 public void visit(Way w) {
129 if (w != null) {
130 if (!w.isTagged() && processedMultipolygonWays.contains(w)) {
131 // no need to write this object again
132 return;
133 }
134 final JsonArrayBuilder array = getCoorsArray(w.getNodes());
135 boolean writeAsPolygon = w.isClosed() && ((!w.isTagged() && UNTAGGED_CLOSED_IS_POLYGON.get())
136 || ElemStyles.hasAreaElemStyle(w, false));
137 if (writeAsPolygon) {
138 geomObj.add("type", "Polygon");
139 geomObj.add("coordinates", Json.createArrayBuilder().add(array));
140 } else {
141 geomObj.add("type", "LineString");
142 geomObj.add("coordinates", array);
143 }
144 }
145 }
146
147 @Override
148 public void visit(Relation r) {
149 if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) {
150 return;
151 }
152 try {
153 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r);
154 final JsonArrayBuilder polygon = Json.createArrayBuilder();
155 Stream.concat(mp.a.stream(), mp.b.stream())
156 .map(p -> getCoorsArray(p.getNodes())
157 // since first node is not duplicated as last node
158 .add(getCoorArray(null, p.getNodes().get(0).getCoor())))
159 .forEach(polygon::add);
160 geomObj.add("type", "MultiPolygon");
161 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
162 geomObj.add("coordinates", multiPolygon);
163 processedMultipolygonWays.addAll(r.getMemberPrimitives(Way.class));
164 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
165 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId());
166 Logging.warn(ex);
167 }
168 }
169
170 private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) {
171 final JsonArrayBuilder builder = Json.createArrayBuilder();
172 for (Node n : nodes) {
173 LatLon ll = n.getCoor();
174 if (ll != null) {
175 builder.add(getCoorArray(null, ll));
176 }
177 }
178 return builder;
179 }
180 }
181
182 private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
183 return getCoorArray(builder, projection.latlon2eastNorth(c));
184 }
185
186 private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) {
187 return (builder != null ? builder : Json.createArrayBuilder())
188 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP))
189 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP));
190 }
191
192 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
193 if (p.isIncomplete() ||
194 (SKIP_EMPTY_NODES.get() && p instanceof Node && p.getKeys().isEmpty())) {
195 return;
196 }
197
198 // Properties
199 final JsonObjectBuilder propObj = Json.createObjectBuilder();
200 for (Entry<String, String> t : p.getKeys().entrySet()) {
201 propObj.add(t.getKey(), convertValueToJson(t.getValue()));
202 }
203 final JsonObject prop = propObj.build();
204
205 // Geometry
206 final JsonObjectBuilder geomObj = Json.createObjectBuilder();
207 p.accept(new GeometryPrimitiveVisitor(geomObj));
208 final JsonObject geom = geomObj.build();
209
210 if (!geom.isEmpty()) {
211 // Build primitive JSON object
212 array.add(Json.createObjectBuilder()
213 .add("type", "Feature")
214 .add("properties", prop.isEmpty() ? JsonValue.NULL : prop)
215 .add("geometry", geom.isEmpty() ? JsonValue.NULL : geom));
216 }
217 }
218
219 private static JsonValue convertValueToJson(String value) {
220 if (value.startsWith(JSON_VALUE_START_MARKER) && value.endsWith(JSON_VALUE_END_MARKER)) {
221 try (JsonParser parser = Json.createParser(new StringReader(value))) {
222 if (parser.hasNext() && parser.next() != null) {
223 return parser.getValue();
224 }
225 } catch (JsonParsingException e) {
226 Logging.warn(e);
227 }
228 }
229 return Json.createValue(value);
230 }
231
232 protected void appendLayerBounds(DataSet ds, JsonObjectBuilder object) {
233 if (ds != null) {
234 Iterator<Bounds> it = ds.getDataSourceBounds().iterator();
235 if (it.hasNext()) {
236 Bounds b = new Bounds(it.next());
237 while (it.hasNext()) {
238 b.extend(it.next());
239 }
240 appendBounds(b, object);
241 }
242 }
243 }
244
245 protected void appendBounds(Bounds b, JsonObjectBuilder object) {
246 if (b != null) {
247 JsonArrayBuilder builder = Json.createArrayBuilder();
248 getCoorArray(builder, b.getMin());
249 getCoorArray(builder, b.getMax());
250 object.add("bbox", builder);
251 }
252 }
253
254 protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) {
255 JsonArrayBuilder array = Json.createArrayBuilder();
256 if (ds != null) {
257 processedMultipolygonWays.clear();
258 Collection<OsmPrimitive> primitives = ds.allNonDeletedPrimitives();
259 // Relations first
260 for (OsmPrimitive p : primitives) {
261 if (p instanceof Relation)
262 appendPrimitive(p, array);
263 }
264 for (OsmPrimitive p : primitives) {
265 if (!(p instanceof Relation))
266 appendPrimitive(p, array);
267 }
268 processedMultipolygonWays.clear();
269 }
270 object.add("features", array);
271 }
272}
Note: See TracBrowser for help on using the repository browser.