Skip to content

Commit

Permalink
Make argument parsing more robust.
Browse files Browse the repository at this point in the history
Allow parsing both coordinates from a single string.
Add a new field in the initial dialog for coordinates,
and allow pasting coordinate strings (like from image EXIF)
into the new field or anywhere over the dialog.
  • Loading branch information
akkana committed Oct 10, 2023
1 parent afea549 commit 0b1e0d6
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 99 deletions.
150 changes: 94 additions & 56 deletions pytopo/MapUtils.py
Expand Up @@ -72,28 +72,62 @@ def deg_min_sec2dec_deg(coord):
return (deg + mins/60. + secs/3600.) * sign


def to_decimal_degrees(coord, degformat="DD"):
def parse_full_coords(coordstr, degformat="DD"):
"""Parse a full coordinate string, e.g.
35 deg 53' 30.81" N 106 deg 24' 4.17" W"
Return a tuple of floats.
# XXX Needs smarts about which is lat and which is lon.
Can also parse a single coordinate, in which case returns a 1-tuple.
"""
firstcoord, charsmatched = to_decimal_degrees(coordstr, degformat,
report_chars_matched=True)
if not charsmatched:
# This can happen in a case like "37.123 -102.456")
try:
coordstrings = coordstr.split()
if len(coordstrings) > 0:
return (firstcoord, to_decimal_degrees(coordstrings[1],
degformat))
except:
return (firstcoord)

coordstr = coordstr[charsmatched:]
if not coordstr:
return (firstcoord)
return ( firstcoord, to_decimal_degrees(coordstr, degformat) )


def to_decimal_degrees(coord, degformat="DD", report_chars_matched=False):
"""Try to parse various different forms of deg-min-sec strings
as well as floats, returning float decimal degrees.
as well as floats for a single coordinate (lat or lon, not both).
Returns float decimal degrees.
degformat can be "DM", "DMS" or "DD" and controls whether
float inputs are just passed through, or converted
from degrees + decimal minutes or degrees minutes seconds.
If report_chars_matched is true, returns a tuple:
the coordinate parsed, plus the number of characters matched,
to make it easier to parse the second coordinate.
"""
# Is it already a number?
try:
coord = float(coord)

if type(coord) is float or type(coord) is int:
if degformat == "DD":
return coord
pass
elif degformat == "DMS":
return deg_min_sec2dec_deg(coord)
coord = deg_min_sec2dec_deg(coord)
elif degformat == "DM":
return deg_min2dec_deg(coord)
coord = deg_min2dec_deg(coord)
else:
print("Error: unknown coordinate format %s" % degformat)
return None
coord = None

if report_chars_matched:
return (coord, 0)
return coord
except:
pass

Expand All @@ -111,45 +145,48 @@ def to_decimal_degrees(coord, degformat="DD"):
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:
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.
if val >= 0:
val += mins/60.
else:
val -= mins/60.

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

raise ValueError("Can't parse '%s' as a coordinate" % coord)
if not m:
raise ValueError("Can't parse '%s' as a coordinate" % coord)

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.
if val >= 0:
val += mins/60.
else:
val -= mins/60.

# Restore the sign
val *= sign

if report_chars_matched:
return (val, len(m.group(0)))
return val


@staticmethod
def bearing(lat1, lon1, lat2, lon2):
Expand Down Expand Up @@ -257,23 +294,24 @@ def Usage():
sys.exit(1)

degfmt = "DD"
for coord in sys.argv[1:]:
if coord == "-h" or coord == "--help":
for coordstr in sys.argv[1:]:
if coordstr == "-h" or coordstr == "--help":
Usage()
if coord == "--dm":
if coordstr == "--dm":
degfmt = "DM"
continue
try:
deg = to_decimal_degrees(coord, degfmt)
except:
print("Can't parse", coord)
coords = parse_full_coords(coordstr, degfmt)
except RuntimeError as e:
print("Can't parse", coordstr, ":", e)
Usage()

print('\n"%s":' % coord)
print("Decimal degrees: ", deg)
d, m, s = dec_deg2dms(deg)
print("Degrees Minutes Seconds: %d° %d' %.3f\"" % (d, m, s))
print("Degrees.Minutes ", dec_deg2deg_min(deg))
print('\n"%s":' % coordstr)
print("Decimal degrees: ", coords)
for c in coords:
d, m, s = dec_deg2dms(c)
print("Degrees Minutes Seconds: %d° %d' %.3f\"" % (d, m, s))
print("Degrees.Minutes ", dec_deg2deg_min(c))


if __name__ == '__main__':
Expand Down
67 changes: 42 additions & 25 deletions pytopo/MapViewer.py
Expand Up @@ -212,6 +212,18 @@ def track_select(self, mapwin):
dialog.destroy()
return False

def use_coordinates(self, lat, lon, mapwin):
"""Center the map on the given coordinates"""
if not mapwin.collection:
collection = self.find_collection(self.default_collection)
# mapwin.change_collection(collection)
mapwin.collection = collection
mapwin.center_lat = lat
mapwin.center_lon = lon
mapwin.pin_lat = lat
mapwin.pin_lon = lon
# mapwin.draw_map()

def use_site(self, site, mapwin):
"""Given a starting site, center the map on it and show the map.
Returns true for success.
Expand Down Expand Up @@ -405,33 +417,38 @@ def parse_args(self, mapwin, args):

# Doesn't match a known site. Maybe the args are coordinates?
try:
if len(args) >= 2:
if len(args) == 1:
lat, lon = MapUtils.parse_full_coords(args[0], "DD")
elif len(args) >= 2:
lat = MapUtils.to_decimal_degrees(args[0], "DD")
lon = MapUtils.to_decimal_degrees(args[1], "DD")
if abs(lat) > 90:
print("Guessing", lat,
"is a longitude. Please specify latitude first")
lat, lon = lon, lat
if lat is not None and lon is not None:
mapwin.center_lat = lat
mapwin.center_lon = lon

# Set a pin on the specified point.
mapwin.pin_lat = lat
mapwin.pin_lon = lon

args = args[2:]

# The next argument after latitude, longitude
# might be a collection, but it also might not.
# Try it and see.
if args:
coll = self.find_collection(args[0])
if coll:
mapwin.collection = coll
args = args[1:]

continue
else:
raise(RuntimeError("Can't make sense of arguments: %s"
% str(args)))
if abs(lat) > 90:
print("Guessing", lat,
"is a longitude. Please specify latitude first")
lat, lon = lon, lat
if lat is not None and lon is not None:
mapwin.center_lat = lat
mapwin.center_lon = lon

# Set a pin on the specified point.
mapwin.pin_lat = lat
mapwin.pin_lon = lon

args = args[2:]

# The next argument after latitude, longitude
# might be a collection, but it also might not.
# Try it and see.
if args:
coll = self.find_collection(args[0])
if coll:
mapwin.collection = coll
args = args[1:]

continue

print("Can't make sense of argument:", args[0])
args = args[1:]
Expand Down

0 comments on commit 0b1e0d6

Please sign in to comment.