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 }