File indexing completed on 2024-04-28 03:50:31

0001 /*
0002     The JsonParser class reads in a GeoJSON document that conforms to
0003     RFC7946 (including relevant errata) and optionally contains
0004     attributes from the Simplestyle specification version 1.1.0
0005     ((https://github.com/mapbox/simplestyle-spec).  Attributes are also
0006     stored as OSM tags as required.
0007 
0008     TODO: Handle the Simplestyle "marker-size", "marker-symbol" and
0009     "marker-color" correctly.
0010 
0011     SPDX-License-Identifier: LGPL-2.1-or-later
0012 
0013     SPDX-FileCopyrightText: 2013 Ander Pijoan <ander.pijoan@deusto.es>
0014     SPDX-FileCopyrightText: 2019 John Zaitseff <J.Zaitseff@zap.org.au>
0015 */
0016 
0017 #include "JsonParser.h"
0018 #include "GeoDataDocument.h"
0019 #include "GeoDataPlacemark.h"
0020 #include "GeoDataPolygon.h"
0021 #include "GeoDataLinearRing.h"
0022 #include "GeoDataPoint.h"
0023 #include "GeoDataMultiGeometry.h"
0024 #include "MarbleDirs.h"
0025 #include "MarbleDebug.h"
0026 #include "StyleBuilder.h"
0027 #include "osm/OsmPlacemarkData.h"
0028 
0029 #include <QIODevice>
0030 #include <QJsonDocument>
0031 #include <QJsonArray>
0032 #include <QJsonObject>
0033 #include <QColor>
0034 
0035 #include "GeoDataStyle.h"
0036 #include "GeoDataIconStyle.h"
0037 #include "GeoDataLineStyle.h"
0038 #include "GeoDataPolyStyle.h"
0039 #include "GeoDataLabelStyle.h"
0040 
0041 
0042 namespace Marble {
0043 
0044 JsonParser::JsonParser() : m_document( nullptr )
0045 {
0046     // Get the default styles set by Marble
0047 
0048     GeoDataPlacemark placemark;
0049     GeoDataStyle::Ptr style(new GeoDataStyle(*(placemark.style())));
0050     m_iconStylePoints = new GeoDataIconStyle(style->iconStyle());
0051     m_iconStyleOther = new GeoDataIconStyle(style->iconStyle());
0052     m_lineStyle = new GeoDataLineStyle(style->lineStyle());
0053     m_polyStyle = new GeoDataPolyStyle(style->polyStyle());
0054     m_labelStyle = new GeoDataLabelStyle(style->labelStyle());
0055 
0056     // Set default styles for GeoJSON objects using Simplestyle specification 1.1.0
0057 
0058     // Set "marker-color": "#7e7e7e" and "marker-size": "medium"
0059     m_iconStylePoints->setColor(QColor("#ff7e7e7e"));
0060     m_iconStylePoints->setIconPath(MarbleDirs::path(QStringLiteral("svg/dot-circle-regular.svg")));
0061     m_iconStylePoints->setSize(QSize(22,22), Qt::KeepAspectRatio);
0062 
0063     m_iconStyleOther->setIconPath(nullptr);
0064     m_iconStyleOther->setColor(QColor("#ff7e7e7e"));
0065 
0066     // Set "stroke": "#555555", "stroke-opacity": 1.0 and "stroke-width": 2 (increased to 2.5 due
0067     // to problems with antialiased lines disappearing on drawn maps
0068     m_lineStyle->setColor(QColor("#ff555555"));
0069     m_lineStyle->setWidth(2.5);
0070 
0071     // Set "fill": "#555555" and "fill-opacity": 0.6
0072     m_polyStyle->setColor(QColor("#99555555"));
0073 
0074     // Set visual properties not part of the Simplestyle spec
0075 
0076     m_labelStyle->setColor(QColor("#ff000000"));
0077     m_labelStyle->setGlow(true);
0078 
0079     m_polyStyle->setFill(true);
0080     m_polyStyle->setOutline(true);
0081 }
0082 
0083 JsonParser::~JsonParser()
0084 {
0085     delete m_document;
0086 
0087     delete m_iconStylePoints;
0088     delete m_iconStyleOther;
0089     delete m_lineStyle;
0090     delete m_polyStyle;
0091     delete m_labelStyle;
0092 }
0093 
0094 GeoDataDocument *JsonParser::releaseDocument()
0095 {
0096     GeoDataDocument* document = m_document;
0097     m_document = nullptr;
0098     return document;
0099 }
0100 
0101 bool JsonParser::read( QIODevice* device )
0102 {
0103     // Release the previous document if required
0104     delete m_document;
0105     m_document = new GeoDataDocument;
0106     Q_ASSERT( m_document );
0107 
0108     // Read JSON file data
0109     QJsonParseError error;
0110     const QJsonDocument jsonDoc = QJsonDocument::fromJson(device->readAll(), &error);
0111 
0112     if (jsonDoc.isNull()) {
0113         qDebug() << "Error parsing GeoJSON:" << error.errorString();
0114         return false;
0115     } else if (! jsonDoc.isObject()) {
0116         qDebug() << "Invalid file, does not contain a GeoJSON object";
0117         return false;
0118     }
0119 
0120     // Valid GeoJSON documents may not always contain a FeatureCollection object with subsidiary
0121     // Feature objects, or even a single Feature object: they might contain just a single geometry
0122     // object.  Handle such cases by creating a wrapper Feature object if required.
0123 
0124     const QString jsonObjectType = jsonDoc.object().value(QStringLiteral("type")).toString();
0125 
0126     if (jsonObjectType == QStringLiteral("FeatureCollection")
0127         || jsonObjectType == QStringLiteral("Feature")) {
0128 
0129         // A normal GeoJSON document: parse it recursively
0130         return parseGeoJsonTopLevel(jsonDoc.object());
0131 
0132     } else {
0133         // Create a wrapper Feature object and parse that
0134 
0135         QJsonObject jsonWrapper;
0136         QJsonObject jsonWrapperProperties;
0137 
0138         jsonWrapper["type"] = QStringLiteral("Feature");
0139         jsonWrapper["geometry"] = jsonDoc.object();
0140         jsonWrapper["properties"] = jsonWrapperProperties;
0141 
0142         return parseGeoJsonTopLevel(jsonWrapper);
0143     }
0144 }
0145 
0146 bool JsonParser::parseGeoJsonTopLevel( const QJsonObject& jsonObject )
0147 {
0148     // Every GeoJSON object must have a case-sensitive "type" member (see RFC7946 section 3)
0149     const QString jsonObjectType = jsonObject.value(QStringLiteral("type")).toString();
0150 
0151     if (jsonObjectType == QStringLiteral("FeatureCollection")) {
0152         // Handle the FeatureCollection object, which may contain multiple Feature objects in it
0153 
0154         const QJsonArray featureArray = jsonObject.value(QStringLiteral("features")).toArray();
0155         for (int featureIndex = 0; featureIndex < featureArray.size(); ++featureIndex) {
0156             if (! parseGeoJsonTopLevel( featureArray[featureIndex].toObject() )) {
0157                 return false;
0158             }
0159         }
0160         return true;
0161 
0162     } else if (jsonObjectType == QStringLiteral("Feature")) {
0163         // Handle the Feature object, which contains a single geometry object and possibly
0164         // associated properties.  Note that only Feature objects can have recognised properties.
0165 
0166         QVector<GeoDataGeometry*> geometryList;     // Populated by parseGeoJsonSubLevel()
0167         bool hasPoints = false;                     // Populated by parseGeoJsonSubLevel()
0168 
0169         if (! parseGeoJsonSubLevel( jsonObject.value(QStringLiteral("geometry")).toObject(),
0170                 geometryList, hasPoints )) {
0171             return false;
0172         }
0173 
0174         // Create the placemark for this feature object with appropriate geometry
0175 
0176         GeoDataPlacemark* placemark = new GeoDataPlacemark();
0177 
0178         if (geometryList.length() < 1) {
0179             // No geometries available to add to the placemark
0180             ;
0181 
0182         } else if (geometryList.length() == 1) {
0183             // Single geometry
0184             placemark->setGeometry(geometryList[0]);
0185 
0186         } else {
0187             // Multiple geometries require a GeoDataMultiGeometry class
0188 
0189             GeoDataMultiGeometry* geom = new GeoDataMultiGeometry();
0190             for (int i = 0; i < geometryList.length(); ++i) {
0191                 geom->append(geometryList[i]);
0192             }
0193             placemark->setGeometry(geom);
0194         }
0195 
0196         // Create copies of the default styles
0197 
0198         GeoDataStyle::Ptr style(new GeoDataStyle(*(placemark->style())));
0199         GeoDataIconStyle iconStyle = hasPoints ? *m_iconStylePoints : *m_iconStyleOther;
0200         GeoDataLineStyle lineStyle = *m_lineStyle;
0201         GeoDataPolyStyle polyStyle = *m_polyStyle;
0202 
0203         // Parse any associated properties
0204 
0205         const QJsonObject propertiesObject = jsonObject.value(QStringLiteral("properties")).toObject();
0206         QJsonObject::ConstIterator iter = propertiesObject.begin();
0207         const QJsonObject::ConstIterator end = propertiesObject.end();
0208 
0209         OsmPlacemarkData osmData;
0210 
0211         for ( ; iter != end; ++iter) {
0212             // Pass the value through QVariant to also get booleans and numbers
0213             const QString propertyValue = iter.value().toVariant().toString();
0214             const QString propertyKey = iter.key();
0215 
0216             if (iter.value().isObject() || iter.value().isArray()) {
0217                 qDebug() << "Skipping unsupported JSON property containing an object or array:" << propertyKey;
0218                 continue;
0219             }
0220 
0221             if (propertyKey == QStringLiteral("name")) {
0222                 // The "name" property is not defined in the Simplestyle specification, but is used
0223                 // extensively in the wild.  Treat "name" and "title" essentially the same for the
0224                 // purposes of placemarks (although osmData tags will preserve the distinction).
0225 
0226                 placemark->setName(propertyValue);
0227                 osmData.addTag(propertyKey, propertyValue);
0228 
0229             } else if (propertyKey == QStringLiteral("title")) {
0230                 placemark->setName(propertyValue);
0231                 osmData.addTag(propertyKey, propertyValue);
0232 
0233             } else if (propertyKey == QStringLiteral("description")) {
0234                 placemark->setDescription(propertyValue);
0235                 osmData.addTag(propertyKey, propertyValue);
0236 
0237             } else if (propertyKey == QStringLiteral("marker-size")) {
0238                 // TODO: Implement marker-size handling
0239                 if (propertyValue == QStringLiteral("")) {
0240                     // Use the default value
0241                     ;
0242                 } else {
0243                     //qDebug() << "Ignoring unimplemented marker-size property:" << propertyValue;
0244                 }
0245 
0246             } else if (propertyKey == QStringLiteral("marker-symbol")) {
0247                 // TODO: Implement marker-symbol handling
0248                 if (propertyValue == QStringLiteral("")) {
0249                     // Use the default value
0250                     ;
0251                 } else {
0252                     //qDebug() << "Ignoring unimplemented marker-symbol property:" << propertyValue;
0253                 }
0254 
0255             } else if (propertyKey == QStringLiteral("marker-color")) {
0256                 // Even though the Simplestyle spec allows colors to omit the leading "#", this
0257                 // implementation assumes it is always present, as this then allows named colors
0258                 // understood by QColor as an extension
0259                 QColor color = QColor(propertyValue);
0260                 if (color.isValid()) {
0261                     iconStyle.setColor(color);  // Currently ignored by Marble
0262                 } else {
0263                     qDebug() << "Ignoring invalid marker-color property:" << propertyValue;
0264                 }
0265 
0266             } else if (propertyKey == QStringLiteral("stroke")) {
0267                 QColor color = QColor(propertyValue);   // Assume leading "#" is present
0268                 if (color.isValid()) {
0269                     color.setAlpha(lineStyle.color().alpha());
0270                     lineStyle.setColor(color);
0271                 } else {
0272                     qDebug() << "Ignoring invalid stroke property:" << propertyValue;
0273                 }
0274 
0275             } else if (propertyKey == QStringLiteral("stroke-opacity")) {
0276                 bool ok;
0277                 float opacity = propertyValue.toFloat(&ok);
0278                 if (ok && opacity >= 0.0 && opacity <= 1.0) {
0279                     QColor color = lineStyle.color();
0280                     color.setAlphaF(opacity);
0281                     lineStyle.setColor(color);
0282                 } else {
0283                     qDebug() << "Ignoring invalid stroke-opacity property:" << propertyValue;
0284                 }
0285 
0286             } else if (propertyKey == QStringLiteral("stroke-width")) {
0287                 bool ok;
0288                 float width = propertyValue.toFloat(&ok);
0289                 if (ok && width >= 0.0) {
0290                     lineStyle.setWidth(width);
0291                 } else {
0292                     qDebug() << "Ignoring invalid stroke-width property:" << propertyValue;
0293                 }
0294 
0295             } else if (propertyKey == QStringLiteral("fill")) {
0296                 QColor color = QColor(propertyValue);   // Assume leading "#" is present
0297                 if (color.isValid()) {
0298                     color.setAlpha(polyStyle.color().alpha());
0299                     polyStyle.setColor(color);
0300                 } else {
0301                     qDebug() << "Ignoring invalid fill property:" << propertyValue;
0302                 }
0303 
0304             } else if (propertyKey == QStringLiteral("fill-opacity")) {
0305                 bool ok;
0306                 float opacity = propertyValue.toFloat(&ok);
0307                 if (ok && opacity >= 0.0 && opacity <= 1.0) {
0308                     QColor color = polyStyle.color();
0309                     color.setAlphaF(opacity);
0310                     polyStyle.setColor(color);
0311                 } else {
0312                     qDebug() << "Ignoring invalid fill-opacity property:" << propertyValue;
0313                 }
0314 
0315             } else {
0316                 // Property is not defined by the Simplestyle spec
0317                 osmData.addTag(propertyKey, propertyValue);
0318             }
0319         }
0320 
0321         style->setIconStyle(iconStyle);
0322         style->setLineStyle(lineStyle);
0323         style->setPolyStyle(polyStyle);
0324         style->setLabelStyle(*m_labelStyle);
0325         placemark->setStyle(style);
0326 
0327         placemark->setOsmData(osmData);
0328         placemark->setVisible(true);
0329 
0330         const GeoDataPlacemark::GeoDataVisualCategory category =
0331             StyleBuilder::determineVisualCategory(osmData);
0332         if (category != GeoDataPlacemark::None) {
0333             placemark->setVisualCategory(category);
0334         }
0335 
0336         m_document->append(placemark);
0337         return true;
0338 
0339     } else {
0340         qDebug() << "Missing FeatureCollection or Feature object in GeoJSON file";
0341         return false;
0342     }
0343 }
0344 
0345 bool JsonParser::parseGeoJsonSubLevel( const QJsonObject& jsonObject,
0346                                        QVector<GeoDataGeometry*>& geometryList, bool& hasPoints )
0347 {
0348     // The GeoJSON object type
0349     const QString jsonObjectType = jsonObject.value(QStringLiteral("type")).toString();
0350 
0351     if (jsonObjectType == QStringLiteral("FeatureCollection")
0352         || jsonObjectType == QStringLiteral("Feature")) {
0353 
0354         qDebug() << "Cannot have FeatureCollection or Feature objects at this level of the GeoJSON file";
0355         return false;
0356 
0357     } else if (jsonObjectType == QStringLiteral("GeometryCollection")) {
0358         // Handle the GeometryCollection object, which may contain multiple geometry objects
0359 
0360         const QJsonArray geometryArray = jsonObject.value(QStringLiteral("geometries")).toArray();
0361         for (int geometryIndex = 0; geometryIndex < geometryArray.size(); ++geometryIndex) {
0362             if (! parseGeoJsonSubLevel( geometryArray[geometryIndex].toObject(), geometryList, hasPoints )) {
0363                 return false;
0364             }
0365         }
0366 
0367         return true;
0368     }
0369 
0370     // Handle remaining GeoJSON objects, which each have a "coordinates" member (an array)
0371 
0372     const QJsonArray coordinateArray = jsonObject.value(QStringLiteral("coordinates")).toArray();
0373 
0374     if (jsonObjectType == QStringLiteral("Point")) {
0375         // A Point object has a single GeoJSON position: an array of at least two values
0376 
0377         GeoDataPoint* geom = new GeoDataPoint();
0378         const qreal lon = coordinateArray.at(0).toDouble();
0379         const qreal lat = coordinateArray.at(1).toDouble();
0380         const qreal alt = coordinateArray.at(2).toDouble();     // If missing, uses 0 as the default
0381 
0382         geom->setCoordinates( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0383         geometryList.append(geom);
0384 
0385         hasPoints = true;
0386         return true;
0387 
0388     } else if (jsonObjectType == QStringLiteral("MultiPoint")) {
0389         // A MultiPoint object has an array of GeoJSON positions (ie, a two-level array)
0390 
0391         for (int positionIndex = 0; positionIndex < coordinateArray.size(); ++positionIndex) {
0392             const QJsonArray positionArray = coordinateArray[positionIndex].toArray();
0393 
0394             GeoDataPoint* geom = new GeoDataPoint();
0395             const qreal lon = positionArray.at(0).toDouble();
0396             const qreal lat = positionArray.at(1).toDouble();
0397             const qreal alt = positionArray.at(2).toDouble();
0398 
0399             geom->setCoordinates( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0400             geometryList.append(geom);
0401         }
0402 
0403         hasPoints = true;
0404         return true;
0405 
0406     } else if (jsonObjectType == QStringLiteral("LineString")) {
0407         // A LineString object has an array of GeoJSON positions (ie, a two-level array)
0408 
0409         GeoDataLineString* geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate );
0410 
0411         for (int positionIndex = 0; positionIndex < coordinateArray.size(); ++positionIndex) {
0412             const QJsonArray positionArray = coordinateArray[positionIndex].toArray();
0413 
0414             const qreal lon = positionArray.at(0).toDouble();
0415             const qreal lat = positionArray.at(1).toDouble();
0416             const qreal alt = positionArray.at(2).toDouble();
0417 
0418             geom->append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0419         }
0420         geometryList.append(geom);
0421 
0422         return true;
0423 
0424     } else if (jsonObjectType == QStringLiteral("MultiLineString")) {
0425         // A MultiLineString object has an array of arrays of GeoJSON positions (three-level)
0426 
0427         for (int lineStringIndex = 0; lineStringIndex < coordinateArray.size(); ++lineStringIndex) {
0428             const QJsonArray lineStringArray = coordinateArray[lineStringIndex].toArray();
0429 
0430             GeoDataLineString* geom = new GeoDataLineString( RespectLatitudeCircle | Tessellate );
0431 
0432             for (int positionIndex = 0; positionIndex < lineStringArray.size(); ++positionIndex) {
0433                 const QJsonArray positionArray = lineStringArray[positionIndex].toArray();
0434 
0435                 const qreal lon = positionArray.at(0).toDouble();
0436                 const qreal lat = positionArray.at(1).toDouble();
0437                 const qreal alt = positionArray.at(2).toDouble();
0438 
0439                 geom->append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0440             }
0441             geometryList.append(geom);
0442         }
0443 
0444         return true;
0445 
0446     } else if (jsonObjectType == QStringLiteral("Polygon")) {
0447         // A Polygon object has an array of arrays of GeoJSON positions: the first array within the
0448         // top-level Polygon coordinates array is the outer boundary, following arrays are inner
0449         // holes (if any)
0450 
0451         GeoDataPolygon* geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate );
0452 
0453         for (int ringIndex = 0; ringIndex < coordinateArray.size(); ++ringIndex) {
0454             const QJsonArray ringArray = coordinateArray[ringIndex].toArray();
0455 
0456             GeoDataLinearRing linearRing;
0457 
0458             for (int positionIndex = 0; positionIndex < ringArray.size(); ++positionIndex) {
0459                 const QJsonArray positionArray = ringArray[positionIndex].toArray();
0460 
0461                 const qreal lon = positionArray.at(0).toDouble();
0462                 const qreal lat = positionArray.at(1).toDouble();
0463                 const qreal alt = positionArray.at(2).toDouble();
0464 
0465                 linearRing.append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0466             }
0467 
0468             if (ringIndex == 0) {
0469                 // Outer boundary of the polygon
0470                 geom->setOuterBoundary(linearRing);
0471             } else {
0472                 geom->appendInnerBoundary(linearRing);
0473             }
0474         }
0475         geometryList.append(geom);
0476 
0477         return true;
0478 
0479     } else if (jsonObjectType == QStringLiteral("MultiPolygon")) {
0480         // A MultiPolygon object has an array of Polygon arrays (ie, a four-level array)
0481 
0482         for (int polygonIndex = 0; polygonIndex < coordinateArray.size(); ++polygonIndex) {
0483             const QJsonArray polygonArray = coordinateArray[polygonIndex].toArray();
0484 
0485             GeoDataPolygon* geom = new GeoDataPolygon( RespectLatitudeCircle | Tessellate );
0486 
0487             for (int ringIndex = 0; ringIndex < polygonArray.size(); ++ringIndex) {
0488                 const QJsonArray ringArray = polygonArray[ringIndex].toArray();
0489 
0490                 GeoDataLinearRing linearRing;
0491 
0492                 for (int positionIndex = 0; positionIndex < ringArray.size(); ++positionIndex) {
0493                     const QJsonArray positionArray = ringArray[positionIndex].toArray();
0494 
0495                     const qreal lon = positionArray.at(0).toDouble();
0496                     const qreal lat = positionArray.at(1).toDouble();
0497                     const qreal alt = positionArray.at(2).toDouble();
0498 
0499                     linearRing.append( GeoDataCoordinates( lon, lat, alt, GeoDataCoordinates::Degree ));
0500                 }
0501 
0502                 if (ringIndex == 0) {
0503                     // Outer boundary of the polygon
0504                     geom->setOuterBoundary(linearRing);
0505                 } else {
0506                     geom->appendInnerBoundary(linearRing);
0507                 }
0508             }
0509             geometryList.append(geom);
0510         }
0511 
0512         return true;
0513 
0514     } else if (jsonObjectType == QStringLiteral("")) {
0515         // Unlocated Feature objects have a null value for "geometry" (RFC7946 section 3.2)
0516         return true;
0517 
0518     } else {
0519         qDebug() << "Unknown GeoJSON object type" << jsonObjectType;
0520         return false;
0521     }
0522 }
0523 
0524 }