5#include <QtCore/QCoreApplication>
7#include <QtCore/QRegularExpression>
8#include <QtCore/QScopeGuard>
9#include <QtCore/QTextStream>
16 constexpr const char *
_errorPrefix = QT_TRANSLATE_NOOP(
"SHPFileHelper",
"SHP file load failed. %1");
21 SAFile qfileOpen(
const char *filename,
const char *access,
void *pvUserData)
25 if (!filename || !access) {
26 qCWarning(SHPFileHelperLog) <<
"QFile open called with null filename or access mode";
31 if (access[0] !=
'r') {
32 qCWarning(SHPFileHelperLog) <<
"QFile hooks only support read mode, requested:" << access;
36 auto *file =
new QFile(QString::fromUtf8(filename));
37 if (!file->open(QIODevice::ReadOnly)) {
38 qCWarning(SHPFileHelperLog) <<
"Failed to open file:" << filename << file->errorString();
43 return reinterpret_cast<SAFile
>(file);
46 SAOffset qfileRead(
void *p, SAOffset size, SAOffset nmemb, SAFile file)
48 if (!file || !p || size == 0) {
51 auto *qfile =
reinterpret_cast<QFile *
>(file);
52 const qint64 bytesRequested =
static_cast<qint64
>(size) *
static_cast<qint64
>(nmemb);
53 const qint64 bytesRead = qfile->read(
static_cast<char *
>(p), bytesRequested);
57 return static_cast<SAOffset
>(bytesRead /
static_cast<qint64
>(size));
60 SAOffset qfileWrite(
const void *p, SAOffset size, SAOffset nmemb, SAFile file)
66 qCWarning(SHPFileHelperLog) <<
"QFile write not supported - shapefiles are read-only in QGC";
70 SAOffset qfileSeek(SAFile file, SAOffset offset,
int whence)
75 auto *qfile =
reinterpret_cast<QFile *
>(file);
80 newPos =
static_cast<qint64
>(offset);
83 newPos = qfile->pos() +
static_cast<qint64
>(offset);
86 newPos = qfile->size() +
static_cast<qint64
>(offset);
96 return qfile->seek(newPos) ? 0 : -1;
99 SAOffset qfileTell(SAFile file)
104 auto *qfile =
reinterpret_cast<QFile *
>(file);
105 return static_cast<SAOffset
>(qfile->pos());
108 int qfileFlush(SAFile file)
115 int qfileClose(SAFile file)
120 auto *qfile =
reinterpret_cast<QFile *
>(file);
126 void setupQFileHooks(SAHooks *hooks)
128 SASetupDefaultHooks(hooks);
129 hooks->FOpen = qfileOpen;
130 hooks->FRead = qfileRead;
131 hooks->FWrite = qfileWrite;
132 hooks->FSeek = qfileSeek;
133 hooks->FTell = qfileTell;
134 hooks->FFlush = qfileFlush;
135 hooks->FClose = qfileClose;
136 hooks->Error = [](
const char *message) {
137 qCWarning(SHPFileHelperLog) <<
"SHP Error:" << message;
152 SHPHandle
_loadShape(
const QString &shpFile,
int *utmZone,
bool *utmSouthernHemisphere, QString &
errorString);
160 if (!shpFile.endsWith(QStringLiteral(
".shp"), Qt::CaseInsensitive)) {
161 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"File is not a .shp file: %1").arg(shpFile));
165 const QString prjFilename = shpFile.left(shpFile.length() - 4) + QStringLiteral(
".prj");
166 QFile prjFile(prjFilename);
167 if (!prjFile.exists()) {
168 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"File not found: %1").arg(prjFilename));
172 if (!prjFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
173 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHPFileHelper",
"PRJ file open failed: %1").arg(prjFile.errorString()));
177 QTextStream strm(&prjFile);
178 const QString line = strm.readLine();
179 if (line.startsWith(QStringLiteral(
"GEOGCS[\"GCS_WGS_1984\","))) {
181 *utmSouthernHemisphere =
false;
182 }
else if (line.startsWith(QStringLiteral(
"PROJCS[\"WGS_1984_UTM_Zone_"))) {
183 static const QRegularExpression regEx(QStringLiteral(
"^PROJCS\\[\"WGS_1984_UTM_Zone_(\\d{1,2})([NS])"));
184 const QRegularExpressionMatch regExMatch = regEx.match(line);
185 const QStringList rgCapture = regExMatch.capturedTexts();
186 if (rgCapture.count() == 3) {
187 const int zone = rgCapture[1].toInt();
188 if ((zone >= 1) && (zone <= 60)) {
190 *utmSouthernHemisphere = (rgCapture[2] == QStringLiteral(
"S"));
195 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"UTM projection is not in supported format. Must be PROJCS[\"WGS_1984_UTM_Zone_##N/S"));
200 QString projectionName;
201 static const QRegularExpression nameRegEx(QStringLiteral(
"^(?:GEOGCS|PROJCS)\\[\"([^\"]+)\""));
202 const QRegularExpressionMatch nameMatch = nameRegEx.match(line);
203 if (nameMatch.hasMatch()) {
204 projectionName = nameMatch.captured(1);
207 if (!projectionName.isEmpty()) {
208 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(
209 QCoreApplication::translate(
"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.")
210 .arg(projectionName));
212 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(
213 QCoreApplication::translate(
"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\"])."));
230 setupQFileHooks(&sHooks);
232 SHPHandle shpHandle = SHPOpenLL(shpFile.toUtf8().constData(),
"rb", &sHooks);
234 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"SHPOpen failed."));
244 ShapeType shapeType = ShapeType::Error;
249 bool utmSouthernHemisphere;
252 Q_CHECK_PTR(shpHandle);
255 SHPGetInfo(shpHandle, &cEntities, &type,
nullptr,
nullptr);
256 qCDebug(SHPFileHelperLog) <<
"SHPGetInfo" << shpHandle << cEntities << type;
258 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"No entities found."));
259 }
else if (type == SHPT_POLYGON || type == SHPT_POLYGONZ) {
260 shapeType = ShapeType::Polygon;
261 }
else if (type == SHPT_ARC || type == SHPT_ARCZ) {
262 shapeType = ShapeType::Polyline;
263 }
else if (type == SHPT_POINT || type == SHPT_POINTZ) {
264 shapeType = ShapeType::Point;
266 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"No supported types found."));
282 bool utmSouthernHemisphere;
289 SHPGetInfo(shpHandle, &cEntities, &type,
nullptr,
nullptr);
298 bool utmSouthernHemisphere =
false;
299 SHPHandle shpHandle =
nullptr;
304 auto cleanup = qScopeGuard([&]() {
305 if (shpHandle) SHPClose(shpHandle);
312 Q_CHECK_PTR(shpHandle);
314 int cEntities, shapeType;
315 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
316 if (shapeType != SHPT_POLYGON && shapeType != SHPT_POLYGONZ) {
317 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHPFileHelper",
"File contains %1, expected Polygon.").arg(SHPTypeName(shapeType)));
321 const bool hasAltitude = (shapeType == SHPT_POLYGONZ);
323 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
324 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
326 qCWarning(SHPFileHelperLog) <<
"Failed to read polygon entity" << entityIdx;
329 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
332 SHPRewindObject(shpHandle, shpObject);
338 const int firstPartEnd = (shpObject->nParts > 1) ? shpObject->panPartStart[1] : shpObject->nVertices;
339 if (shpObject->nParts > 1) {
340 qCDebug(SHPFileHelperLog) <<
"Polygon entity" << entityIdx <<
"has" << shpObject->nParts
341 <<
"parts; using outer ring only (" << firstPartEnd <<
"vertices)";
344 QList<QGeoCoordinate> vertices;
345 const bool entityHasAltitude = hasAltitude && shpObject->padfZ;
347 for (
int i = 0; i < firstPartEnd; i++) {
348 QGeoCoordinate coord;
350 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[i], shpObject->padfY[i], utmZone, utmSouthernHemisphere, coord)) {
351 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for entity" << entityIdx <<
"vertex" << i;
355 coord.setLatitude(shpObject->padfY[i]);
356 coord.setLongitude(shpObject->padfX[i]);
358 if (entityHasAltitude) {
359 coord.setAltitude(shpObject->padfZ[i]);
361 vertices.append(coord);
364 if (vertices.count() < 3) {
365 qCWarning(SHPFileHelperLog) <<
"Skipping polygon entity" << entityIdx <<
"with less than 3 vertices";
370 if (filterMeters > 0) {
371 const QGeoCoordinate firstVertex = vertices.first();
375 constexpr double kClosureThreshold = 0.01;
376 const bool hadExplicitClosure = vertices.last().distanceTo(firstVertex) < kClosureThreshold;
380 while (i < (vertices.count() - 1)) {
381 if ((vertices.count() > 3) && (vertices[i].distanceTo(vertices[i + 1]) < filterMeters)) {
382 vertices.removeAt(i + 1);
391 if (hadExplicitClosure && vertices.count() > 3 &&
392 vertices.last().distanceTo(firstVertex) < kClosureThreshold) {
393 vertices.removeLast();
397 polygons.append(vertices);
400 if (polygons.isEmpty()) {
401 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"No valid polygons found."));
411 bool utmSouthernHemisphere =
false;
412 SHPHandle shpHandle =
nullptr;
417 auto cleanup = qScopeGuard([&]() {
418 if (shpHandle) SHPClose(shpHandle);
425 Q_CHECK_PTR(shpHandle);
427 int cEntities, shapeType;
428 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
429 if (shapeType != SHPT_ARC && shapeType != SHPT_ARCZ) {
430 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHPFileHelper",
"File contains %1, expected Arc.").arg(SHPTypeName(shapeType)));
434 const bool hasAltitude = (shapeType == SHPT_ARCZ);
436 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
437 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
439 qCWarning(SHPFileHelperLog) <<
"Failed to read polyline entity" << entityIdx;
442 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
447 const int firstPartEnd = (shpObject->nParts > 1) ? shpObject->panPartStart[1] : shpObject->nVertices;
448 if (shpObject->nParts > 1) {
449 qCDebug(SHPFileHelperLog) <<
"Polyline entity" << entityIdx <<
"has" << shpObject->nParts
450 <<
"parts; using first part only (" << firstPartEnd <<
"vertices)";
453 QList<QGeoCoordinate> vertices;
454 const bool entityHasAltitude = hasAltitude && shpObject->padfZ;
456 for (
int i = 0; i < firstPartEnd; i++) {
457 QGeoCoordinate coord;
459 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[i], shpObject->padfY[i], utmZone, utmSouthernHemisphere, coord)) {
460 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for entity" << entityIdx <<
"vertex" << i;
464 coord.setLatitude(shpObject->padfY[i]);
465 coord.setLongitude(shpObject->padfX[i]);
467 if (entityHasAltitude) {
468 coord.setAltitude(shpObject->padfZ[i]);
470 vertices.append(coord);
473 if (vertices.count() < 2) {
474 qCWarning(SHPFileHelperLog) <<
"Skipping polyline entity" << entityIdx <<
"with less than 2 vertices";
479 if (filterMeters > 0) {
481 while (i < (vertices.count() - 1)) {
482 if ((vertices.count() > 2) && (vertices[i].distanceTo(vertices[i+1]) < filterMeters)) {
483 vertices.removeAt(i+1);
490 polylines.append(vertices);
493 if (polylines.isEmpty()) {
494 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHP",
"No valid polylines found."));
504 bool utmSouthernHemisphere =
false;
505 SHPHandle shpHandle =
nullptr;
510 auto cleanup = qScopeGuard([&]() {
511 if (shpHandle) SHPClose(shpHandle);
518 Q_CHECK_PTR(shpHandle);
520 int cEntities, shapeType;
521 SHPGetInfo(shpHandle, &cEntities, &shapeType,
nullptr,
nullptr);
522 if (shapeType != SHPT_POINT && shapeType != SHPT_POINTZ) {
523 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"SHPFileHelper",
"File contains %1, expected Point.").arg(SHPTypeName(shapeType)));
527 const bool hasAltitude = (shapeType == SHPT_POINTZ);
529 for (
int entityIdx = 0; entityIdx < cEntities; entityIdx++) {
530 SHPObject *shpObject = SHPReadObject(shpHandle, entityIdx);
532 qCWarning(SHPFileHelperLog) <<
"Failed to read point entity" << entityIdx;
535 auto shpObjectCleanup = qScopeGuard([shpObject]() { SHPDestroyObject(shpObject); });
538 if (shpObject->nVertices != 1) {
539 qCWarning(SHPFileHelperLog) <<
"Skipping point entity" << entityIdx <<
"with unexpected vertex count:" << shpObject->nVertices;
543 QGeoCoordinate coord;
545 if (!
QGCGeo::convertUTMToGeo(shpObject->padfX[0], shpObject->padfY[0], utmZone, utmSouthernHemisphere, coord)) {
546 qCWarning(SHPFileHelperLog) <<
"UTM conversion failed for point entity" << entityIdx;
550 coord.setLatitude(shpObject->padfY[0]);
551 coord.setLongitude(shpObject->padfX[0]);
554 if (hasAltitude && shpObject->padfZ) {
555 coord.setAltitude(shpObject->padfZ[0]);
558 points.append(coord);
561 if (points.isEmpty()) {
562 errorString = QCoreApplication::translate(
"SHPFileHelper", _errorPrefix).arg(QCoreApplication::translate(
"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)