diff --git a/gpxsee.pro b/gpxsee.pro index e6a39d48..44fa8f96 100644 --- a/gpxsee.pro +++ b/gpxsee.pro @@ -186,7 +186,8 @@ HEADERS += src/common/config.h \ src/map/IMG/textitem.h \ src/map/IMG/label.h \ src/data/csv.h \ - src/data/cupparser.h + src/data/cupparser.h \ + src/data/gpiparser.h SOURCES += src/main.cpp \ src/GUI/popup.cpp \ src/common/coordinates.cpp \ @@ -321,7 +322,8 @@ SOURCES += src/main.cpp \ src/map/IMG/textitem.cpp \ src/data/csv.cpp \ src/data/cupparser.cpp \ - src/GUI/graphicsscene.cpp + src/GUI/graphicsscene.cpp \ + src/data/gpiparser.cpp greaterThan(QT_MAJOR_VERSION, 4) { HEADERS += src/data/geojsonparser.h diff --git a/src/data/data.cpp b/src/data/data.cpp index bc3e202f..4ff6aa04 100644 --- a/src/data/data.cpp +++ b/src/data/data.cpp @@ -18,6 +18,7 @@ #endif // ENABLE_GEOJSON #include "exifparser.h" #include "cupparser.h" +#include "gpiparser.h" #include "dem.h" #include "data.h" @@ -39,6 +40,7 @@ static GeoJSONParser geojson; #endif // ENABLE_GEOJSON static EXIFParser exif; static CUPParser cup; +static GPIParser gpi; static QHash parsers() { @@ -63,6 +65,7 @@ static QHash parsers() hash.insert("jpeg", &exif); hash.insert("jpg", &exif); hash.insert("cup", &cup); + hash.insert("gpi", &gpi); return hash; } @@ -177,6 +180,7 @@ QString Data::formats() #ifdef ENABLE_GEOJSON + qApp->translate("Data", "GeoJSON files") + " (*.geojson *.json);;" #endif // ENABLE_GEOJSON + + qApp->translate("Data", "GPI files") + " (*.gpi);;" + qApp->translate("Data", "GPX files") + " (*.gpx);;" + qApp->translate("Data", "IGC files") + " (*.igc);;" + qApp->translate("Data", "JPEG images") + " (*.jpg *.jpeg);;" diff --git a/src/data/gpiparser.cpp b/src/data/gpiparser.cpp new file mode 100644 index 00000000..8eb52ce0 --- /dev/null +++ b/src/data/gpiparser.cpp @@ -0,0 +1,440 @@ +#include +#include +#include +#include +#include "gpiparser.h" + + +struct RecordHeader { + quint16 type; + quint16 flags; + quint32 size; + quint32 extra; +}; + +class TranslatedString { +public: + TranslatedString() {} + TranslatedString(const QString &lang, const QString &str) + : _lang(lang), _str(str) {} + + const QString &str() const {return _str;} + const QString &lang() const {return _lang;} + +private: + QString _lang; + QString _str; +}; + +static inline double toWGS(qint32 v) +{ + return (double)(((double)v / (double)(1U<<31)) * (double)180); +} + +static quint16 nextHeaderType(QDataStream &stream) +{ + quint16 type = 0; + stream.device()->peek((char*)&type, sizeof(type)); + return qFromLittleEndian(type); +} + +static quint8 readRecordHeader(QDataStream &stream, RecordHeader &hdr) +{ + stream >> hdr.type >> hdr.flags >> hdr.size; + if (hdr.flags & 0xA) + stream >> hdr.extra; + return (hdr.flags & 0xA) ? 12 : 8; +} + +static quint32 skipRecord(QDataStream &stream) +{ + RecordHeader rh; + quint8 rs = readRecordHeader(stream, rh); + stream.skipRawData(rh.size); + + return rs + rh.size; +} + +static quint32 readFprsRecord(QDataStream &stream) +{ + RecordHeader rh; + quint16 s1; + quint8 rs, s2, s3, s4; + + rs = readRecordHeader(stream, rh); + stream >> s1 >> s2 >> s3 >> s4; + + return rs + 5; +} + +static quint16 readString(QDataStream &stream, QTextCodec *codec, QString &str) +{ + quint16 len; + stream >> len; + QByteArray ba; + ba.resize(len); + stream.readRawData(ba.data(), len); + str = codec ? codec->toUnicode(ba) : QString::fromLatin1(ba); + + return len + 2; +} + +static quint32 readTranslatedObjects(QDataStream &stream, QTextCodec *codec, + QList &objects) +{ + qint32 size = 0, ret; + char lang[2]; + + objects.clear(); + + stream >> size; + ret = size + 4; + while (size > 0) { + QString str; + stream.readRawData(lang, sizeof(lang)); + size -= readString(stream, codec, str) + 2; + objects.append(TranslatedString(lang, str)); + } + + if (size < 0) + stream.setStatus(QDataStream::ReadCorruptData); + + return ret; +} + +static quint32 readDescription(QDataStream &stream, QTextCodec *codec, + Waypoint &waypoint) +{ + RecordHeader rh; + quint8 rs; + quint32 ds; + QList obj; + + rs = readRecordHeader(stream, rh); + ds = readTranslatedObjects(stream, codec, obj); + if (!obj.isEmpty()) + waypoint.setDescription(obj.first().str()); + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +static quint32 readNotes(QDataStream &stream, QTextCodec *codec, + Waypoint &waypoint) +{ + RecordHeader rh; + quint8 rs, s1; + quint32 ds = 1; + + rs = readRecordHeader(stream, rh); + stream >> s1; + if (s1 & 0x1) { + QList obj; + ds += readTranslatedObjects(stream, codec, obj); + if (!obj.isEmpty() && waypoint.description().isNull()) + waypoint.setDescription(obj.first().str()); + } + if (s1 & 0x2) { + QString str; + ds += readString(stream, codec, str); + if (!str.isEmpty() && waypoint.description().isNull()) + waypoint.setDescription(str); + } + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +static quint32 readContact(QDataStream &stream, QTextCodec *codec, + Waypoint &waypoint) +{ + RecordHeader rh; + quint8 rs; + quint16 s1; + quint32 ds = 2; + QString str; + QList obj; + + rs = readRecordHeader(stream, rh); + stream >> s1; + + if (s1 & 0x1) // phone + ds += readString(stream, codec, str); + if (s1 & 0x2) // phone2 + ds += readString(stream, codec, str); + if (s1 & 0x4) // fax + ds += readString(stream, codec, str); + if (s1 & 0x8) // mail + ds += readString(stream, codec, str); + if (s1 & 0x10) { // web + ds += readString(stream, codec, str); + QUrl url(str); + waypoint.addLink(Link(url.scheme().isEmpty() + ? "http://" + str : str, str)); + } + if (s1 & 0x20) // unknown + ds += readTranslatedObjects(stream, codec, obj); + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +static quint32 readPOI(QDataStream &stream, QTextCodec *codec, + QVector &waypoints) +{ + RecordHeader rh; + quint8 rs; + quint32 ds; + qint32 s1, s2; + quint16 s3; + QList obj; + + rs = readRecordHeader(stream, rh); + stream >> s1 >> s2 >> s3; + stream.skipRawData(s3); + ds = 10 + s3; + ds += readTranslatedObjects(stream, codec, obj); + + waypoints.append(Waypoint(Coordinates(toWGS(s2), toWGS(s1)))); + if (!obj.isEmpty()) + waypoints.last().setName(obj.first().str()); + + while (ds < rh.size) { + switch(nextHeaderType(stream)) { + case 10: + ds += readDescription(stream, codec, waypoints.last()); + break; + case 12: + ds += readContact(stream, codec, waypoints.last()); + break; + case 14: + ds += readNotes(stream, codec, waypoints.last()); + break; + default: + ds += skipRecord(stream); + } + } + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +static quint32 readSpatialIndex(QDataStream &stream, QTextCodec *codec, + QVector &waypoints) +{ + RecordHeader rh; + quint32 ds, s5; + qint32 top, right, bottom, left; + quint16 s6; + quint8 rs; + + rs = readRecordHeader(stream, rh); + stream >> top >> right >> bottom >> left >> s5 >> s6; + stream.skipRawData(s6); + ds = 22 + s6; + if (rh.flags & 0x8) { + while (ds < rh.size) { + switch(nextHeaderType(stream)) { + case 2: + ds += readPOI(stream, codec, waypoints); + break; + case 8: + ds += readSpatialIndex(stream, codec, waypoints); + break; + default: + ds += skipRecord(stream); + } + } + } + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +static quint32 readFileDataRecord(QDataStream &stream, QTextCodec *codec) +{ + RecordHeader rh; + quint32 ds, s1; + quint16 s2, s3; + quint8 rs; + QList obj; + + rs = readRecordHeader(stream, rh); + stream >> s1 >> s2 >> s3; + ds = 8; + ds += readTranslatedObjects(stream, codec, obj); + ds += readTranslatedObjects(stream, codec, obj); + + if (s1 & 0x10) { + quint8 ss1, ss2; + quint16 ss3; + stream >> ss1 >> ss2 >> ss3; + ds += 4; + } + if (s1 & 0x100) { + quint32 ss1; + stream >> ss1; + if (ss1) + stream.skipRawData(ss1); + ds += ss1 + 4; + } + if (s1 & 0x400) { + QString str; + ds += readString(stream, codec, str); + } + if (s1 & 0x400000) { + quint16 ss1; + stream >> ss1; + if (ss1) + stream.skipRawData(ss1); + ds += ss1 + 2; + } + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); + + return rs + rh.size; +} + +bool GPIParser::readFileHeader(QDataStream &stream) +{ + RecordHeader rh; + quint32 ds, s7; + quint16 s10; + quint8 s5, s6, s8, s9; + char magic[6]; + + readRecordHeader(stream, rh); + stream.readRawData(magic, sizeof(magic)); + if (memcmp(magic, "GRMREC", sizeof(magic))) { + _errorString = "Not a GPI file"; + return false; + } + stream >> s5 >> s6 >> s7 >> s8 >> s9 >> s10; + stream.skipRawData(s10); + ds = sizeof(magic) + 10 + s10; + if (rh.flags & 8) + ds += readFprsRecord(stream); + + if (s8 & 0x4) { + _errorString = "Encrypted GPI files not supported"; + return false; + } + + if (stream.status() != QDataStream::Ok || ds != rh.size) { + _errorString = "Invalid file header"; + return false; + } else + return true; +} + +bool GPIParser::readGPIHeader(QDataStream &stream, QTextCodec *codec) +{ + RecordHeader rh; + char m1[6], m2[2]; + quint16 codepage = 0; + quint8 s2, s3; + quint32 ds; + + readRecordHeader(stream, rh); + stream.readRawData(m1, sizeof(m1)); + stream.readRawData(m2, sizeof(m2)); + stream >> codepage >> s2 >> s3; + ds = sizeof(m1) + sizeof(m2) + 4; + + if (codepage == 65001) + codec = QTextCodec::codecForName("UTF-8"); + else if (codepage == 0) + codec = 0; + else + codec = QTextCodec::codecForName(QString("CP%1").arg(codepage) + .toLatin1()); + + if (s2 & 0x10) + ds += readFileDataRecord(stream, codec); + + if (stream.status() != QDataStream::Ok || ds != rh.size) { + _errorString = "Invalid GPI header"; + return false; + } else + return true; +} + +void GPIParser::readPOIDatabase(QDataStream &stream, QTextCodec *codec, + QVector &waypoints) +{ + RecordHeader rh; + QList obj; + quint32 ds; + + readRecordHeader(stream, rh); + ds = readTranslatedObjects(stream, codec, obj); + ds += readSpatialIndex(stream, codec, waypoints); + if (rh.flags & 0x8) { + while (ds < rh.size) { + switch(nextHeaderType(stream)) { + case 5: // symbol + case 7: // category + default: + ds += skipRecord(stream); + } + } + } + + if (ds != rh.size) + stream.setStatus(QDataStream::ReadCorruptData); +} + +bool GPIParser::readEntry(QDataStream &stream, QTextCodec *codec, + QVector &waypoints) +{ + switch (nextHeaderType(stream)) { + case 0x09: // POI database + readPOIDatabase(stream, codec, waypoints); + break; + case 0xffff: // EOF + skipRecord(stream); + return false; + case 0x16: // route + case 0x15: // info header + default: + skipRecord(stream); + } + + return true; +} + +bool GPIParser::parse(QFile *file, QList &tracks, + QList &routes, QList &polygons, QVector &waypoints) +{ + Q_UNUSED(tracks); + Q_UNUSED(routes); + Q_UNUSED(polygons); + QDataStream stream(file); + QTextCodec *codec = 0; + + stream.setByteOrder(QDataStream::LittleEndian); + + if (!readFileHeader(stream) || !readGPIHeader(stream, codec)) + return false; + + while (stream.status() == QDataStream::Ok) + if (!readEntry(stream, codec, waypoints)) + break; + + if (!stream.atEnd()) { + _errorString = "Invalid/corrupted GPI data"; + return false; + } else + return true; +} diff --git a/src/data/gpiparser.h b/src/data/gpiparser.h new file mode 100644 index 00000000..74d041cd --- /dev/null +++ b/src/data/gpiparser.h @@ -0,0 +1,28 @@ +#ifndef GPIPARSER_H +#define GPIPARSER_H + +#include "parser.h" + +class QDataStream; +class QTextCodec; + +class GPIParser : public Parser +{ +public: + bool parse(QFile *file, QList &tracks, QList &routes, + QList &polygons, QVector &waypoints); + QString errorString() const {return _errorString;} + int errorLine() const {return 0;} + +private: + bool readFileHeader(QDataStream &stream); + bool readGPIHeader(QDataStream &stream, QTextCodec *codec); + bool readEntry(QDataStream &stream, QTextCodec *codec, + QVector &waypoints); + void readPOIDatabase(QDataStream &stream, QTextCodec *codec, + QVector &waypoints); + + QString _errorString; +}; + +#endif // GPIPARSER_H