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 }