diff --git a/src/plugins/runner/json/JsonParser.h b/src/plugins/runner/json/JsonParser.h --- a/src/plugins/runner/json/JsonParser.h +++ b/src/plugins/runner/json/JsonParser.h @@ -1,31 +1,36 @@ /* - This file is part of the Marble Virtual Globe. + This file is part of the Marble Virtual Globe. - This program is free software licensed under the GNU LGPL. You can - find a copy of this license in LICENSE.txt in the top directory of - the source code. + This program is free software licensed under the GNU LGPL. You can + find a copy of this license in LICENSE.txt in the top directory of + the source code. - Copyright 2013 Ander Pijoan + Copyright 2013 Ander Pijoan + Copyright 2019 John Zaitseff */ #ifndef MARBLE_JSONPARSER_H #define MARBLE_JSONPARSER_H class QIODevice; +class QJsonObject; + +#include namespace Marble { class GeoDataDocument; +class GeoDataGeometry; class JsonParser { public: JsonParser(); ~JsonParser(); /** - * @brief parse the json file - * @return true if the parsed has been successful + * @brief parse the GeoJSON file + * @return true if parsing of the file was successful */ bool read(QIODevice*); @@ -39,6 +44,21 @@ private: GeoDataDocument* m_document; + + /** + * @brief parse a top-level GeoJSON object (FeatureCollection or Feature) + * @param jsonObject the object to parse + * @return true if parsing of the top-level object was successful + */ + bool parseGeoJsonTopLevel(const QJsonObject&); + + /** + * @brief parse a sub-level GeoJSON object + * @param jsonObject the object to parse + * @param geometryList a list of geometries pass back to the caller + * @return true if parsing of the object was successful + */ + bool parseGeoJsonSubLevel(const QJsonObject&, QVector&); }; } diff --git a/src/plugins/runner/json/JsonParser.cpp b/src/plugins/runner/json/JsonParser.cpp --- a/src/plugins/runner/json/JsonParser.cpp +++ b/src/plugins/runner/json/JsonParser.cpp @@ -1,19 +1,25 @@ /* - This file is part of the Marble Virtual Globe. + This file is part of the Marble Virtual Globe. - This program is free software licensed under the GNU LGPL. You can - find a copy of this license in LICENSE.txt in the top directory of - the source code. + The JsonParser class reads in a GeoJSON document that conforms to + RFC7946 (including relevant errata). Attributes are stored as OSM + tags. - Copyright 2013 Ander Pijoan + This program is free software licensed under the GNU LGPL. You can + find a copy of this license in LICENSE.txt in the top directory of + the source code. + + Copyright 2013 Ander Pijoan + Copyright 2019 John Zaitseff */ #include "JsonParser.h" #include "GeoDataDocument.h" #include "GeoDataPlacemark.h" #include "GeoDataPolygon.h" #include "GeoDataLinearRing.h" #include "GeoDataPoint.h" +#include "GeoDataMultiGeometry.h" #include "MarbleDebug.h" #include "StyleBuilder.h" #include "osm/OsmPlacemarkData.h" @@ -44,274 +50,316 @@ bool JsonParser::read( QIODevice* device ) { - // Assert previous document got released. + // Release the previous document if required delete m_document; m_document = new GeoDataDocument; Q_ASSERT( m_document ); - // Read file data + // Read JSON file data QJsonParseError error; const QJsonDocument jsonDoc = QJsonDocument::fromJson(device->readAll(), &error); if (jsonDoc.isNull()) { - qDebug() << "Error parsing GeoJSON : " << error.errorString(); + qDebug() << "Error parsing GeoJSON:" << error.errorString(); + return false; + } else if (! jsonDoc.isObject()) { + qDebug() << "Invalid file, does not contain a GeoJSON object"; return false; } - // Start parsing - const QJsonValue featuresValue = jsonDoc.object().value(QStringLiteral("features")); + // Valid GeoJSON documents may not always contain a FeatureCollection object with subsidiary + // Feature objects, or even a single Feature object: they might contain just a single geometry + // object. Handle such cases by creating a wrapper Feature object if required. + + const QString jsonObjectType = jsonDoc.object().value(QStringLiteral("type")).toString(); + + if (jsonObjectType == QStringLiteral("FeatureCollection") + || jsonObjectType == QStringLiteral("Feature")) { + + // A normal GeoJSON document: parse it recursively + return parseGeoJsonTopLevel(jsonDoc.object()); + + } else { + // Create a wrapper Feature object and parse that + + QJsonObject jsonWrapper; + QJsonObject jsonWrapperProperties; + + jsonWrapper["type"] = QStringLiteral("Feature"); + jsonWrapper["geometry"] = jsonDoc.object(); + jsonWrapper["properties"] = jsonWrapperProperties; + + return parseGeoJsonTopLevel(jsonWrapper); + } +} + +bool JsonParser::parseGeoJsonTopLevel( const QJsonObject& jsonObject ) +{ + // Every GeoJSON object must have a case-sensitive "type" member (see RFC7946 section 3) + const QString jsonObjectType = jsonObject.value(QStringLiteral("type")).toString(); - // In GeoJSON format, geometries are stored in features, so we iterate on features - if (featuresValue.isArray()) { - const QJsonArray featureArray = featuresValue.toArray(); + if (jsonObjectType == QStringLiteral("FeatureCollection")) { + // Handle the FeatureCollection object, which may contain multiple Feature objects in it - // Parse each feature + const QJsonArray featureArray = jsonObject.value(QStringLiteral("features")).toArray(); for (int featureIndex = 0; featureIndex < featureArray.size(); ++featureIndex) { - const QJsonObject featureObject = featureArray[featureIndex].toObject(); + if (! parseGeoJsonTopLevel( featureArray[featureIndex].toObject() )) { + return false; + } + } + return true; + + } else if (jsonObjectType == QStringLiteral("Feature")) { + // Handle the Feature object, which contains a single geometry object and possibly + // associated properties. Note that only Feature objects can have recognised properties. - // Check if the feature contains a geometry - const QJsonValue geometryValue = featureObject.value(QStringLiteral("geometry")); - if (geometryValue.isObject()) { - const QJsonObject geometryObject = geometryValue.toObject(); + QVector geometryList; // Populated by parseGeoJsonSubLevel() - // Variables for creating the geometry - QList geometryList; - QList placemarkList; + if (! parseGeoJsonSubLevel( jsonObject.value(QStringLiteral("geometry")).toObject(), + geometryList )) { + return false; + } - // Create the different geometry types - const QString geometryType = geometryObject.value(QStringLiteral("type")).toString().toUpper(); + // Create the placemark for this feature object with appropriate geometry - if (geometryType == QLatin1String("POLYGON")) { - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinateArray = coordinatesValue.toArray(); + GeoDataPlacemark* placemark = new GeoDataPlacemark(); - GeoDataPolygon * geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate ); + if (geometryList.length() < 1) { + // No geometries available to add to the placemark + ; - // Coordinates first array will be the outer boundary, if there are more - // positions those will be inner holes - for (int ringIndex = 0 ; ringIndex < coordinateArray.size(); ++ringIndex) { - const QJsonArray ringArray = coordinateArray[ringIndex].toArray(); + } else if (geometryList.length() == 1) { + // Single geometry + placemark->setGeometry(geometryList[0]); - GeoDataLinearRing linearRing; + } else { + // Multiple geometries require a GeoDataMultiGeometry class - for (int coordinatePairIndex = 0; coordinatePairIndex < ringArray.size(); ++coordinatePairIndex) { - const QJsonArray coordinatePairArray = ringArray[coordinatePairIndex].toArray(); + GeoDataMultiGeometry* geom = new GeoDataMultiGeometry(); + for (int i = 0; i < geometryList.length(); ++i) { + geom->append(geometryList[i]); + } + placemark->setGeometry(geom); + } - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + // Parse any associated properties - linearRing.append( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); - } + const QJsonObject propertiesObject = jsonObject.value(QStringLiteral("properties")).toObject(); + QJsonObject::ConstIterator iter = propertiesObject.begin(); + const QJsonObject::ConstIterator end = propertiesObject.end(); - // Outer ring - if (ringIndex == 0) { - geom->setOuterBoundary( linearRing ); - } - // Inner holes - else { - geom->appendInnerBoundary( linearRing ); - } - } - geometryList.append( geom ); - } + OsmPlacemarkData osmData; - } else if (geometryType == QLatin1String("MULTIPOLYGON")) { - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinateArray = coordinatesValue.toArray(); + for ( ; iter != end; ++iter) { + // Pass the value through QVariant to also get booleans and numbers + const QString propertyValue = iter.value().toVariant().toString(); + const QString propertyKey = iter.key(); - for (int polygonIndex = 0; polygonIndex < coordinateArray.size(); ++polygonIndex) { - const QJsonArray polygonArray = coordinateArray[polygonIndex].toArray(); + if (iter.value().isObject() || iter.value().isArray()) { + qDebug() << "Skipping unsupported JSON property containing an object or array:" << propertyKey; + continue; + } - GeoDataPolygon * geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate ); + osmData.addTag(propertyKey, propertyValue); - // Coordinates first array will be the outer boundary, if there are more - // positions those will be inner holes - for (int ringIndex = 0 ; ringIndex < polygonArray.size(); ++ringIndex) { - const QJsonArray ringArray = polygonArray[ringIndex].toArray(); + if (propertyKey == QStringLiteral("name")) { + placemark->setName(propertyValue); + } + } - GeoDataLinearRing linearRing; + placemark->setOsmData(osmData); + placemark->setVisible(true); - for (int coordinatePairIndex = 0; coordinatePairIndex < ringArray.size(); ++coordinatePairIndex) { - const QJsonArray coordinatePairArray = ringArray[coordinatePairIndex].toArray(); + const GeoDataPlacemark::GeoDataVisualCategory category = + StyleBuilder::determineVisualCategory(osmData); + if (category != GeoDataPlacemark::None) { + placemark->setVisualCategory(category); + } - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + m_document->append(placemark); + return true; - linearRing.append( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); - } + } else { + qDebug() << "Missing FeatureCollection or Feature object in GeoJSON file"; + return false; + } +} - // Outer ring - if (ringIndex == 0) { - geom->setOuterBoundary( linearRing ); - } - // Inner holes - else { - geom->appendInnerBoundary( linearRing ); - } - } - geometryList.append( geom ); - } - } +bool JsonParser::parseGeoJsonSubLevel( const QJsonObject& jsonObject, + QVector& geometryList ) +{ + // The GeoJSON object type + const QString jsonObjectType = jsonObject.value(QStringLiteral("type")).toString(); - } else if (geometryType == QLatin1String("LINESTRING")) { + if (jsonObjectType == QStringLiteral("FeatureCollection") + || jsonObjectType == QStringLiteral("Feature")) { - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinateArray = coordinatesValue.toArray(); + qDebug() << "Cannot have FeatureCollection or Feature objects at this level of the GeoJSON file"; + return false; - GeoDataLineString * geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate ); + } else if (jsonObjectType == QStringLiteral("GeometryCollection")) { + // Handle the GeometryCollection object, which may contain multiple geometry objects - for (int coordinatePairIndex = 0; coordinatePairIndex < coordinateArray.size(); ++coordinatePairIndex) { - const QJsonArray coordinatePairArray = coordinateArray[coordinatePairIndex].toArray(); + const QJsonArray geometryArray = jsonObject.value(QStringLiteral("geometries")).toArray(); + for (int geometryIndex = 0; geometryIndex < geometryArray.size(); ++geometryIndex) { + if (! parseGeoJsonSubLevel( geometryArray[geometryIndex].toObject(), geometryList )) { + return false; + } + } - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + return true; + } - geom->append( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); - } - geometryList.append( geom ); - } + // Handle remaining GeoJSON objects, which each have a "coordinates" member (an array) - } else if (geometryType == QLatin1String("MULTILINESTRING")) { + const QJsonArray coordinateArray = jsonObject.value(QStringLiteral("coordinates")).toArray(); - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinateArray = coordinatesValue.toArray(); - - for (int lineStringIndex = 0; lineStringIndex < coordinateArray.size(); ++lineStringIndex) { - const QJsonArray lineStringArray = coordinateArray[lineStringIndex].toArray(); + if (jsonObjectType == QStringLiteral("Point")) { + // A Point object has a single GeoJSON position: an array of at least two values - GeoDataLineString * geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate ); + GeoDataPoint* geom = new GeoDataPoint(); + const qreal lon = coordinateArray.at(0).toDouble(); + const qreal lat = coordinateArray.at(1).toDouble(); + const qreal alt = coordinateArray.at(2).toDouble(); // If missing, uses 0 as the default - for (int coordinatePairIndex = 0; coordinatePairIndex < lineStringArray.size(); ++coordinatePairIndex) { - const QJsonArray coordinatePairArray = lineStringArray[coordinatePairIndex].toArray(); + geom->setCoordinates( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + geometryList.append(geom); - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + return true; - geom->append( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); - } - geometryList.append( geom ); - } - } - - } else if (geometryType == QLatin1String("POINT")) { - - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinatePairArray = coordinatesValue.toArray(); - - GeoDataPoint * geom = new GeoDataPoint(); - - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + } else if (jsonObjectType == QStringLiteral("MultiPoint")) { + // A MultiPoint object has an array of GeoJSON positions (ie, a two-level array) - geom->setCoordinates( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); + for (int positionIndex = 0; positionIndex < coordinateArray.size(); ++positionIndex) { + const QJsonArray positionArray = coordinateArray[positionIndex].toArray(); - geometryList.append( geom ); - } - } else if (geometryType == QLatin1String("MULTIPOINT")) { + GeoDataPoint* geom = new GeoDataPoint(); + const qreal lon = positionArray.at(0).toDouble(); + const qreal lat = positionArray.at(1).toDouble(); + const qreal alt = positionArray.at(2).toDouble(); - // Check first that there are coordinates - const QJsonValue coordinatesValue = geometryObject.value(QStringLiteral("coordinates")); - if (coordinatesValue.isArray()) { - const QJsonArray coordinateArray = coordinatesValue.toArray(); + geom->setCoordinates( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + geometryList.append(geom); + } - for (int pointIndex = 0; pointIndex < coordinateArray.size(); ++pointIndex) { - const QJsonArray coordinatePairArray = coordinateArray[pointIndex].toArray(); + return true; - GeoDataPoint * geom = new GeoDataPoint(); + } else if (jsonObjectType == QStringLiteral("LineString")) { + // A LineString object has an array of GeoJSON positions (ie, a two-level array) - const qreal longitude = coordinatePairArray.at(0).toDouble(); - const qreal latitude = coordinatePairArray.at(1).toDouble(); + GeoDataLineString* geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate ); - geom->setCoordinates( GeoDataCoordinates( longitude , latitude , 0 , GeoDataCoordinates::Degree ) ); + for (int positionIndex = 0; positionIndex < coordinateArray.size(); ++positionIndex) { + const QJsonArray positionArray = coordinateArray[positionIndex].toArray(); - geometryList.append( geom ); - } - } - } - - - // Parse the features properties - const QJsonValue propertiesValue = featureObject.value(QStringLiteral("properties")); - if (!geometryList.isEmpty() && propertiesValue.isObject()) { - const QJsonObject propertiesObject = propertiesValue.toObject(); - - // First create a placemark for each geometry, there could be multi geometries - // that are translated into more than one geometry/placemark - for ( int numberGeometries = 0 ; numberGeometries < geometryList.length() ; numberGeometries++ ) { - GeoDataPlacemark * placemark = new GeoDataPlacemark(); - placemarkList.append( placemark ); - } + const qreal lon = positionArray.at(0).toDouble(); + const qreal lat = positionArray.at(1).toDouble(); + const qreal alt = positionArray.at(2).toDouble(); - OsmPlacemarkData osmData; + geom->append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + } + geometryList.append(geom); - QJsonObject::ConstIterator it = propertiesObject.begin(); - const QJsonObject::ConstIterator end = propertiesObject.end(); - for ( ; it != end; ++it) { - if (it.value().isObject() || it.value().isArray()) { - qDebug() << "Skipping property, values of type arrays and objects not supported:" << it.key(); - continue; - } + return true; - // pass value through QVariant to also get bool & numbers - osmData.addTag(it.key(), it.value().toVariant().toString()); - } + } else if (jsonObjectType == QStringLiteral("MultiLineString")) { + // A MultiLineString object has an array of arrays of GeoJSON positions (three-level) - // If the property read, is the features name - const auto tagIter = osmData.findTag(QStringLiteral("name")); - if (tagIter != osmData.tagsEnd()) { - const QString& name = tagIter.value(); - for (int pl = 0 ; pl < placemarkList.length(); ++pl) { - placemarkList.at(pl)->setName(name); - } - } + for (int lineStringIndex = 0; lineStringIndex < coordinateArray.size(); ++lineStringIndex) { + const QJsonArray lineStringArray = coordinateArray[lineStringIndex].toArray(); - const GeoDataPlacemark::GeoDataVisualCategory category = StyleBuilder::determineVisualCategory(osmData); - if (category != GeoDataPlacemark::None) { - // Add the visual category to all the placemarks - for (int pl = 0 ; pl < placemarkList.length(); ++pl) { - placemarkList.at(pl)->setVisualCategory(category); - placemarkList.at(pl)->setOsmData(osmData); - } - } - } + GeoDataLineString* geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate ); - // Add the geometry to the document - if ( geometryList.length() == placemarkList.length() ) { - - while( placemarkList.length() > 0 ) { - - GeoDataPlacemark * placemark = placemarkList.last(); - placemarkList.pop_back(); - - GeoDataGeometry * geom = geometryList.last(); - geometryList.pop_back(); - - placemark->setGeometry( geom ); - placemark->setVisible( true ); - m_document->append( placemark ); - } - } - - // If geometries or placemarks missing inside the lists, delete them - qDeleteAll( geometryList.begin(), geometryList.end() ); - geometryList.clear(); - qDeleteAll( placemarkList.begin(), placemarkList.end() ); - placemarkList.clear(); - } - } + for (int positionIndex = 0; positionIndex < lineStringArray.size(); ++positionIndex) { + const QJsonArray positionArray = lineStringArray[positionIndex].toArray(); + + const qreal lon = positionArray.at(0).toDouble(); + const qreal lat = positionArray.at(1).toDouble(); + const qreal alt = positionArray.at(2).toDouble(); + + geom->append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + } + geometryList.append(geom); + } + + return true; + + } else if (jsonObjectType == QStringLiteral("Polygon")) { + // A Polygon object has an array of arrays of GeoJSON positions: the first array within the + // top-level Polygon coordinates array is the outer boundary, following arrays are inner + // holes (if any) + + GeoDataPolygon* geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate ); + + for (int ringIndex = 0; ringIndex < coordinateArray.size(); ++ringIndex) { + const QJsonArray ringArray = coordinateArray[ringIndex].toArray(); + + GeoDataLinearRing linearRing; + + for (int positionIndex = 0; positionIndex < ringArray.size(); ++positionIndex) { + const QJsonArray positionArray = ringArray[positionIndex].toArray(); + + const qreal lon = positionArray.at(0).toDouble(); + const qreal lat = positionArray.at(1).toDouble(); + const qreal alt = positionArray.at(2).toDouble(); + + linearRing.append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + } + + if (ringIndex == 0) { + // Outer boundary of the polygon + geom->setOuterBoundary(linearRing); + } else { + geom->appendInnerBoundary(linearRing); + } + } + geometryList.append(geom); + + return true; + + } else if (jsonObjectType == QStringLiteral("MultiPolygon")) { + // A MultiPolygon object has an array of Polygon arrays (ie, a four-level array) + + for (int polygonIndex = 0; polygonIndex < coordinateArray.size(); ++polygonIndex) { + const QJsonArray polygonArray = coordinateArray[polygonIndex].toArray(); + + GeoDataPolygon* geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate ); + + for (int ringIndex = 0; ringIndex < polygonArray.size(); ++ringIndex) { + const QJsonArray ringArray = polygonArray[ringIndex].toArray(); + + GeoDataLinearRing linearRing; + + for (int positionIndex = 0; positionIndex < ringArray.size(); ++positionIndex) { + const QJsonArray positionArray = ringArray[positionIndex].toArray(); + + const qreal lon = positionArray.at(0).toDouble(); + const qreal lat = positionArray.at(1).toDouble(); + const qreal alt = positionArray.at(2).toDouble(); + + linearRing.append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree )); + } + + if (ringIndex == 0) { + // Outer boundary of the polygon + geom->setOuterBoundary(linearRing); + } else { + geom->appendInnerBoundary(linearRing); + } + } + geometryList.append(geom); + } + + return true; + + } else if (jsonObjectType == QStringLiteral("")) { + // Unlocated Feature objects have a null value for "geometry" (RFC7946 section 3.2) + return true; + + } else { + qDebug() << "Unknown GeoJSON object type" << jsonObjectType; + return false; } - return true; } } -