Skip to content

Commit

Permalink
Improvements to coordinate argument parsing.
Browse files Browse the repository at this point in the history
Handle a range of formats that include the output of exiftool
and jhead, to make it easier to use pytopo to show the location
of a GPS-tagged photo.

Update the argparsing unit test to test those cases,
plus some examples in east/south quadrants.
  • Loading branch information
akkana committed Jul 31, 2023
1 parent 89d43f4 commit afea549
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 19 deletions.
52 changes: 39 additions & 13 deletions pytopo/MapUtils.py
Expand Up @@ -97,30 +97,56 @@ def to_decimal_degrees(coord, degformat="DD"):
except:
pass

# Format used in EXIF in Pixel phone cameras
# e.g. 35 deg 54' 35.67" N, 106 deg 16' 10.78" W
# which is also the format handled by exiftool.
# Also handles a form with an actual degree symbol,
# like 106° 16' 10.78" W, or a ^ in place of the degree symbol.
DMS_COORD_PAT = "\s*([+-]?[0-9]{1,3})\s*(?:deg|°|\^)" \
"\s*([0-9]{1,3})'\s*" \
"([0-9]{1,3})(.[0-9]+\")?\s*([NSEW])?"
m = re.match(DMS_COORD_PAT, coord)
# Match the following formats and variants:
# jhead, for EXIF from photos: N 35d 51m 21.08s
# exiftool for EXIF from photos: 35 deg 51' 21.08" N
# More general format: 35° 54' 35.67" N, 106 deg 16' 10.78" W
# For degrees, accept deg d ° ^
# For minutes, accept ' min m
# For seconds, accept " sec s
# NSEW can either begin or end the string, or may be missing entirely,
# in which case positive means north/east.
# Degrees may start with a + or -
coord_pat = r"\s*([NSEW])?\s*([+-]?[0-9]{1,3})\s*(?:deg|d|°|\^)" \
r"\s*([0-9]{1,3})(?:'|m|min)\s*" \
r"([0-9]{1,3})(.[0-9]+)?\s*(?:\"|s|sec)\s*([NSEW])?"
m = re.match(coord_pat, coord)
if m:
deg, mins, secs, subsecs, direc = m.groups()
direc1, deg, mins, secs, subsecs, direc2 = m.groups()
if direc1 and direc2 and direc1 != direc2:
print("%s: Conflicting directions, %s vs %s" % (coord,
direc1, direc2),
file=sys.stderr)
raise ValueError("Can't parse '%s' as a coordinate" % coord)

# Sign fiddling: end up with positive value but save the sign
if direc2 and not direc1:
direc1 = direc2
if direc1 == 'S' or direc1 == 'W':
sign = -1
else:
sign = 1
val = int(deg) * sign
if val < 0:
sign = -1
val = -val
else:
sign = 1

secs = float(secs)
if subsecs:
if subsecs.endswith('"'):
subsecs = subsecs[:-1]
secs += float(subsecs)
mins = int(mins) + secs/60.
val = int(deg)
if val >= 0:
val += mins/60.
else:
val -= mins/60.
if direc == 'S' or direc == 'W':
val = -val

# Restore the sign
val *= sign
# print("Parsed", coord, "to", val)
return val

raise ValueError("Can't parse '%s' as a coordinate" % coord)
Expand Down
36 changes: 30 additions & 6 deletions test/test_argparsing.py
Expand Up @@ -142,13 +142,36 @@ def test_gpx_plus_overlay(self):
-106.2283611, 35.895508))

def test_explicit_coords(self):
args = [ 'pytopo', '37.537', '-122.37' ]

mapwin = MapWindow(self.viewer)
self.viewer.parse_args(mapwin, args)
argslists = [
# Several syntax variants for the same location:
# decimal degrees syntax
( [ 'pytopo', '37.537', '-122.37' ],
[ 37.537, -122.37] ),
# exiftool (and most normal humans) syntax
( [ 'pytopo',
'37 deg 32\' 13.12" N', '122° 22\' 12.0" W' ],
[ 37.537, -122.37] ),
# jhead syntax
( [ 'pytopo', 'N 37d 32m 13.12s', 'W 122d 22m 12.0s' ],
[ 37.537, -122.37] ),

# east and south
( [ 'pytopo',
'37 deg 32\' 13.12" S', '122° 22\' 12.0" E' ],
[ -37.537, 122.37] ),
# negative signs
( [ 'pytopo',
'-37 deg 32\' 13.12"', '-122° 22\' 12.0" E' ],
[ -37.537, -122.37] ),
]

for argtuple in argslists:
args, realcoords = argtuple
mapwin = MapWindow(self.viewer)
self.viewer.parse_args(mapwin, args)

assertCloseEnough(mapwin.center_lon, -122.37)
assertCloseEnough(mapwin.center_lat, 37.537)
assertCloseEnough(mapwin.center_lat, realcoords[0])
assertCloseEnough(mapwin.center_lon, realcoords[1])

def test_explicit_coords_plus_gpx(self):
args = [ 'pytopo', '35.85', '-106.4',
Expand Down Expand Up @@ -227,3 +250,4 @@ def test_known_site_plus_overlay(self):

assertCloseEnough(mapwin.center_lon, sitelon)
assertCloseEnough(mapwin.center_lat, sitelat)

0 comments on commit afea549

Please sign in to comment.