/* WARNING: This code uses internal Qt API - the QZipReader class for reading ZIP files - and things may break if Qt changes the API. For Qt5 this is not a problem as we can "see the future" now and there are no changes in all the supported Qt5 versions up to the last one (5.15). In Qt6 the class might change or even disappear in the future, but this is very unlikely as there were no changes for several years and The Qt Company's policy is: "do not invest any resources into any desktop related stuff unless absolutely necessary". There is an issue (QTBUG-3897) since the year 2009 to include the ZIP reader into the public API, which aptly illustrates the effort The Qt Company is willing to make about anything desktop related... */ #include #include #include #include #include #include #include #include "common/util.h" #include "kmlparser.h" static bool isZIP(QFile *file) { quint32 magic; return (file->read((char *)&magic, sizeof(magic)) == (qint64)sizeof(magic) && qFromLittleEndian(magic) == 0x04034b50); } qreal KMLParser::number() { bool res; qreal ret = _reader.readElementText().toDouble(&res); if (!res) _reader.raiseError(QString("Invalid %1").arg( _reader.name().toString())); return ret; } QDateTime KMLParser::time() { QDateTime d = QDateTime::fromString(_reader.readElementText(), Qt::ISODate); if (!d.isValid()) _reader.raiseError(QString("Invalid %1").arg( _reader.name().toString())); return d; } bool KMLParser::coord(Trackpoint &trackpoint) { QString data = _reader.readElementText(); const QChar *sp, *ep, *cp, *vp; int c = 0; qreal val[3]; bool res; sp = data.constData(); ep = sp + data.size(); for (cp = sp; cp < ep; cp++) if (!cp->isSpace()) break; for (vp = cp; cp <= ep; cp++) { if (cp->isSpace() || cp->isNull()) { if (c > 2) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; if (c == 1) { trackpoint.setCoordinates(Coordinates(val[0], val[1])); if (!trackpoint.coordinates().isValid()) return false; } else if (c == 2) trackpoint.setElevation(val[2]); while (cp->isSpace()) cp++; vp = cp; c++; } } return true; } bool KMLParser::pointCoordinates(Waypoint &waypoint) { QString data = _reader.readElementText(); const QChar *sp, *ep, *cp, *vp; int c = 0; qreal val[3]; bool res; sp = data.constData(); ep = sp + data.size(); for (cp = sp; cp < ep; cp++) if (!cp->isSpace()) break; for (vp = cp; cp <= ep; cp++) { if (*cp == ',') { if (c > 1) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; c++; vp = cp + 1; } else if (cp->isSpace() || cp->isNull()) { if (c < 1) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; waypoint.setCoordinates(Coordinates(val[0], val[1])); if (!waypoint.coordinates().isValid()) return false; if (c == 2) waypoint.setElevation(val[2]); while (cp->isSpace()) cp++; c = 3; } } return true; } bool KMLParser::lineCoordinates(SegmentData &segment) { QString data = _reader.readElementText(); const QChar *sp, *ep, *cp, *vp; int c = 0; qreal val[3]; bool res; sp = data.constData(); ep = sp + data.size(); for (cp = sp; cp < ep; cp++) if (!cp->isSpace()) break; for (vp = cp; cp <= ep; cp++) { if (*cp == ',') { if (c > 1) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; c++; vp = cp + 1; } else if (cp->isSpace() || cp->isNull()) { if (c < 1 || c > 2) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; segment.append(Trackpoint(Coordinates(val[0], val[1]))); if (!segment.last().coordinates().isValid()) return false; if (c == 2) segment.last().setElevation(val[2]); while (cp->isSpace()) cp++; c = 0; vp = cp; } } return true; } bool KMLParser::polygonCoordinates(QVector &points) { QString data = _reader.readElementText(); const QChar *sp, *ep, *cp, *vp; int c = 0; qreal val[3]; bool res; sp = data.constData(); ep = sp + data.size(); for (cp = sp; cp < ep; cp++) if (!cp->isSpace()) break; for (vp = cp; cp <= ep; cp++) { if (*cp == ',') { if (c > 1) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; c++; vp = cp + 1; } else if (cp->isSpace() || cp->isNull()) { if (c < 1 || c > 2) return false; val[c] = QString(vp, cp - vp).toDouble(&res); if (!res) return false; points.append(Coordinates(val[0], val[1])); if (!points.last().isValid()) return false; while (cp->isSpace()) cp++; c = 0; vp = cp; } } return true; } QDateTime KMLParser::timeStamp() { QDateTime ts; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("when")) ts = time(); else _reader.skipCurrentElement(); } return ts; } void KMLParser::lineString(SegmentData &segment) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("coordinates")) { if (!lineCoordinates(segment)) _reader.raiseError("Invalid coordinates"); } else _reader.skipCurrentElement(); } } void KMLParser::linearRing(QVector &coordinates) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("coordinates")) { if (!polygonCoordinates(coordinates)) _reader.raiseError("Invalid coordinates"); } else _reader.skipCurrentElement(); } } void KMLParser::boundary(QVector &coordinates) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("LinearRing")) linearRing(coordinates); else _reader.skipCurrentElement(); } } void KMLParser::polygon(Area &area) { Polygon polygon; while (_reader.readNextStartElement()) { QVector path; if (_reader.name() == QLatin1String("outerBoundaryIs")) { if (!polygon.isEmpty()) { _reader.raiseError("Multiple polygon outerBoundaryIss"); return; } boundary(path); polygon.append(path); } else if (_reader.name() == QLatin1String("innerBoundaryIs")) { if (polygon.isEmpty()) { _reader.raiseError("Missing polygon outerBoundaryIs"); return; } boundary(path); polygon.append(path); } else _reader.skipCurrentElement(); } area.append(polygon); } void KMLParser::point(Waypoint &waypoint) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("coordinates")) { if (!pointCoordinates(waypoint)) _reader.raiseError("Invalid coordinates"); } else _reader.skipCurrentElement(); } if (waypoint.coordinates().isNull()) _reader.raiseError("Missing Point coordinates"); } void KMLParser::heartRate(SegmentData &segment, int start) { int i = start; const char error[] = "Heartrate data count mismatch"; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("value")) { if (i < segment.size()) segment[i++].setHeartRate(number()); else { _reader.raiseError(error); return; } } else _reader.skipCurrentElement(); } if (i != segment.size()) _reader.raiseError(error); } void KMLParser::cadence(SegmentData &segment, int start) { int i = start; const char error[] = "Cadence data count mismatch"; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("value")) { if (i < segment.size()) segment[i++].setCadence(number()); else { _reader.raiseError(error); return; } } else _reader.skipCurrentElement(); } if (i != segment.size()) _reader.raiseError(error); } void KMLParser::speed(SegmentData &segment, int start) { int i = start; const char error[] = "Speed data count mismatch"; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("value")) { if (i < segment.size()) segment[i++].setSpeed(number()); else { _reader.raiseError(error); return; } } else _reader.skipCurrentElement(); } if (i != segment.size()) _reader.raiseError(error); } void KMLParser::temperature(SegmentData &segment, int start) { int i = start; const char error[] = "Temperature data count mismatch"; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("value")) { if (i < segment.size()) segment[i++].setTemperature(number()); else { _reader.raiseError(error); return; } } else _reader.skipCurrentElement(); } if (i != segment.size()) _reader.raiseError(error); } void KMLParser::schemaData(SegmentData &segment, int start) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("SimpleArrayData")) { QXmlStreamAttributes attr = _reader.attributes(); QString name(attr.value("name").toString()); if (name == QLatin1String("Heartrate")) heartRate(segment, start); else if (name == QLatin1String("Cadence")) cadence(segment, start); else if (name == QLatin1String("Speed")) speed(segment, start); else if (name == QLatin1String("Temperature")) temperature(segment, start); else _reader.skipCurrentElement(); } else _reader.skipCurrentElement(); } } void KMLParser::extendedData(SegmentData &segment, int start) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("SchemaData")) schemaData(segment, start); else _reader.skipCurrentElement(); } } void KMLParser::track(SegmentData &segment) { const char error[] = "gx:coord/when element count mismatch"; int first = segment.size(); int i = first; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("when")) { segment.append(Trackpoint()); segment.last().setTimestamp(time()); } else if (_reader.name() == QLatin1String("coord")) { if (i == segment.size()) { _reader.raiseError(error); return; } else if (!coord(segment[i])) { _reader.raiseError("Invalid coordinates"); return; } i++; } else if (_reader.name() == QLatin1String("ExtendedData")) extendedData(segment, first); else _reader.skipCurrentElement(); } if (i != segment.size()) _reader.raiseError(error); } void KMLParser::multiTrack(TrackData &t) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Track")) { t.append(SegmentData()); track(t.last()); } else _reader.skipCurrentElement(); } } void KMLParser::multiGeometry(QList &tracks, QList &areas, QVector &waypoints, const QString &name, const QString &desc, const QDateTime ×tamp) { TrackData *tp = 0; Area *ap = 0; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Point")) { waypoints.append(Waypoint()); Waypoint &w = waypoints.last(); w.setName(name); w.setDescription(desc); w.setTimestamp(timestamp); point(w); } else if (_reader.name() == QLatin1String("LineString")) { if (!tp) { tracks.append(TrackData()); tp = &tracks.last(); tp->setName(name); tp->setDescription(desc); } tp->append(SegmentData()); lineString(tp->last()); } else if (_reader.name() == QLatin1String("Polygon")) { if (!ap) { areas.append(Area()); ap = &areas.last(); ap->setName(name); ap->setDescription(desc); } polygon(*ap); } else _reader.skipCurrentElement(); } } void KMLParser::photoOverlay(const Ctx &ctx, QVector &waypoints, PointStyleMap &pointStyles) { QString img, id; Waypoint w; QMap unused; QMap unused2; static QRegularExpression re("\\$\\[[^\\]]+\\]"); while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("name")) w.setName(_reader.readElementText()); else if (_reader.name() == QLatin1String("description")) w.setDescription(_reader.readElementText()); else if (_reader.name() == QLatin1String("phoneNumber")) w.setPhone(_reader.readElementText()); else if (_reader.name() == QLatin1String("address")) w.setAddress(_reader.readElementText()); else if (_reader.name() == QLatin1String("TimeStamp")) w.setTimestamp(timeStamp()); else if (_reader.name() == QLatin1String("Style")) { style(ctx.dir, pointStyles, unused, unused2); id = QString(); } else if (_reader.name() == QLatin1String("StyleMap")) styleMap(pointStyles, unused, unused2); else if (_reader.name() == QLatin1String("Icon")) img = icon(); else if (_reader.name() == QLatin1String("Point")) point(w); else if (_reader.name() == QLatin1String("styleUrl")) id = styleUrl(); else _reader.skipCurrentElement(); } if (!w.coordinates().isNull()) { w.setStyle(pointStyles.value(id)); img.replace(re, "0"); if (!QUrl(img).scheme().isEmpty()) w.addLink(Link(img, "Photo Overlay")); else if (ctx.zip && Util::tempDir().isValid()) { QFileInfo fi(img); QByteArray id(ctx.path.toUtf8() + img.toUtf8()); QString path(Util::tempDir().path() + "/" + QString("%0.%1") .arg(QCryptographicHash::hash(id, QCryptographicHash::Sha1) .toHex(), QString(fi.suffix()))); QFile::rename(ctx.dir.absoluteFilePath(img), path); w.addImage(path); } else if (!ctx.zip) w.addImage(ctx.dir.absoluteFilePath(img)); waypoints.append(w); } } void KMLParser::placemark(const Ctx &ctx, QList &tracks, QList &areas, QVector &waypoints, PointStyleMap &pointStyles, PolygonStyleMap &polyStyles, LineStyleMap &lineStyles) { QString name, desc, phone, address, id; QDateTime timestamp; Waypoint *wp = 0; TrackData *tp = 0; Area *ap = 0; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("name")) name = _reader.readElementText(); else if (_reader.name() == QLatin1String("description")) desc = _reader.readElementText(); else if (_reader.name() == QLatin1String("phoneNumber")) phone = _reader.readElementText(); else if (_reader.name() == QLatin1String("address")) address = _reader.readElementText(); else if (_reader.name() == QLatin1String("TimeStamp")) timestamp = timeStamp(); else if (_reader.name() == QLatin1String("Style")) { style(ctx.dir, pointStyles, polyStyles, lineStyles); id = QString(); } else if (_reader.name() == QLatin1String("StyleMap")) styleMap(pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("MultiGeometry")) multiGeometry(tracks, areas, waypoints, name, desc, timestamp); else if (_reader.name() == QLatin1String("Point")) { waypoints.append(Waypoint()); wp = &waypoints.last(); point(*wp); } else if (_reader.name() == QLatin1String("LineString") || _reader.name() == QLatin1String("LinearRing")) { tracks.append(TrackData()); tp = &tracks.last(); tp->append(SegmentData()); lineString(tp->last()); } else if (_reader.name() == QLatin1String("Track")) { tracks.append(TrackData()); tp = &tracks.last(); tp->append(SegmentData()); track(tp->last()); } else if (_reader.name() == QLatin1String("MultiTrack")) { tracks.append(TrackData()); tp = &tracks.last(); multiTrack(*tp); } else if (_reader.name() == QLatin1String("Polygon")) { areas.append(Area()); ap = &areas.last(); polygon(*ap); } else if (_reader.name() == QLatin1String("styleUrl")) id = styleUrl(); else _reader.skipCurrentElement(); } if (wp) { wp->setName(name); wp->setDescription(desc); wp->setTimestamp(timestamp); wp->setAddress(address); wp->setPhone(phone); wp->setStyle(pointStyles.value(id)); } else if (tp) { tp->setName(name); tp->setDescription(desc); tp->setStyle(lineStyles.value(id)); } else if (ap) { ap->setName(name); ap->setDescription(desc); ap->setStyle(polyStyles.value(id)); } } QString KMLParser::icon() { QString path; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("href")) path = _reader.readElementText(); else _reader.skipCurrentElement(); } return path; } QString KMLParser::styleUrl() { QString id(_reader.readElementText()); return (id.at(0) == '#') ? id.right(id.size() - 1) : QString(); } void KMLParser::iconStyle(const QDir &dir, const QString &id, PointStyleMap &styles) { QPixmap img; QColor color(Qt::white); while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Icon")) img = QPixmap(dir.absoluteFilePath(icon())); else if (_reader.name() == QLatin1String("color")) color = QColor("#" + _reader.readElementText()); else _reader.skipCurrentElement(); } styles.insert(id, PointStyle(img, color)); } void KMLParser::polyStyle(const QString &id, PolygonStyleMap &styles) { QColor color(Qt::white); uint fill = 1, outline = 1; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("color")) color = QColor("#" + _reader.readElementText()); else if (_reader.name() == QLatin1String("fill")) fill = _reader.readElementText().toUInt(); else if (_reader.name() == QLatin1String("outline")) outline = _reader.readElementText().toUInt(); else _reader.skipCurrentElement(); } QPen pen = (color.isValid() && outline) ? QPen(color) : QPen(Qt::NoPen); QBrush brush = (color.isValid() && fill) ? QBrush(color) : QBrush(Qt::NoBrush); styles.insert(id, PolygonStyle(pen, brush)); } void KMLParser::lineStyle(const QString &id, LineStyleMap &styles) { QColor color(Qt::white); uint width = 1; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("color")) color = QColor("#" + _reader.readElementText()); else if (_reader.name() == QLatin1String("width")) width = _reader.readElementText().toUInt(); else _reader.skipCurrentElement(); } styles.insert(id, LineStyle(color, width)); } void KMLParser::styleMapPair(const QString &id, PointStyleMap &pointStyles, PolygonStyleMap &polyStyles, LineStyleMap &lineStyles) { QString key, url; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("key")) key = _reader.readElementText(); else if (_reader.name() == QLatin1String("styleUrl")) url = styleUrl(); else _reader.skipCurrentElement(); } if (key == "normal") { pointStyles.insert(id, pointStyles.value(url)); polyStyles.insert(id, polyStyles.value(url)); lineStyles.insert(id, lineStyles.value(url)); } } void KMLParser::styleMap(PointStyleMap &pointStyles, PolygonStyleMap &polyStyles, LineStyleMap &lineStyles) { QString id = _reader.attributes().value("id").toString(); while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Pair")) styleMapPair(id, pointStyles, polyStyles, lineStyles); else _reader.skipCurrentElement(); } } void KMLParser::style(const QDir &dir, PointStyleMap &pointStyles, PolygonStyleMap &polyStyles, LineStyleMap &lineStyles) { QString id = _reader.attributes().value("id").toString(); while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("IconStyle")) iconStyle(dir, id, pointStyles); else if (_reader.name() == QLatin1String("PolyStyle")) polyStyle(id, polyStyles); else if (_reader.name() == QLatin1String("LineStyle")) lineStyle(id, lineStyles); else _reader.skipCurrentElement(); } } void KMLParser::folder(const Ctx &ctx, QList &tracks, QList &areas, QVector &waypoints, PointStyleMap &pointStyles, PolygonStyleMap &polyStyles, LineStyleMap &lineStyles) { while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Document")) document(ctx, tracks, areas, waypoints); else if (_reader.name() == QLatin1String("Folder")) folder(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("Placemark")) placemark(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("PhotoOverlay")) photoOverlay(ctx, waypoints, pointStyles); else _reader.skipCurrentElement(); } } void KMLParser::document(const Ctx &ctx, QList &tracks, QList &areas, QVector &waypoints) { PointStyleMap pointStyles; PolygonStyleMap polyStyles; LineStyleMap lineStyles; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Document")) document(ctx, tracks, areas, waypoints); else if (_reader.name() == QLatin1String("Folder")) folder(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("Placemark")) placemark(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("PhotoOverlay")) photoOverlay(ctx, waypoints, pointStyles); else if (_reader.name() == QLatin1String("Style")) style(ctx.dir, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("StyleMap")) styleMap(pointStyles, polyStyles, lineStyles); else _reader.skipCurrentElement(); } } void KMLParser::kml(const Ctx &ctx, QList &tracks, QList &areas, QVector &waypoints) { PointStyleMap pointStyles; PolygonStyleMap polyStyles; LineStyleMap lineStyles; while (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("Document")) document(ctx, tracks, areas, waypoints); else if (_reader.name() == QLatin1String("Folder")) folder(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("Placemark")) placemark(ctx, tracks, areas, waypoints, pointStyles, polyStyles, lineStyles); else if (_reader.name() == QLatin1String("PhotoOverlay")) photoOverlay(ctx, waypoints, pointStyles); else _reader.skipCurrentElement(); } } bool KMLParser::parse(QFile *file, QList &tracks, QList &routes, QList &areas, QVector &waypoints) { Q_UNUSED(routes); QFileInfo fi(*file); _reader.clear(); if (isZIP(file)) { QZipReader zip(fi.absoluteFilePath(), QIODevice::ReadOnly); QTemporaryDir tempDir; if (!tempDir.isValid() || !zip.extractAll(tempDir.path())) _reader.raiseError("Error extracting ZIP file"); else { QDir zipDir(tempDir.path()); QFileInfoList files(zipDir.entryInfoList(QStringList("*.kml"), QDir::Files)); if (files.isEmpty()) _reader.raiseError("No KML file found in ZIP file"); else { QFile kmlFile(files.first().absoluteFilePath()); if (!kmlFile.open(QIODevice::ReadOnly)) _reader.raiseError("Error opening KML file"); else { _reader.setDevice(&kmlFile); if (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("kml")) kml(Ctx(fi.absoluteFilePath(), zipDir, true), tracks, areas, waypoints); else _reader.raiseError("Not a KML file"); } } } } } else { file->reset(); _reader.setDevice(file); if (_reader.readNextStartElement()) { if (_reader.name() == QLatin1String("kml")) kml(Ctx(fi.absoluteFilePath(), fi.absoluteDir(), false), tracks, areas, waypoints); else _reader.raiseError("Not a KML file"); } } return !_reader.error(); }