source: josm/trunk/src/org/openstreetmap/josm/io/GeoJSONReader.java@ 19010

Last change on this file since 19010 was 18723, checked in by taylor.smock, 17 months ago

Fix #22432, see #22941: Start migrating from javax to jakarta

Parsson was split out from the JSONP repository in 2021 (see
https://github.com/jakartaee/jsonp-api/issues/285 ). It is the default provider,
and will "just work" without additional configuration.

Many plugins use javax.json, so the scheduled removal of the javax.json
dependencies is set to milestone:"24.12" (see #22941).

Changes between javax.json and jakarta.json 2.0:

  • Rename of javax.json to jakarta.json
  • Some additional bug fixes

This will enable us to move easily to jakarta.json 2.1 in the future.
The changes of note with 2.1 includes:

  • Better handling of duplicated keys
  • Additional APIs around primitive types
  • API to get current event from JsonParser

We cannot currently move to jakarta.json 2.1 since it requires Java 11+.

  • Property svn:eol-style set to native
File size: 23.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedInputStream;
7import java.io.BufferedReader;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.InputStreamReader;
11import java.io.StringReader;
12import java.nio.charset.StandardCharsets;
13import java.util.ArrayList;
14import java.util.Collections;
15import java.util.List;
16import java.util.Map;
17import java.util.Objects;
18import java.util.Optional;
19import java.util.stream.Collectors;
20
21import jakarta.json.Json;
22import jakarta.json.JsonArray;
23import jakarta.json.JsonNumber;
24import jakarta.json.JsonObject;
25import jakarta.json.JsonString;
26import jakarta.json.JsonValue;
27import jakarta.json.stream.JsonParser;
28import jakarta.json.stream.JsonParser.Event;
29import jakarta.json.stream.JsonParsingException;
30
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.coor.LatLon;
33import org.openstreetmap.josm.data.osm.BBox;
34import org.openstreetmap.josm.data.osm.DataSet;
35import org.openstreetmap.josm.data.osm.Node;
36import org.openstreetmap.josm.data.osm.OsmPrimitive;
37import org.openstreetmap.josm.data.osm.Relation;
38import org.openstreetmap.josm.data.osm.RelationMember;
39import org.openstreetmap.josm.data.osm.Tag;
40import org.openstreetmap.josm.data.osm.TagCollection;
41import org.openstreetmap.josm.data.osm.TagMap;
42import org.openstreetmap.josm.data.osm.UploadPolicy;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.data.projection.Projection;
45import org.openstreetmap.josm.data.projection.Projections;
46import org.openstreetmap.josm.data.validation.TestError;
47import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
48import org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil;
49import org.openstreetmap.josm.gui.conflict.tags.TagConflictResolverModel;
50import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
51import org.openstreetmap.josm.gui.progress.ProgressMonitor;
52import org.openstreetmap.josm.tools.CheckParameterUtil;
53import org.openstreetmap.josm.tools.Logging;
54import org.openstreetmap.josm.tools.Utils;
55
56/**
57 * Reader that reads GeoJSON files. See <a href="https://tools.ietf.org/html/rfc7946">RFC7946</a> for more information.
58 * @since 15424
59 */
60public class GeoJSONReader extends AbstractReader {
61
62 private static final String CRS = "crs";
63 private static final String NAME = "name";
64 private static final String LINK = "link";
65 private static final String COORDINATES = "coordinates";
66 private static final String FEATURES = "features";
67 private static final String PROPERTIES = "properties";
68 private static final String GEOMETRY = "geometry";
69 private static final String TYPE = "type";
70 /** The record separator is 0x1E per RFC 7464 */
71 private static final byte RECORD_SEPARATOR_BYTE = 0x1E;
72 /**
73 * WGS 84 is the specified CRS for geojson -- alternate coordinate systems are considered to be deprecated from
74 * GJ2008.
75 */
76 private static final String CRS_GEOJSON = "EPSG:4326";
77 private Projection projection = Projections.getProjectionByCode(CRS_GEOJSON); // WGS 84
78
79 GeoJSONReader() {
80 // Restricts visibility
81 }
82
83 private void parse(final JsonParser parser) throws IllegalDataException {
84 while (parser.hasNext()) {
85 Event event = parser.next();
86 if (event == Event.START_OBJECT) {
87 parseRoot(parser.getObject());
88 }
89 }
90 parser.close();
91 }
92
93 private void parseRoot(final JsonObject object) throws IllegalDataException {
94 parseCrs(object.getJsonObject(CRS));
95 switch (Optional.ofNullable(object.getJsonString(TYPE))
96 .orElseThrow(() -> new IllegalDataException("No type")).getString()) {
97 case "FeatureCollection":
98 JsonValue.ValueType valueType = object.get(FEATURES).getValueType();
99 CheckParameterUtil.ensureThat(valueType == JsonValue.ValueType.ARRAY, "features must be ARRAY, but is " + valueType);
100 parseFeatureCollection(object.getJsonArray(FEATURES), false);
101 break;
102 case "Feature":
103 parseFeature(object);
104 break;
105 case "GeometryCollection":
106 parseGeometryCollection(null, object, false);
107 break;
108 default:
109 parseGeometry(null, object);
110 }
111 }
112
113 /**
114 * Parse CRS as per <a href="https://geojson.org/geojson-spec.html#coordinate-reference-system-objects">
115 * https://geojson.org/geojson-spec.html#coordinate-reference-system-objects</a>.
116 * CRS are obsolete in RFC7946 but still allowed for interoperability with older applications.
117 * Only named CRS are supported.
118 *
119 * @param crs CRS JSON object
120 * @throws IllegalDataException in case of error
121 */
122 private void parseCrs(final JsonObject crs) throws IllegalDataException {
123 if (crs != null) {
124 // Inspired by https://github.com/JOSM/geojson/commit/f13ceed4645244612a63581c96e20da802779c56
125 JsonObject properties = crs.getJsonObject(PROPERTIES);
126 if (properties != null) {
127 switch (crs.getString(TYPE)) {
128 case NAME:
129 String crsName = properties.getString(NAME);
130 if ("urn:ogc:def:crs:OGC:1.3:CRS84".equals(crsName)) {
131 // https://osgeo-org.atlassian.net/browse/GEOT-1710
132 crsName = CRS_GEOJSON;
133 } else if (crsName.startsWith("urn:ogc:def:crs:EPSG:")) {
134 crsName = crsName.replace("urn:ogc:def:crs:", "");
135 }
136 projection = Optional.ofNullable(Projections.getProjectionByCode(crsName))
137 .orElse(Projections.getProjectionByCode(CRS_GEOJSON)); // WGS84
138 break;
139 case LINK: // Not supported (security risk)
140 default:
141 throw new IllegalDataException(crs.toString());
142 }
143 }
144 }
145 }
146
147 private Optional<? extends OsmPrimitive> parseFeatureCollection(final JsonArray features, boolean createRelation) {
148 List<OsmPrimitive> primitives = features.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
149 .map(this::parseFeature).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
150 if (createRelation && primitives.size() > 1) {
151 Relation relation = new Relation();
152 relation.setMembers(primitives.stream().map(osm -> new RelationMember("", osm)).collect(Collectors.toList()));
153 getDataSet().addPrimitive(relation);
154 return Optional.of(relation);
155 } else if (primitives.size() == 1) {
156 return Optional.of(primitives.get(0));
157 }
158 return Optional.empty();
159 }
160
161 private Optional<? extends OsmPrimitive> parseFeature(final JsonObject feature) {
162 JsonValue geometry = feature.get(GEOMETRY);
163 if (geometry != null && geometry.getValueType() == JsonValue.ValueType.OBJECT) {
164 return parseGeometry(feature, geometry.asJsonObject());
165 } else {
166 JsonValue properties = feature.get(PROPERTIES);
167 if (properties != null && properties.getValueType() == JsonValue.ValueType.OBJECT) {
168 return parseNonGeometryFeature(feature, properties.asJsonObject());
169 } else {
170 Logging.warn(tr("Relation/non-geometry feature without properties found: {0}", feature));
171 }
172 }
173 return Optional.empty();
174 }
175
176 private Optional<? extends OsmPrimitive> parseNonGeometryFeature(final JsonObject feature, final JsonObject properties) {
177 // get relation type
178 JsonValue type = properties.get(TYPE);
179 if (type == null || properties.getValueType() == JsonValue.ValueType.STRING) {
180 Logging.warn(tr("Relation/non-geometry feature without type found: {0}", feature));
181 if (!feature.containsKey(FEATURES)) {
182 return Optional.empty();
183 }
184 }
185
186 // create misc. non-geometry feature
187 OsmPrimitive primitive = null;
188 if (feature.containsKey(FEATURES) && feature.get(FEATURES).getValueType() == JsonValue.ValueType.ARRAY) {
189 Optional<? extends OsmPrimitive> osm = parseFeatureCollection(feature.getJsonArray(FEATURES), true);
190 if (osm.isPresent()) {
191 primitive = osm.get();
192 fillTagsFromFeature(feature, primitive);
193 }
194 }
195 return Optional.ofNullable(primitive);
196 }
197
198 private Optional<Relation> parseGeometryCollection(final JsonObject feature, final JsonObject geometry, boolean createRelation) {
199 List<RelationMember> relationMembers = new ArrayList<>(geometry.getJsonArray("geometries").size());
200 for (JsonValue jsonValue : geometry.getJsonArray("geometries")) {
201 parseGeometry(feature, jsonValue.asJsonObject()).map(osm -> new RelationMember("", osm)).ifPresent(relationMembers::add);
202 }
203 if (createRelation) {
204 Relation relation = new Relation();
205 relation.setMembers(relationMembers);
206 getDataSet().addPrimitive(relation);
207 return Optional.of(fillTagsFromFeature(feature, relation));
208 }
209 return Optional.empty();
210 }
211
212 private Optional<? extends OsmPrimitive> parseGeometry(final JsonObject feature, final JsonObject geometry) {
213 if (geometry == null) {
214 parseNullGeometry(feature);
215 return Optional.empty();
216 }
217
218 switch (geometry.getString(TYPE)) {
219 case "Point":
220 return parsePoint(feature, geometry.getJsonArray(COORDINATES));
221 case "MultiPoint":
222 return parseMultiPoint(feature, geometry);
223 case "LineString":
224 return parseLineString(feature, geometry.getJsonArray(COORDINATES));
225 case "MultiLineString":
226 return parseMultiLineString(feature, geometry);
227 case "Polygon":
228 return parsePolygon(feature, geometry.getJsonArray(COORDINATES));
229 case "MultiPolygon":
230 return parseMultiPolygon(feature, geometry);
231 case "GeometryCollection":
232 return parseGeometryCollection(feature, geometry, true);
233 default:
234 parseUnknown(geometry);
235 return Optional.empty();
236 }
237 }
238
239 private LatLon getLatLon(final JsonArray coordinates) {
240 return projection.eastNorth2latlon(new EastNorth(
241 parseCoordinate(coordinates.get(0)),
242 parseCoordinate(coordinates.get(1))));
243 }
244
245 private static double parseCoordinate(JsonValue coordinate) {
246 if (coordinate instanceof JsonString) {
247 return Double.parseDouble(((JsonString) coordinate).getString());
248 } else if (coordinate instanceof JsonNumber) {
249 return ((JsonNumber) coordinate).doubleValue();
250 } else {
251 throw new IllegalArgumentException(Objects.toString(coordinate));
252 }
253 }
254
255 private Optional<Node> parsePoint(final JsonObject feature, final JsonArray coordinates) {
256 return Optional.of(fillTagsFromFeature(feature, createNode(getLatLon(coordinates))));
257 }
258
259 private Optional<Relation> parseMultiPoint(final JsonObject feature, final JsonObject geometry) {
260 List<RelationMember> nodes = new ArrayList<>(geometry.getJsonArray(COORDINATES).size());
261 for (JsonValue coordinate : geometry.getJsonArray(COORDINATES)) {
262 parsePoint(feature, coordinate.asJsonArray()).map(node -> new RelationMember("", node)).ifPresent(nodes::add);
263 }
264 Relation returnRelation = new Relation();
265 returnRelation.setMembers(nodes);
266 getDataSet().addPrimitive(returnRelation);
267 return Optional.of(fillTagsFromFeature(feature, returnRelation));
268 }
269
270 private Optional<Way> parseLineString(final JsonObject feature, final JsonArray coordinates) {
271 if (!coordinates.isEmpty()) {
272 Optional<Way> way = createWay(coordinates, false);
273 way.ifPresent(tWay -> fillTagsFromFeature(feature, tWay));
274 return way;
275 }
276 return Optional.empty();
277 }
278
279 private Optional<Relation> parseMultiLineString(final JsonObject feature, final JsonObject geometry) {
280 final List<RelationMember> ways = new ArrayList<>(geometry.getJsonArray(COORDINATES).size());
281 for (JsonValue coordinate : geometry.getJsonArray(COORDINATES)) {
282 parseLineString(feature, coordinate.asJsonArray()).map(way -> new RelationMember("", way)).ifPresent(ways::add);
283 }
284 final Relation relation = new Relation();
285 relation.setMembers(ways);
286 getDataSet().addPrimitive(relation);
287 return Optional.of(fillTagsFromFeature(feature, relation));
288 }
289
290 private Optional<? extends OsmPrimitive> parsePolygon(final JsonObject feature, final JsonArray coordinates) {
291 final int size = coordinates.size();
292 if (size == 1) {
293 Optional<Way> optionalWay = createWay(coordinates.getJsonArray(0), true);
294 optionalWay.ifPresent(way -> fillTagsFromFeature(feature, way));
295 return optionalWay;
296 } else if (size > 1) {
297 // create multipolygon
298 final Relation multipolygon = new Relation();
299 createWay(coordinates.getJsonArray(0), true)
300 .ifPresent(way -> multipolygon.addMember(new RelationMember("outer", way)));
301
302 for (JsonValue interiorRing : coordinates.subList(1, size)) {
303 createWay(interiorRing.asJsonArray(), true)
304 .ifPresent(way -> multipolygon.addMember(new RelationMember("inner", way)));
305 }
306
307 fillTagsFromFeature(feature, multipolygon);
308 multipolygon.put(TYPE, "multipolygon");
309 getDataSet().addPrimitive(multipolygon);
310 return Optional.of(multipolygon);
311 }
312 return Optional.empty();
313 }
314
315 private Optional<Relation> parseMultiPolygon(final JsonObject feature, final JsonObject geometry) {
316 List<RelationMember> relationMembers = new ArrayList<>(geometry.getJsonArray(COORDINATES).size());
317 for (JsonValue coordinate : geometry.getJsonArray(COORDINATES)) {
318 parsePolygon(feature, coordinate.asJsonArray()).map(poly -> new RelationMember("", poly)).ifPresent(relationMembers::add);
319 }
320 Relation relation = new Relation();
321 relation.setMembers(relationMembers);
322 return Optional.of(fillTagsFromFeature(feature, relation));
323 }
324
325 private Node createNode(final LatLon latlon) {
326 final List<Node> existingNodes = getDataSet().searchNodes(new BBox(latlon, latlon));
327 if (!existingNodes.isEmpty()) {
328 // reuse existing node, avoid multiple nodes on top of each other
329 return existingNodes.get(0);
330 }
331 final Node node = new Node(latlon);
332 getDataSet().addPrimitive(node);
333 return node;
334 }
335
336 private Optional<Way> createWay(final JsonArray coordinates, final boolean autoClose) {
337 if (coordinates.isEmpty()) {
338 return Optional.empty();
339 }
340
341 final List<LatLon> latlons = coordinates.stream()
342 .map(coordinate -> getLatLon(coordinate.asJsonArray()))
343 .collect(Collectors.toList());
344
345 final int size = latlons.size();
346 final boolean doAutoclose;
347 if (size > 1) {
348 if (latlons.get(0).equals(latlons.get(size - 1))) {
349 doAutoclose = false; // already closed
350 } else {
351 doAutoclose = autoClose;
352 }
353 } else {
354 doAutoclose = false;
355 }
356
357 final Way way = new Way();
358 getDataSet().addPrimitive(way);
359 final List<Node> rawNodes = latlons.stream().map(this::createNode).collect(Collectors.toList());
360 if (doAutoclose) {
361 rawNodes.add(rawNodes.get(0));
362 }
363 // see #19833: remove duplicated references to the same node
364 final List<Node> wayNodes = new ArrayList<>(rawNodes.size());
365 Node last = null;
366 for (Node curr : rawNodes) {
367 if (last != curr)
368 wayNodes.add(curr);
369 last = curr;
370 }
371 way.setNodes(wayNodes);
372
373 return Optional.of(way);
374 }
375
376 /**
377 * Merge existing tags in primitive (if any) with the values given in the GeoJSON feature.
378 * @param feature the GeoJSON feature
379 * @param primitive the OSM primitive
380 * @param <O> The primitive type
381 * @return The primitive passed in as {@code primitive} for easier chaining
382 */
383 private static <O extends OsmPrimitive> O fillTagsFromFeature(final JsonObject feature, final O primitive) {
384 if (feature != null) {
385 TagCollection featureTags = getTags(feature);
386 primitive.setKeys(new TagMap(primitive.isTagged() ? mergeAllTagValues(primitive, featureTags) : featureTags));
387 }
388 return primitive;
389 }
390
391 private static TagCollection mergeAllTagValues(final OsmPrimitive primitive, TagCollection featureTags) {
392 TagCollection tags = TagCollection.from(primitive).union(featureTags);
393 TagConflictResolutionUtil.applyAutomaticTagConflictResolution(tags);
394 TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(tags, Collections.singletonList(primitive));
395 TagConflictResolverModel tagModel = new TagConflictResolverModel();
396 tagModel.populate(new TagCollection(tags), tags.getKeysWithMultipleValues());
397 tagModel.actOnDecisions((k, d) -> d.keepAll());
398 return tagModel.getAllResolutions();
399 }
400
401 private static void parseUnknown(final JsonObject object) {
402 Logging.warn(tr("Unknown json object found {0}", object));
403 }
404
405 private static void parseNullGeometry(JsonObject feature) {
406 Logging.warn(tr("Geometry of feature {0} is null", feature));
407 }
408
409 private static TagCollection getTags(final JsonObject feature) {
410 final TagCollection tags = new TagCollection();
411
412 if (feature.containsKey(PROPERTIES) && !feature.isNull(PROPERTIES)) {
413 JsonValue properties = feature.get(PROPERTIES);
414 if (properties != null && properties.getValueType() == JsonValue.ValueType.OBJECT) {
415 for (Map.Entry<String, JsonValue> stringJsonValueEntry : properties.asJsonObject().entrySet()) {
416 final JsonValue value = stringJsonValueEntry.getValue();
417
418 if (value instanceof JsonString) {
419 tags.add(new Tag(stringJsonValueEntry.getKey(), ((JsonString) value).getString()));
420 } else if (value instanceof JsonObject) {
421 Logging.warn(
422 "The GeoJSON contains an object with property '" + stringJsonValueEntry.getKey()
423 + "' whose value has the unsupported type '" + value.getClass().getSimpleName()
424 + "'. That key-value pair is ignored!"
425 );
426 } else if (value.getValueType() != JsonValue.ValueType.NULL) {
427 tags.add(new Tag(stringJsonValueEntry.getKey(), value.toString()));
428 }
429 }
430 }
431 }
432 return tags;
433 }
434
435 /**
436 * Check if the inputstream follows RFC 7464
437 * @param source The source to check (should be at the beginning)
438 * @return {@code true} if the initial character is {@link GeoJSONReader#RECORD_SEPARATOR_BYTE}.
439 */
440 private static boolean isLineDelimited(InputStream source) {
441 source.mark(2);
442 try {
443 int start = source.read();
444 if (RECORD_SEPARATOR_BYTE == start) {
445 return true;
446 }
447 source.reset();
448 } catch (IOException e) {
449 Logging.error(e);
450 }
451 return false;
452 }
453
454 @Override
455 protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
456 try (InputStream markSupported = source.markSupported() ? source : new BufferedInputStream(source)) {
457 ds.setUploadPolicy(UploadPolicy.DISCOURAGED);
458 if (isLineDelimited(markSupported)) {
459 try (BufferedReader reader = new BufferedReader(new InputStreamReader(markSupported, StandardCharsets.UTF_8))) {
460 String line;
461 String rs = new String(new byte[]{RECORD_SEPARATOR_BYTE}, StandardCharsets.US_ASCII);
462 while ((line = reader.readLine()) != null) {
463 line = Utils.strip(line, rs);
464 try (JsonParser parser = Json.createParser(new StringReader(line))) {
465 parse(parser);
466 }
467 }
468 }
469 } else {
470 try (JsonParser parser = Json.createParser(markSupported)) {
471 parse(parser);
472 }
473 }
474 mergeEqualMultipolygonWays();
475 } catch (IOException | IllegalArgumentException | JsonParsingException e) {
476 throw new IllegalDataException(e);
477 }
478 return getDataSet();
479 }
480
481 /**
482 * Import may create duplicate ways were one is member of a multipolygon and untagged and the other is tagged.
483 * Try to merge them here.
484 */
485 private void mergeEqualMultipolygonWays() {
486 DuplicateWay test = new DuplicateWay();
487 test.startTest(null);
488 for (Way w: getDataSet().getWays()) {
489 test.visit(w);
490 }
491 test.endTest();
492
493 if (test.getErrors().isEmpty())
494 return;
495
496 for (TestError e : test.getErrors()) {
497 if (e.getPrimitives().size() == 2 && !e.isFixable()) {
498 List<Way> mpWays = new ArrayList<>();
499 Way replacement = null;
500 for (OsmPrimitive p : e.getPrimitives()) {
501 if (p.isTagged() && !p.referrers(Relation.class).findAny().isPresent())
502 replacement = (Way) p;
503 else if (p.referrers(Relation.class).anyMatch(Relation::isMultipolygon))
504 mpWays.add((Way) p);
505 }
506 if (replacement == null && mpWays.size() == 2) {
507 replacement = mpWays.remove(1);
508 }
509 if (replacement != null && mpWays.size() == 1) {
510 Way mpWay = mpWays.get(0);
511 for (Relation r : mpWay.referrers(Relation.class).filter(Relation::isMultipolygon)
512 .collect(Collectors.toList())) {
513 for (int i = 0; i < r.getMembersCount(); i++) {
514 if (r.getMember(i).getMember().equals(mpWay)) {
515 r.setMember(i, new RelationMember(r.getRole(i), replacement));
516 }
517 }
518 }
519 mpWay.setDeleted(true);
520 }
521 }
522 }
523 ds.cleanupDeletedPrimitives();
524 }
525
526 /**
527 * Parse the given input source and return the dataset.
528 *
529 * @param source the source input stream. Must not be null.
530 * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed
531 * @return the dataset with the parsed data
532 * @throws IllegalDataException if an error was found while parsing the data from the source
533 * @throws IllegalArgumentException if source is null
534 */
535 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
536 return new GeoJSONReader().doParseDataSet(source, progressMonitor);
537 }
538}
Note: See TracBrowser for help on using the repository browser.