Hi,

This is a tentative to make subsurface a bit more flexible when parsing GPS coordinates. I like to geo-reference my dives, but copy and pasting the coordinates from web sites was quite a pain (needed a lot of manual correction).

Please have a look and comment. I'm quite tempted to go wild and do another rewrite without using regular expressions... would be more flexible and less code, IMHO.

Thanks
>From 6deecb432f6baabc16171c7bae61bae5725e70fa Mon Sep 17 00:00:00 2001
From: Patrick Valsecchi <[email protected]>
Date: Tue, 3 Feb 2015 08:01:40 +0100
Subject: [PATCH] More tolerant about spaces in GPS coordinates.

Refactored the parsing logic to make it more solid (no more guessing).
Added a test for checking that.
Fixed a few warnings.

Signed-off-by: Patrick Valsecchi <[email protected]>
---
 CMakeLists.txt          |  16 ++--
 qthelper.cpp            | 235 ++++++++++++++++++++++++++----------------------
 tests/testgpscoords.cpp |  77 ++++++++++++++++
 tests/testgpscoords.h   |  25 ++++++
 4 files changed, 240 insertions(+), 113 deletions(-)
 create mode 100644 tests/testgpscoords.cpp
 create mode 100644 tests/testgpscoords.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8c8cad1..4a260e7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -235,11 +235,13 @@ ADD_DEPENDENCIES(subsurface_interface subsurface_generated_ui)
 ADD_DEPENDENCIES(subsurface_generated_ui version)
 ADD_DEPENDENCIES(subsurface_corelib version)
 
-ENABLE_TESTING()
-ADD_EXECUTABLE(TestUnitConversion tests/testunitconversion.cpp)
-TARGET_LINK_LIBRARIES(TestUnitConversion subsurface_corelib ${QT_TEST_LIBRARIES}  ${SUBSURFACE_LINK_LIBRARIES} -lzip -ldivecomputer)
-ADD_TEST(NAME TestUnitConversion COMMAND TestUnitConversion)
+MACRO(test NAME FILE)
+    ADD_EXECUTABLE(${NAME} tests/${FILE})
+    TARGET_LINK_LIBRARIES(${NAME} subsurface_corelib ${QT_TEST_LIBRARIES}  ${SUBSURFACE_LINK_LIBRARIES} -lzip -ldivecomputer)
+    ADD_TEST(NAME ${NAME} COMMAND ${NAME})
+ENDMACRO()
 
-ADD_EXECUTABLE(TestProfile tests/testprofile.cpp)
-TARGET_LINK_LIBRARIES(TestProfile subsurface_corelib ${QT_TEST_LIBRARIES} ${SUBSURFACE_LINK_LIBRARIES} -lzip -ldivecomputer)
-ADD_TEST(NAME TestProfile COMMAND TestProfile)
+ENABLE_TESTING()
+test(TestUnitConversion testunitconversion.cpp)
+test(TestProfile testprofile.cpp)
+test(TestGpsCoords testgpscoords.cpp)
diff --git a/qthelper.cpp b/qthelper.cpp
index 0c1c68a..6166d55 100644
--- a/qthelper.cpp
+++ b/qthelper.cpp
@@ -1,14 +1,11 @@
 #include "qthelper.h"
-#include "qt-gui.h"
 #include "gettextfromc.h"
-#include "dive.h"
 #include "statistics.h"
 #include <exif.h>
 #include "file.h"
 #include <QFile>
 #include <QRegExp>
 #include <QDir>
-#include <QMap>
 #include <QDebug>
 #include <QSettings>
 #include <libxslt/documents.h>
@@ -47,10 +44,10 @@ QString printGPSCoords(int lat, int lon)
 	lonh = lon >= 0 ? translate("gettextFromC", "E") : translate("gettextFromC", "W");
 	lat = abs(lat);
 	lon = abs(lon);
-	latdeg = lat / 1000000;
-	londeg = lon / 1000000;
-	latmin = (lat % 1000000) * 60;
-	lonmin = (lon % 1000000) * 60;
+	latdeg = lat / 1000000U;
+	londeg = lon / 1000000U;
+	latmin = (lat % 1000000U) * 60U;
+	lonmin = (lon % 1000000U) * 60U;
 	latsec = (latmin % 1000000) * 60;
 	lonsec = (lonmin % 1000000) * 60;
 	result.sprintf("%u%s%02d\'%06.3f\"%s %u%s%02d\'%06.3f\"%s",
@@ -59,111 +56,137 @@ QString printGPSCoords(int lat, int lon)
 	return result;
 }
 
+static const QString trHemisphereN = translate("gettextFromC", "N");
+static const QString trHemisphereS = translate("gettextFromC", "S");
+static const QString trHemisphereE = translate("gettextFromC", "E");
+static const QString trHemisphereW = translate("gettextFromC", "W");
+
+class CoordKind {
+public:
+	static bool parse(const QString &txt, double &lat, double &lon)
+	{
+			foreach (CoordKind *cur, _kinds) {
+				if (cur->parse(txt)) {
+					cur->getCoords(lat, lon);
+					return true;
+				}
+			}
+		return false;
+	}
+
+protected:
+	CoordKind(const QString &re) : _re(re)
+	{
+		_kinds.append(this);
+	}
+
+	virtual bool parse(const QString &txt)
+	{
+		return _re.exactMatch(txt);
+	}
+
+	virtual void getCoords(double &lat, double &lon) const = 0;
+
+	void fixSigns(double &lat, double &lon, int latSym, int lonSym) const
+	{
+		const QString latVal = _re.cap(latSym);
+		if (latVal == "S" || latVal == trHemisphereS || latVal == "-")
+			lat *= -1.0;
+
+		const QString lonVal = _re.cap(lonSym);
+		if (lonVal == "W" || lonVal == trHemisphereW || lonVal == "-")
+			lon *= -1.0;
+	}
+
+	QRegExp _re;
+	static QList<CoordKind *> _kinds;
+};
+
+QList<CoordKind *> CoordKind::_kinds;
+
+static struct ISO6709dCoordKind : public CoordKind {
+	ISO6709dCoordKind() :
+		CoordKind(QString(
+			"(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)\\s*'\\s*(\\d+)([,\\.](\\d+))?\\s*\"\\s*([NS%1%2])\\s*"
+			"(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)\\s*'\\s*(\\d+)([,\\.](\\d+))?\\s*\"\\s*([EW%3%4])")
+			.arg(trHemisphereN)
+			.arg(trHemisphereS)
+			.arg(trHemisphereE)
+			.arg(trHemisphereW)) {}
+
+	virtual void getCoords(double &lat, double &lon) const
+	{
+		lat = _re.cap(1).toInt() + _re.cap(2).toInt() / 60.0 +
+			(_re.cap(3) + QString(".") + _re.cap(5)).toDouble() / 3600.0;
+		lon = _re.cap(7).toInt() + _re.cap(8).toInt() / 60.0 +
+			(_re.cap(9) + QString(".") + _re.cap(11)).toDouble() / 3600.0;
+		fixSigns(lat, lon, 6, 12);
+	}
+} iso6709dCoordKind;
+
+static struct SecondsCoordKind : public CoordKind {
+	SecondsCoordKind() :
+		CoordKind(QString(
+			"([NS%1%2])\\s*(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)\\s*'\\s*(\\d+)([,\\.](\\d+))?\\s*\"\\s*"
+			"([EW%3%4])\\s*(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)\\s*'\\s*(\\d+)([,\\.](\\d+))?\\s*\"")
+			.arg(trHemisphereN)
+			.arg(trHemisphereS)
+			.arg(trHemisphereE)
+			.arg(trHemisphereW)) {}
+
+	virtual void getCoords(double &lat, double &lon) const
+	{
+		lat = _re.cap(2).toInt() + _re.cap(3).toInt() / 60.0 +
+			(_re.cap(4) + QString(".") + _re.cap(6)).toDouble() / 3600.0;
+		lon = _re.cap(8).toInt() + _re.cap(9).toInt() / 60.0 +
+			(_re.cap(10) + QString(".") + _re.cap(12)).toDouble() / 3600.0;
+		fixSigns(lat, lon, 1, 7);
+	}
+} secondsCoordKind;
+
+static struct MinutesCoordKind : public CoordKind {
+	MinutesCoordKind() :
+		CoordKind(QString(
+			"([NS%1%2])\\s*(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)\\s*([,\\.](\\d+))?\\s*'\\s*"
+			"([EW%3%4])\\s*(\\d+)\\s*[" UTF8_DEGREE "\\s]\\s*(\\d+)([,\\.](\\d+))?\\s*'")
+			.arg(trHemisphereN)
+			.arg(trHemisphereS)
+			.arg(trHemisphereE)
+			.arg(trHemisphereW)) {}
+
+	virtual void getCoords(double &lat, double &lon) const
+	{
+		lat = _re.cap(2).toInt() + (_re.cap(3) + QString(".") + _re.cap(5)).toDouble() / 60.0;
+		lon = _re.cap(7).toInt() + (_re.cap(8) + QString(".") + _re.cap(10)).toDouble() / 60.0;
+		fixSigns(lat, lon, 1, 6);
+	}
+} minutesCoordKind;
+
+static struct DecimalCoordKind : public CoordKind {
+	DecimalCoordKind() :
+		CoordKind(QString("([-NS%1%2]?)\\s*(\\d+)[,\\.](\\d+)\\s*([-EW%3%4]?)\\s*(\\d+)[,\\.](\\d+)")
+			.arg(trHemisphereN)
+			.arg(trHemisphereS)
+			.arg(trHemisphereE)
+			.arg(trHemisphereW)) {}
+
+	virtual void getCoords(double &lat, double &lon) const
+	{
+		lat = (_re.cap(2) + QString(".") + _re.cap(3)).toDouble();
+		lon = (_re.cap(5) + QString(".") + _re.cap(6)).toDouble();
+		fixSigns(lat, lon, 1, 4);
+	}
+} decimalCoordKind;
+
 bool parseGpsText(const QString &gps_text, double *latitude, double *longitude)
 {
-	enum {
-		ISO6709D,
-		SECONDS,
-		MINUTES,
-		DECIMAL
-	} gpsStyle = ISO6709D;
-	int eastWest = 4;
-	int northSouth = 1;
-	QString trHemisphere[4];
-	trHemisphere[0] = translate("gettextFromC", "N");
-	trHemisphere[1] = translate("gettextFromC", "S");
-	trHemisphere[2] = translate("gettextFromC", "E");
-	trHemisphere[3] = translate("gettextFromC", "W");
-	QString regExp;
-	/* an empty string is interpreted as 0.0,0.0 and therefore "no gps location" */
-	if (gps_text.trimmed().isEmpty()) {
+	const QString trimmed = gps_text.trimmed();
+	if (trimmed.isEmpty()) {
 		*latitude = 0.0;
 		*longitude = 0.0;
 		return true;
 	}
-	// trying to parse all formats in one regexp might be possible, but it seems insane
-	// so handle the four formats we understand separately
-
-	// ISO 6709 Annex D representation
-	// http://en.wikipedia.org/wiki/ISO_6709#Representation_at_the_human_interface_.28Annex_D.29
-	// e.g. 52°49'02.388"N 1°36'17.388"E
-	if (gps_text.at(0).isDigit() && (gps_text.count(",") % 2) == 0) {
-		gpsStyle = ISO6709D;
-		regExp = QString("(\\d+)[" UTF8_DEGREE "\\s](\\d+)[\'\\s](\\d+)([,\\.](\\d+))?[\"\\s]([NS%1%2])"
-				 "\\s*(\\d+)[" UTF8_DEGREE "\\s](\\d+)[\'\\s](\\d+)([,\\.](\\d+))?[\"\\s]([EW%3%4])")
-				 .arg(trHemisphere[0])
-				 .arg(trHemisphere[1])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3]);
-	} else if (gps_text.count(QChar('"')) == 2) {
-		gpsStyle = SECONDS;
-		regExp = QString("\\s*([NS%1%2])\\s*(\\d+)[" UTF8_DEGREE "\\s]+(\\d+)[\'\\s]+(\\d+)([,\\.](\\d+))?[^EW%3%4]*"
-				 "([EW%5%6])\\s*(\\d+)[" UTF8_DEGREE "\\s]+(\\d+)[\'\\s]+(\\d+)([,\\.](\\d+))?")
-				 .arg(trHemisphere[0])
-				 .arg(trHemisphere[1])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3]);
-	} else if (gps_text.count(QChar('\'')) == 2) {
-		gpsStyle = MINUTES;
-		regExp = QString("\\s*([NS%1%2])\\s*(\\d+)[" UTF8_DEGREE "\\s]+(\\d+)([,\\.](\\d+))?[^EW%3%4]*"
-				 "([EW%5%6])\\s*(\\d+)[" UTF8_DEGREE "\\s]+(\\d+)([,\\.](\\d+))?")
-				 .arg(trHemisphere[0])
-				 .arg(trHemisphere[1])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3]);
-	} else {
-		gpsStyle = DECIMAL;
-		regExp = QString("\\s*([-NS%1%2]?)\\s*(\\d+)[,\\.](\\d+)[^-EW%3%4\\d]*([-EW%5%6]?)\\s*(\\d+)[,\\.](\\d+)")
-				 .arg(trHemisphere[0])
-				 .arg(trHemisphere[1])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3])
-				 .arg(trHemisphere[2])
-				 .arg(trHemisphere[3]);
-	}
-	QRegExp r(regExp);
-	if (r.indexIn(gps_text) != -1) {
-		// qDebug() << "Hemisphere" << r.cap(1) << "deg" << r.cap(2) << "min" << r.cap(3) << "decimal" << r.cap(4);
-		// qDebug() << "Hemisphere" << r.cap(5) << "deg" << r.cap(6) << "min" << r.cap(7) << "decimal" << r.cap(8);
-		switch (gpsStyle) {
-		case ISO6709D:
-			*latitude = r.cap(1).toInt() + r.cap(2).toInt() / 60.0 +
-				    (r.cap(3) + QString(".") + r.cap(5)).toDouble() / 3600.0;
-			*longitude = r.cap(7).toInt() + r.cap(8).toInt() / 60.0 +
-				     (r.cap(9) + QString(".") + r.cap(11)).toDouble() / 3600.0;
-			northSouth = 6;
-			eastWest = 12;
-			break;
-		case SECONDS:
-			*latitude = r.cap(2).toInt() + r.cap(3).toInt() / 60.0 +
-				    (r.cap(4) + QString(".") + r.cap(6)).toDouble() / 3600.0;
-			*longitude = r.cap(8).toInt() + r.cap(9).toInt() / 60.0 +
-				     (r.cap(10) + QString(".") + r.cap(12)).toDouble() / 3600.0;
-			eastWest = 7;
-			break;
-		case MINUTES:
-			*latitude = r.cap(2).toInt() + (r.cap(3) + QString(".") + r.cap(5)).toDouble() / 60.0;
-			*longitude = r.cap(7).toInt() + (r.cap(8) + QString(".") + r.cap(10)).toDouble() / 60.0;
-			eastWest = 6;
-			break;
-		case DECIMAL:
-		default:
-			*latitude = (r.cap(2) + QString(".") + r.cap(3)).toDouble();
-			*longitude = (r.cap(5) + QString(".") + r.cap(6)).toDouble();
-			break;
-		}
-		if (r.cap(northSouth) == "S" || r.cap(northSouth) == trHemisphere[1] || r.cap(northSouth) == "-")
-			*latitude *= -1.0;
-		if (r.cap(eastWest) == "W" || r.cap(eastWest) == trHemisphere[3] || r.cap(eastWest) == "-")
-			*longitude *= -1.0;
-		// qDebug("%s -> %8.5f / %8.5f", gps_text.toLocal8Bit().data(), *latitude, *longitude);
-		return true;
-	}
-	return false;
+	return CoordKind::parse(trimmed, *latitude, *longitude);
 }
 
 bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out)
diff --git a/tests/testgpscoords.cpp b/tests/testgpscoords.cpp
new file mode 100644
index 0000000..feb6c33
--- /dev/null
+++ b/tests/testgpscoords.cpp
@@ -0,0 +1,77 @@
+#include <dive.h>
+#include "testgpscoords.h"
+
+//unit under test
+extern bool parseGpsText(const QString &gps_text, double *latitude, double *longitude);
+
+
+void TestGpsCoords::testISO6709DParse()
+{
+	testParseOK("52°49'02.388\"N 1°36'17.388\"E",
+		coord2double(52, 49, 2.388), coord2double(1, 36, 17.388));
+}
+
+void TestGpsCoords::testNegativeISO6709DParse()
+{
+	testParseOK("52°49'02.388\"S 1°36'17.388\"W",
+		coord2double(-52, -49, -2.388), coord2double(-1, -36, -17.388));
+}
+
+void TestGpsCoords::testSpaceISO6709DParse()
+{
+	testParseOK("52 ° 49 ' 02.388 \" N  1 ° 36 ' 17.388 \" E",
+		coord2double(52, 49, 2.388), coord2double(1, 36, 17.388));
+}
+
+void TestGpsCoords::testSecondsParse()
+{
+	testParseOK("N52°49'02.388\" E1°36'17.388\"",
+		coord2double(52, 49, 2.388), coord2double(1, 36, 17.388));
+}
+
+void TestGpsCoords::testSpaceSecondsParse()
+{
+	testParseOK(" N 52 ° 49 ' 02.388 \"  E 1 ° 36 ' 17.388 \"",
+		coord2double(52, 49, 2.388), coord2double(1, 36, 17.388));
+}
+
+void TestGpsCoords::testMinutesParse()
+{
+	testParseOK("N52°49.03' E1°36.23'",
+		coord2double(52, 49.03), coord2double(1, 36.23));
+}
+
+void TestGpsCoords::testSpaceMinutesParse()
+{
+	testParseOK(" N 52 ° 49.03 '  E 1 ° 36.23 ' ",
+		coord2double(52, 49.03), coord2double(1, 36.23));
+}
+
+void TestGpsCoords::testDecimalParse()
+{
+	testParseOK("N52.83 E1.61",
+		coord2double(52.83), coord2double(1.61));
+}
+
+void TestGpsCoords::testSpaceDecimalParse()
+{
+	testParseOK(" N 52.83  E 1.61 ",
+		coord2double(52.83), coord2double(1.61));
+}
+
+
+void TestGpsCoords::testParseOK(const QString &txt, double expectedLat,
+	double expectedLon)
+{
+	double actualLat, actualLon;
+	QVERIFY(parseGpsText(txt, &actualLat, &actualLon));
+	QCOMPARE(actualLat, expectedLat);
+	QCOMPARE(actualLon, expectedLon);
+}
+
+double TestGpsCoords::coord2double(double deg, double min, double sec)
+{
+	return deg + min / 60.0 + sec / 3600.0;
+}
+
+QTEST_MAIN(TestGpsCoords)
diff --git a/tests/testgpscoords.h b/tests/testgpscoords.h
new file mode 100644
index 0000000..0fa7e33
--- /dev/null
+++ b/tests/testgpscoords.h
@@ -0,0 +1,25 @@
+#ifndef TESTGPSCOORDS_H
+#define TESTGPSCOORDS_H
+
+#include <QtTest>
+
+class TestGpsCoords : public QObject {
+Q_OBJECT
+private slots:
+	void testISO6709DParse();
+	void testNegativeISO6709DParse();
+	void testSpaceISO6709DParse();
+	void testSecondsParse();
+	void testSpaceSecondsParse();
+	void testMinutesParse();
+	void testSpaceMinutesParse();
+	void testDecimalParse();
+	void testSpaceDecimalParse();
+
+private:
+	static void testParseOK(const QString &txt, double expectedLat,
+		double expectedLon);
+	static double coord2double(double deg, double min = 0.0, double sec = 0.0);
+};
+
+#endif
-- 
2.1.0

_______________________________________________
subsurface mailing list
[email protected]
http://lists.subsurface-divelog.org/cgi-bin/mailman/listinfo/subsurface

Reply via email to