File indexing completed on 2025-01-05 03:59:27

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