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

Last change on this file since 17320 was 17320, checked in by Don-vip, 3 years ago

fix #20066 - NPE with nodes without coordinates

  • Property svn:eol-style set to native
File size: 10.6 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, ll));
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 -> {
157 JsonArrayBuilder array = getCoorsArray(p.getNodes());
158 LatLon ll = p.getNodes().get(0).getCoor();
159 // since first node is not duplicated as last node
160 return ll != null ? array.add(getCoorArray(null, ll)) : array;
161 })
162 .forEach(polygon::add);
163 geomObj.add("type", "MultiPolygon");
164 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
165 geomObj.add("coordinates", multiPolygon);
166 processedMultipolygonWays.addAll(r.getMemberPrimitives(Way.class));
167 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
168 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId());
169 Logging.warn(ex);
170 }
171 }
172
173 private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) {
174 final JsonArrayBuilder builder = Json.createArrayBuilder();
175 for (Node n : nodes) {
176 LatLon ll = n.getCoor();
177 if (ll != null) {
178 builder.add(getCoorArray(null, ll));
179 }
180 }
181 return builder;
182 }
183 }
184
185 private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
186 return getCoorArray(builder, projection.latlon2eastNorth(c));
187 }
188
189 private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) {
190 return (builder != null ? builder : Json.createArrayBuilder())
191 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP))
192 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP));
193 }
194
195 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
196 if (p.isIncomplete() ||
197 (SKIP_EMPTY_NODES.get() && p instanceof Node && p.getKeys().isEmpty())) {
198 return;
199 }
200
201 // Properties
202 final JsonObjectBuilder propObj = Json.createObjectBuilder();
203 for (Entry<String, String> t : p.getKeys().entrySet()) {
204 propObj.add(t.getKey(), convertValueToJson(t.getValue()));
205 }
206 final JsonObject prop = propObj.build();
207
208 // Geometry
209 final JsonObjectBuilder geomObj = Json.createObjectBuilder();
210 p.accept(new GeometryPrimitiveVisitor(geomObj));
211 final JsonObject geom = geomObj.build();
212
213 if (!geom.isEmpty()) {
214 // Build primitive JSON object
215 array.add(Json.createObjectBuilder()
216 .add("type", "Feature")
217 .add("properties", prop.isEmpty() ? JsonValue.NULL : prop)
218 .add("geometry", geom.isEmpty() ? JsonValue.NULL : geom));
219 }
220 }
221
222 private static JsonValue convertValueToJson(String value) {
223 if (value.startsWith(JSON_VALUE_START_MARKER) && value.endsWith(JSON_VALUE_END_MARKER)) {
224 try (JsonParser parser = Json.createParser(new StringReader(value))) {
225 if (parser.hasNext() && parser.next() != null) {
226 return parser.getValue();
227 }
228 } catch (JsonParsingException e) {
229 Logging.warn(e);
230 }
231 }
232 return Json.createValue(value);
233 }
234
235 protected void appendLayerBounds(DataSet ds, JsonObjectBuilder object) {
236 if (ds != null) {
237 Iterator<Bounds> it = ds.getDataSourceBounds().iterator();
238 if (it.hasNext()) {
239 Bounds b = new Bounds(it.next());
240 while (it.hasNext()) {
241 b.extend(it.next());
242 }
243 appendBounds(b, object);
244 }
245 }
246 }
247
248 protected void appendBounds(Bounds b, JsonObjectBuilder object) {
249 if (b != null) {
250 JsonArrayBuilder builder = Json.createArrayBuilder();
251 getCoorArray(builder, b.getMin());
252 getCoorArray(builder, b.getMax());
253 object.add("bbox", builder);
254 }
255 }
256
257 protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) {
258 JsonArrayBuilder array = Json.createArrayBuilder();
259 if (ds != null) {
260 processedMultipolygonWays.clear();
261 Collection<OsmPrimitive> primitives = ds.allNonDeletedPrimitives();
262 // Relations first
263 for (OsmPrimitive p : primitives) {
264 if (p instanceof Relation)
265 appendPrimitive(p, array);
266 }
267 for (OsmPrimitive p : primitives) {
268 if (!(p instanceof Relation))
269 appendPrimitive(p, array);
270 }
271 processedMultipolygonWays.clear();
272 }
273 object.add("features", array);
274 }
275}
Note: See TracBrowser for help on using the repository browser.