6#include <QtCore/QRegularExpression>
7#include <QtCore/QScopeGuard>
8#include <QtCore/QTextStream>
15 constexpr const char *
_errorPrefix = QT_TR_NOOP(
"SHP file load failed. %1");
20 SAFile qfileOpen(
const char *filename,
const char *access,
void *pvUserData)
24 if (!filename || !access) {
25 qCWarning(SHPFileHelperLog) <<
"QFile open called with null filename or access mode";
30 if (access[0] !=
'r') {
31 qCWarning(SHPFileHelperLog) <<
"QFile hooks only support read mode, requested:" << access;
35 auto *file =
new QFile(QString::fromUtf8(filename));
36 if (!file->open(QIODevice::ReadOnly)) {
37 qCWarning(SHPFileHelperLog) <<
"Failed to open file:" << filename << file->errorString();
42 return reinterpret_cast<SAFile
>(file);
45 SAOffset qfileRead(
void *p, SAOffset size, SAOffset nmemb, SAFile file)
47 if (!file || !p || size == 0) {
50 auto *qfile =
reinterpret_cast<QFile *
>(file);
51 const qint64 bytesRequested =
static_cast<qint64
>(size) *
static_cast<qint64
>(nmemb);
52 const qint64 bytesRead = qfile->read(
static_cast<char *
>(p), bytesRequested);
56 return static_cast<SAOffset
>(bytesRead /
static_cast<qint64
>(size));
59 SAOffset qfileWrite(
const void *p, SAOffset size, SAOffset nmemb, SAFile file)
65 qCWarning(SHPFileHelperLog) <<
"QFile write not supported - shapefiles are read-only in QGC";
69 SAOffset qfileSeek(SAFile file, SAOffset offset,
int whence)
74 auto *qfile =
reinterpret_cast<QFile *
>(file);
79 newPos =
static_cast<qint64
>(offset);
82 newPos = qfile->pos() +
static_cast<qint64
>(offset);
85 newPos = qfile->size() +
static_cast<qint64
>(offset);
95 return qfile->seek(newPos) ? 0 : -1;
98 SAOffset qfileTell(SAFile file)
103 auto *qfile =
reinterpret_cast<QFile *
>(file);
104 return static_cast<SAOffset
>(qfile->pos());
107 int qfileFlush(SAFile file)
114 int qfileClose(SAFile file)
119 auto *qfile =
reinterpret_cast<QFile *
>(file);
125 void setupQFileHooks(SAHooks *hooks)
127 SASetupDefaultHooks(hooks);
128 hooks->FOpen = qfileOpen;
129 hooks->FRead = qfileRead;
130 hooks->FWrite = qfileWrite;
131 hooks->FSeek = qfileSeek;
132 hooks->FTell = qfileTell;
133 hooks->FFlush = qfileFlush;
134 hooks->FClose = qfileClose;
135 hooks->Error = [](
const char *message) {
136 qCWarning(SHPFileHelperLog) <<
"SHP Error:" << message;
151 SHPHandle
_loadShape(
const QString &shpFile,
int *utmZone,
bool *utmSouthernHemisphere, QString &
errorString);
159 if (!shpFile.endsWith(QStringLiteral(
".shp"), Qt::CaseInsensitive)) {
160 errorString = QString(_errorPrefix).arg(QString(QT_TRANSLATE_NOOP(
"SHP",
"File is not a .shp file: %1")).arg(shpFile));
164 const QString prjFilename = shpFile.left(shpFile.length() - 4) + QStringLiteral(
".prj");
165 QFile prjFile(prjFilename);
166 if (!prjFile.exists()) {
167 errorString = QString(_errorPrefix).arg(QString(QT_TRANSLATE_NOOP(
"SHP",
"File not found: %1")).arg(prjFilename));
171 if (!prjFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
172 errorString = QString(_errorPrefix).arg(QObject::tr(
"PRJ file open failed: %1").arg(prjFile.errorString()));
176 QTextStream strm(&prjFile);
177 const QString line = strm.readLine();
178 if (line.startsWith(QStringLiteral(
"GEOGCS[\"GCS_WGS_1984\","))) {
180 *utmSouthernHemisphere =
false;
181 }
else if (line.startsWith(QStringLiteral(
"PROJCS[\"WGS_1984_UTM_Zone_"))) {
182 static const QRegularExpression regEx(QStringLiteral(
"^PROJCS\\[\"WGS_1984_UTM_Zone_(\\d{1,2})([NS])"));
183 const QRegularExpressionMatch regExMatch = regEx.match(line);
184 const QStringList rgCapture = regExMatch.capturedTexts();
185 if (rgCapture.count() == 3) {
186 const int zone = rgCapture[1].toInt();
187 if ((zone >= 1) && (zone <= 60)) {
189 *utmSouthernHemisphere = (rgCapture[2] == QStringLiteral(
"S"));
194 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"UTM projection is not in supported format. Must be PROJCS[\"WGS_1984_UTM_Zone_##N/S"));
199 QString projectionName;
200 static const QRegularExpression nameRegEx(QStringLiteral(
"^(?:GEOGCS|PROJCS)\\[\"([^\"]+)\""));
201 const QRegularExpressionMatch nameMatch = nameRegEx.match(line);
202 if (nameMatch.hasMatch()) {
203 projectionName = nameMatch.captured(1);
206 if (!projectionName.isEmpty()) {
208 QString(QT_TRANSLATE_NOOP(
"SHP",
"Unsupported projection: %1. Supported projections are: WGS84 (GEOGCS[\"GCS_WGS_1984\"]) and UTM (PROJCS[\"WGS_1984_UTM_Zone_##N/S\"]). Convert your shapefile to WGS84 using QGIS or ogr2ogr."))
209 .arg(projectionName));
212 QT_TRANSLATE_NOOP(
"SHP",
"Unable to parse projection from PRJ file. Supported projections are: WGS84 (GEOGCS[\"GCS_WGS_1984\"]) and UTM (PROJCS[\"WGS_1984_UTM_Zone_##N/S\"])."));
229 setupQFileHooks(&sHooks);
231 SHPHandle shpHandle = SHPOpenLL(shpFile.toUtf8().constData(),
"rb", &sHooks);
233 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"SHPOpen failed."));
241 using ShapeType = ShapeFileHelper::ShapeType;
243 ShapeType shapeType = ShapeType::Error;
248 bool utmSouthernHemisphere;
251 Q_CHECK_PTR(shpHandle);
254 SHPGetInfo(shpHandle, &cEntities, &type,
nullptr,
nullptr);
255 qCDebug(SHPFileHelperLog) <<
"SHPGetInfo" << shpHandle << cEntities << type;
257 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"No entities found."));
258 }
else if (type == SHPT_POLYGON || type == SHPT_POLYGONZ) {
259 shapeType = ShapeType::Polygon;
260 }
else if (type == SHPT_ARC || type == SHPT_ARCZ) {
261 shapeType = ShapeType::Polyline;
262 }
else if (type == SHPT_POINT || type == SHPT_POINTZ) {
263 shapeType = ShapeType::Point;
265 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"No supported types found."));
281 bool utmSouthernHemisphere;
288 SHPGetInfo(shpHandle, &cEntities, &type,
nullptr,
nullptr);
297 bool utmSouthernHemisphere =
false;
298 SHPHandle shpHandle =
nullptr;
303 auto cleanup = qScopeGuard([&]() {
304 if (shpHandle) SHPClose(shpHandle);
311 Q_CHECK_PTR(shpHandle);
313 int cEntities, shapeType;
314 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
315 if (shapeType != SHPT_POLYGON && shapeType != SHPT_POLYGONZ) {
316 errorString = QString(_errorPrefix).arg(QObject::tr(
"File contains %1, expected Polygon.").arg(SHPTypeName(shapeType)));
320 const bool hasAltitude = (shapeType == SHPT_POLYGONZ);
322 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
323 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
325 qCWarning(SHPFileHelperLog) <<
"Failed to read polygon entity" << entityIdx;
328 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
331 SHPRewindObject(shpHandle, shpObject);
337 const int firstPartEnd = (shpObject->nParts > 1) ? shpObject->panPartStart[1] : shpObject->nVertices;
338 if (shpObject->nParts > 1) {
339 qCDebug(SHPFileHelperLog) <<
"Polygon entity" << entityIdx <<
"has" << shpObject->nParts
340 <<
"parts; using outer ring only (" << firstPartEnd <<
"vertices)";
343 QList<QGeoCoordinate> vertices;
344 const bool entityHasAltitude = hasAltitude && shpObject->padfZ;
346 for (
int i = 0; i < firstPartEnd; i++) {
347 QGeoCoordinate coord;
349 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[i], shpObject->padfY[i], utmZone, utmSouthernHemisphere, coord)) {
350 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for entity" << entityIdx <<
"vertex" << i;
354 coord.setLatitude(shpObject->padfY[i]);
355 coord.setLongitude(shpObject->padfX[i]);
357 if (entityHasAltitude) {
358 coord.setAltitude(shpObject->padfZ[i]);
360 vertices.append(coord);
363 if (vertices.count() < 3) {
364 qCWarning(SHPFileHelperLog) <<
"Skipping polygon entity" << entityIdx <<
"with less than 3 vertices";
369 if (filterMeters > 0) {
370 const QGeoCoordinate firstVertex = vertices.first();
374 constexpr double kClosureThreshold = 0.01;
375 const bool hadExplicitClosure = vertices.last().distanceTo(firstVertex) < kClosureThreshold;
379 while (i < (vertices.count() - 1)) {
380 if ((vertices.count() > 3) && (vertices[i].distanceTo(vertices[i + 1]) < filterMeters)) {
381 vertices.removeAt(i + 1);
390 if (hadExplicitClosure && vertices.count() > 3 &&
391 vertices.last().distanceTo(firstVertex) < kClosureThreshold) {
392 vertices.removeLast();
396 polygons.append(vertices);
399 if (polygons.isEmpty()) {
400 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"No valid polygons found."));
410 bool utmSouthernHemisphere =
false;
411 SHPHandle shpHandle =
nullptr;
416 auto cleanup = qScopeGuard([&]() {
417 if (shpHandle) SHPClose(shpHandle);
424 Q_CHECK_PTR(shpHandle);
426 int cEntities, shapeType;
427 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
428 if (shapeType != SHPT_ARC && shapeType != SHPT_ARCZ) {
429 errorString = QString(_errorPrefix).arg(QObject::tr(
"File contains %1, expected Arc.").arg(SHPTypeName(shapeType)));
433 const bool hasAltitude = (shapeType == SHPT_ARCZ);
435 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
436 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
438 qCWarning(SHPFileHelperLog) <<
"Failed to read polyline entity" << entityIdx;
441 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
446 const int firstPartEnd = (shpObject->nParts > 1) ? shpObject->panPartStart[1] : shpObject->nVertices;
447 if (shpObject->nParts > 1) {
448 qCDebug(SHPFileHelperLog) <<
"Polyline entity" << entityIdx <<
"has" << shpObject->nParts
449 <<
"parts; using first part only (" << firstPartEnd <<
"vertices)";
452 QList<QGeoCoordinate> vertices;
453 const bool entityHasAltitude = hasAltitude && shpObject->padfZ;
455 for (
int i = 0; i < firstPartEnd; i++) {
456 QGeoCoordinate coord;
458 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[i], shpObject->padfY[i], utmZone, utmSouthernHemisphere, coord)) {
459 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for entity" << entityIdx <<
"vertex" << i;
463 coord.setLatitude(shpObject->padfY[i]);
464 coord.setLongitude(shpObject->padfX[i]);
466 if (entityHasAltitude) {
467 coord.setAltitude(shpObject->padfZ[i]);
469 vertices.append(coord);
472 if (vertices.count() < 2) {
473 qCWarning(SHPFileHelperLog) <<
"Skipping polyline entity" << entityIdx <<
"with less than 2 vertices";
478 if (filterMeters > 0) {
480 while (i < (vertices.count() - 1)) {
481 if ((vertices.count() > 2) && (vertices[i].distanceTo(vertices[i+1]) < filterMeters)) {
482 vertices.removeAt(i+1);
489 polylines.append(vertices);
492 if (polylines.isEmpty()) {
493 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"No valid polylines found."));
503 bool utmSouthernHemisphere =
false;
504 SHPHandle shpHandle =
nullptr;
509 auto cleanup = qScopeGuard([&]() {
510 if (shpHandle) SHPClose(shpHandle);
517 Q_CHECK_PTR(shpHandle);
519 int cEntities, shapeType;
520 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
521 if (shapeType != SHPT_POINT && shapeType != SHPT_POINTZ) {
522 errorString = QString(_errorPrefix).arg(QObject::tr(
"File contains %1, expected Point.").arg(SHPTypeName(shapeType)));
526 const bool hasAltitude = (shapeType == SHPT_POINTZ);
528 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
529 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
531 qCWarning(SHPFileHelperLog) <<
"Failed to read point entity" << entityIdx;
534 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
537 if (shpObject->nVertices != 1) {
538 qCWarning(SHPFileHelperLog) <<
"Skipping point entity" << entityIdx <<
"with unexpected vertex count:" << shpObject->nVertices;
542 QGeoCoordinate coord;
544 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[0], shpObject->padfY[0], utmZone, utmSouthernHemisphere, coord)) {
545 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for point entity" << entityIdx;
549 coord.setLatitude(shpObject->padfY[0]);
550 coord.setLongitude(shpObject->padfX[0]);
553 if (hasAltitude && shpObject->padfZ) {
554 coord.setAltitude(shpObject->padfZ[0]);
557 points.append(coord);
560 if (points.isEmpty()) {
561 errorString = QString(_errorPrefix).arg(QT_TRANSLATE_NOOP(
"SHP",
"No valid points found."));
Geographic coordinate conversion utilities using GeographicLib.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
constexpr const char * _errorPrefix
bool convertUTMToGeo(double easting, double northing, int zone, bool southhemi, QGeoCoordinate &coord)
ShapeFileHelper::ShapeType determineShapeType(const QString &file, QString &errorString)
bool loadPointsFromFile(const QString &shpFile, QList< QGeoCoordinate > &points, QString &errorString)
Load all point entities.
int getEntityCount(const QString &shpFile, QString &errorString)
Get the number of entities in the shapefile.
bool loadPolygonsFromFile(const QString &shpFile, QList< QList< QGeoCoordinate > > &polygons, QString &errorString, double filterMeters=ShapeFileHelper::kDefaultVertexFilterMeters)
SHPHandle _loadShape(const QString &shpFile, int *utmZone, bool *utmSouthernHemisphere, QString &errorString)
bool _validateSHPFiles(const QString &shpFile, int *utmZone, bool *utmSouthernHemisphere, QString &errorString)
bool loadPolylinesFromFile(const QString &shpFile, QList< QList< QGeoCoordinate > > &polylines, QString &errorString, double filterMeters=ShapeFileHelper::kDefaultVertexFilterMeters)