#!/usr/bin/env python # topomap: show topographic maps using data files from Topo! CD # to generate a map at the specified coordinates. # Copyright 2005,2007 by Akkana Peck. # You may use, distribute or modify this program under the terms of the GPL. # We will do all calculations in decimal degrees, # but take inputs in deg.decimal_minutes. VersionString = "PyTopo Version 0.6, Copyright (c)2006,2007 by Akkana Peck." import sys, os, math, gtk, gc ######################################################### # # Global Variables that you can override in ~/.pytopo: # # Map collections you have, with format information. # You must define at least one collection in ~/.pytopo. # It should be a list of instances of MapCollection subtypes. # Example: #Collections = [ # Topo1MapCollection( "deathvalley", "/home/yourname/Maps/dvl_data", 7.5 ), # GenericMapCollection( "pa-geo", "/home/yourname/Maps/pa-geo", # "pa-map-", ".png", # 123, 37.5, 10742, 13120, 2, True, True ) #] # The currently supported types are: # Topo1MapCollection: data from local-area Topo! packages # (the kind that have 7.5 minute and 15 minute varieties included). # ( collection_name, directory_path, initial_scale ) # Topo2MapCollection: data from local-area Topo! packages # that use 410x256 maplets. # GenericMapCollection: a more general reader, for maps you split up # yourself or the Topo! national park maps. # ( collection_name, directory_path, filename_prefix, filename_suffix, # left_longitude, top_latitude, x_scale, y_scale, # num_digits, use_dash, latitude_first ) # Filenames might look like: pa-map-03-17.png # where prefix and suffix are pa-map- and .png, # left_longitude and top_latitude specify the top left corner of # the (0, 0) image in degrees.decimal_minutes, # x_scale and y_scale are in pixels per degree, # num_digits is the number of digits used to specify grid points, # usedash specifies whether to put a dash between grid numbers, # and latitude_first indicates that latitude changes more rapidly # than longitude (i.e. in pa-map-03-17.png, it's the third map over # and the 17th map down). # GenericMapCollection is subject to change (to add new parameters) as # different types of map are added and the rules need to be generalized. Collections = [] # Named sites you might want to use as starting points. # Format: [ sitename, longitude, latitude, collection_name ] # Coordinates are in degrees.decimal_minutes. # Example: # KnownSites = [ # # San Francisco Bay Area # [ "saratogagap", 122.0725, 37.155, "sfr" ], # [ "lexington", 121.594, 37.12, "sfr" ], # # Death Valley # [ "zabriskie", 116.475, 36.245, "deathvalley" ], # # From the Big Sur map: # [ "pinnacles", 121.0865, 36.3247, "bigsur" ], # ] KnownSites = [] # Print distances in metric? UseMetric = 0 # Where to save generated maps. The default is fine for most people. MapSaveDir = os.environ["HOME"] + "/Topo/" # Size of the window: how many maplets will be shown at once? # Don't get too attached to this one; it will eventually change # to be window size in pixels, since maplet size can vary. num_long = 3 num_lat = 2 # # End of variables likely to need customization. # ######################################################### ######################################################### # # Types of map collections we understand. # If you split your own map into maplets, you may # want to define your own subclass to handle it: # see GenericMapCollection for an example. # class MapCollection : def __init__(self, _name, _location) : self.name = _name self.location = _location # get_maplet returns: # pixbuf,x_off,y_off # - the pixbuf for the maplet image (or null) # - the offset into the image for the given coordinates # Subclasses must override this. # # XXX What are x_off and y_off? The amount the map needs to be # XXX offset in order to draw with the given coordinates centered. # XXX Not yet implemented; right now all you can rely on is that # XXX the maplet containing those coords is somewhere near center. def get_maplet(self, longitude, latitude) : return None,0,0 class GenericMapCollection(MapCollection) : def __init__(self, _name, _location, _prefix, _ext, _left_long, _top_lat, _img_width, _img_height, _xscale, _yscale, _numdigits, _usedash, _latfirst) : MapCollection.__init__(self, _name, _location) self.prefix = _prefix self.numdigits = _numdigits self.usedash = _usedash self.ext = _ext self.latfirst = _latfirst # Default maplet width and height: these will be reset from the images. self.img_width = _img_width self.img_height = _img_height self.left_longitude = _left_long # Left of 00-00 image self.top_latitude = _top_lat # Top of 00-00 image self.xscale = _xscale # Pixels per degree self.yscale = _yscale # Pixels per degree def get_maplet(self, longitude, latitude) : filename = self.CoordsToFilename(longitude, latitude) if filename == "" or not os.access(filename, os.R_OK) : #print "Can't open", filename, "for", longitude, latitude return None, 0, 0 #print "Opened", filename, "for", longitude, latitude pixbuf = gtk.gdk.pixbuf_new_from_file(filename) # Offsets aren't implemented yet: x_off = 0 y_off = 0 return pixbuf, x_off, y_off # Given coordinates in decimal degrees, map to the closest filename def CoordsToFilename(self, longitude, latitude) : if self.left_longitude < longitude or self.top_latitude < latitude : return "" x_grid = intTrunc((self.left_longitude - longitude) * self.xscale / self.img_width) y_grid = intTrunc((self.top_latitude - latitude) * self.yscale / self.img_height) if not self.latfirst : temp = x_grid x_grid = y_grid y_grid = temp retstr = self.location + "/" + self.prefix + \ ohstring(x_grid, self.numdigits); if self.usedash: retstr = retstr + "-" retstr = retstr + ohstring(y_grid, self.numdigits) + self.ext return retstr class Topo1MapCollection(MapCollection) : def __init__(self, _name, _location, _series) : MapCollection.__init__(self, _name, _location) self.img_width = 262 self.img_height = 328 self.set_series(_series) def set_series(self, _series) : self.series = _series self.xscale = self.img_width * 600 / self.series self.yscale = self.img_height * 600 / self.series # 600 is minutes/degree * maplets/minute def get_maplet(self, longitude, latitude) : filename = self.CoordsToFilename(longitude, latitude) if not os.access(filename, os.R_OK) : return None, 0, 0 pixbuf = gtk.gdk.pixbuf_new_from_file(filename) x_off = 0 y_off = 0 return pixbuf, x_off, y_off # Given a pair of coordinates in deg.mmss, map to the closest filename # e.g. q37122c2/012t0501.gif. def CoordsToFilename(self, longitude, latitude) : latDeg = intTrunc(latitude) longDeg = intTrunc(longitude) latMin = (latitude - latDeg) * 60. longMin = (longitude - longDeg) * 60. # The 7.5 here is because of the 7.5 in the directory names above # (we're getting the offset of this image from the origin of # the 7.5-series map covered by the directory), not the map series # we're actually plotting now. longMinOrd = intTrunc(longMin / 7.5) latMinOrd = intTrunc(latMin / 7.5) #print "longMin =", longMin, "latMin =", latMin #print "Ords:", longMinOrd, latMinOrd dirname = "q" + str(latDeg) + str(longDeg) + \ chr(ord('a') + latMinOrd) + str(longMinOrd+1) # Find the difference between our desired coordinates # and the origin of the map this directory represents. # The 7.5 here is because of the 7.5 in the directory names above. latMinDiff = latMin - (latMinOrd * 7.5) longMinDiff = longMin - (longMinOrd * 7.5) latOffset = intTrunc(latMinDiff * 10 / self.series) longOffset = intTrunc(longMinDiff * 10 / self.series) #print "Diffs:", longMinDiff, latMinDiff #print "Offsets:", longOffset, latOffset # Now calculate the current filename. # Note that series is either 7.5 or 15 if (self.series > 13) : fileprefix = "024t" numcharts = 5 else : fileprefix = "012t" numcharts = 10 filename = fileprefix + ohstring(numcharts-longOffset, 2) + \ ohstring(numcharts-latOffset, 2) + ".gif" return self.location + "/" + dirname + "/" + filename # End of Topo1MapCollection class # On North Palisade 7.5 (q37118a5) we get 410x256 pixel images. class Topo2MapCollection(MapCollection) : def __init__(self, _name, _location) : MapCollection.__init__(self, _name, _location) self.img_width = 410 self.img_height = 256 self.longcharts = 5 self.latcharts = 10 self.set_series(7.5) def set_series(self, _series) : self.series = _series self.xscale = self.img_width * 60 * self.longcharts / self.series self.yscale = self.img_height * 60 * self.latcharts / self.series def get_maplet(self, longitude, latitude) : filename = self.CoordsToFilename(longitude, latitude) print "TT2 get_maplet ", filename if not os.access(filename, os.R_OK) : return None, 0, 0 pixbuf = gtk.gdk.pixbuf_new_from_file(filename) x_off = 0 y_off = 0 return pixbuf, x_off, y_off # Given a pair of coordinates in deg.mmss, map to the closest filename # e.g. q37122c2/topo0501.gif. def CoordsToFilename(self, longitude, latitude) : # TT - series is always 7.5 latDeg = intTrunc(latitude) longDeg = intTrunc(longitude) latMin = (latitude - latDeg) * 60. longMin = (longitude - longDeg) * 60. # The 7.5 here is because of the 7.5 in the directory names above # (we're getting the offset of this image from the origin of # the 7.5-series map covered by the directory), not the map series # we're actually plotting now. longMinOrd = intTrunc(longMin / 7.5) latMinOrd = intTrunc(latMin / 7.5) #print "longMin =", longMin, "latMin =", latMin #print "Ords:", longMinOrd, latMinOrd dirname = "q" + str(latDeg) + str(longDeg) + \ chr(ord('a') + latMinOrd) + str(longMinOrd+1) # Find the difference between our desired coordinates # and the origin of the map this directory represents. # The 7.5 here is because of the 7.5 in the directory names above. latMinDiff = latMin - (latMinOrd * 7.5) longMinDiff = longMin - (longMinOrd * 7.5) latOffset = intTrunc(latMinDiff * self.latcharts / 7.5) longOffset = intTrunc(longMinDiff * self.longcharts / 7.5) #print "Diffs:", longMinDiff, latMinDiff #print "Offsets:", longOffset, latOffset # Now calculate the current filename. filename = "topo" + ohstring(self.longcharts-longOffset, 2) + \ ohstring(self.latcharts-latOffset, 2) + ".jpg" return self.location + "/" + dirname + "/" + filename # End of Topo2MapCollection class # # Global variables used by the program. # You shouldn't need to set any of these. # # The current map collection being used: Collection = None start_long = 0 start_lat = 0 # X/gtk graphics variables we need: drawing_area = 0 xgc = 0 def Usage() : global VersionString print VersionString print "Usage: pytopo start_lat start_long" print " pytopo site_name" print " pytopo -p" print "Use degrees.decimal_minutes format for coordinates." print "Set up site names in ~/.pytopo" print "Print list of known sites with pytopo -p" print "" print "Move around using arrow keys. q quits." print "Click in the map to print the coordinates of the clicked location." print "'s' will attempt to save the current map as a GIF file in ~/Topo/." sys.exit(1) def GracefulExit() : #PrintCenter() sys.exit(0) def intTrunc(num) : return int(num + .00001) # Convert degrees.minutes to decimal degrees def DegMinToDecDeg(coord) : deg = intTrunc(coord) dec = (coord - deg) / .6 return deg + dec # Convert decimal degrees to degrees.minutes def DecDegToDegMin(coord) : deg = intTrunc(coord) min = (coord - deg) * .6 return deg + min # Convert decimal degrees to a nice degrees/minutes string def DecDegToDegMinStr(coord) : deg = intTrunc(coord) min = (coord - deg) * 60. min = TruncateToFrac(min, 100) return str(deg) + "^" + str(min) + "'" # Each X pixel represents an angle of dAngle / img_width #def XtoLong(x) : # global start_long, img_width, dAngle # return start_long + (num_long * img_width - x) * dAngle / img_width # Each Y pixel represents an angle of dAngle / img_height #def YtoLat(y) : # global start_lat, img_height, dAngle # return start_lat + (num_lat * img_height - y) * dAngle / img_height def TruncateToFrac(num, frac) : return (float(intTrunc(num * frac)) / frac) # Given a center, figure out the appropriate start coordinates # for the lower right. def CenterToStart(longC, latC) : global num_long, num_lat # Find the start coords of the map containing the given point. # Must be a multiple of .75min, which is 1/80 of a degree. chartLong = TruncateToFrac(longC, 80) chartLat = TruncateToFrac(latC, 80) #print "CenterToStart: center chart is", \ # DecDegToDegMinStr(chartLong), DecDegToDegMinStr(chartLat) # Now move over by the appropriate number of charts. chartLong -= int(num_long / 2) * Collection.img_width / Collection.xscale chartLat -= int(num_lat / 2) * Collection.img_height / Collection.yscale return chartLong, chartLat # What's the current center of the maps we're showing? def StartToCenter(longS, latS) : global num_long, num_lat return longS + Collection.img_width / Collection.xscale * num_long / 2., \ latS + Collection.img_height / Collection.yscale * num_lat / 2. def PrintCenter() : global start_long, start_lat clong, clat = StartToCenter(start_long, start_lat) print "Currently:", DecDegToDegMinStr(clong), " ", \ DecDegToDegMinStr(clat) def ohstring(num, numdigits) : s = str(num) mult = pow(10, numdigits-1) while (numdigits > 1) : if (num < mult) : s = "0" + s mult = mult / 10 numdigits = numdigits-1 return s def draw_map(start_long, start_lat, collection) : global xgc # How big the map grid is: grid_w = collection.img_width grid_h = collection.img_height w = grid_w h = grid_h # Get the current window size: win_width, win_height = drawing_area.window.get_size() curlat = start_lat cur_y = win_height while cur_y >= 0: curlong = start_long cur_x = win_width while cur_x >= 0 : pixbuf, x_off, y_off = collection.get_maplet(curlong, curlat) if pixbuf != None : #print "Got maplet for", curlong, curlat w = pixbuf.get_width() h = pixbuf.get_height() # If the image won't completely fill the grid space, # fill the whole rectangle first with black. # Note: this won't guard against images with # transparent areas. Don't do that. if (w < grid_w or h < grid_h) : drawing_area.window.draw_rectangle(xgc, 1, cur_x-grid_w, cur_y-grid_h, grid_w, grid_h) drawing_area.window.draw_pixbuf(xgc, pixbuf, 0, 0, cur_x - grid_w, cur_y - grid_h, w, h) # Useful when testing: #drawing_area.window.draw_rectangle(xgc, 0, # cur_x - grid_w, # cur_y - grid_h, w, h) #drawing_area.window.draw_line(xgc, cur_x-grid_w, cur_y-grid_h, # cur_x, cur_y) # Make sure the pixbuf goes out of scope properly: pixbuf = 0 #gc.collect() else : drawing_area.window.draw_rectangle(xgc, 1, cur_x-grid_w, cur_y-grid_h, collection.img_width, collection.img_height) # You may ask, why not just do this subtraction before # draw_pixbuf so we don't have to subtract w and h twice? # Alas, we may not have the real w and h until we've done # pixbuf.get_width(), so we'd be subtracting the wrong thing. cur_x -= grid_w curlong += float(grid_w) / collection.xscale cur_y -= grid_h curlat += float(grid_h) / collection.yscale # Free all pixbuf data. Just letting pixbuf go out of scope # isn't enough; it's necessary to force garbage collection # otherwise Python will let the process grow until it # fills all of memory. # http://www.daa.com.au/pipermail/pygtk/2003-December/006499.html gc.collect() # Save the current map as something which could be gimped or printed. def save_as() : global Collection global start_long, start_lat global MapSaveDir file_list = "" # Calculate dAngle in decimal degrees dAngle = Collection.img_width / Collection.xscale #print "-------------- dAngle =", dAngle # Don't use the global num_long, num_lat; # calculate based on window size, and round up # so the saved map shows at least as much as the window does. win_width, win_height = drawing_area.window.get_size() num_long = int (.8 + float(win_width) / Collection.img_width) num_lat = int (.8 + float(win_height) / Collection.img_height) ny = 0 curlat = start_lat while ny < num_lat : curlong = start_long nx = 0 while nx < num_long : file_list = Collection.CoordsToFilename(curlong, curlat) + \ " " + file_list curlong += dAngle nx += 1 curlat += dAngle ny += 1 outfile = MapSaveDir + "topo" + "_" + \ str(start_long) + "_" + str(start_lat) + ".gif" cmdstr = "montage -geometry 262x328 -tile " + \ str(nx) + "x" + str(ny) + " " + \ file_list + " " + outfile #print "Running:", cmdstr os.system(cmdstr) if (os.access(outfile, os.R_OK)) : print "Saved:", outfile def expose_event(widget, event) : global xgc, drawing_area #print "Expose:", event.type #print "area:", event.area.x, event.area.y, event.area.width, event.area.height if xgc == 0 : xgc = drawing_area.window.new_gc() #xgc.set_foreground(white) #x, y, w, h = event.area draw_map(start_long, start_lat, Collection) #widget.queue_draw() return True def key_quit(*args) : GracefulExit() def key_scroll(accelgroup, win, key, accel) : #print args #print gtk.keysyms.h if key == gtk.keysyms.h : print "It was an h" else : print "key_scroll, not an h" return True def key_press_event(widget, event) : global start_lat, start_long if event.string == "q" : GracefulExit() if event.string == "+" or event.string == "=" : if isinstance(Collection, Topo1MapCollection) : Collection.set_series(7.5) elif event.string == "-" : if isinstance(Collection, Topo1MapCollection) : Collection.set_series(15) elif event.string == "?" or event.string == " ": PrintCenter() elif event.keyval == gtk.keysyms.Left : start_long += float(Collection.img_width) / Collection.xscale elif event.keyval == gtk.keysyms.Right : start_long -= float(Collection.img_width) / Collection.xscale elif event.keyval == gtk.keysyms.Up : start_lat += float(Collection.img_height) / Collection.yscale elif event.keyval == gtk.keysyms.Down : start_lat -= float(Collection.img_height) / Collection.yscale elif event.keyval == gtk.keysyms.l and \ event.state == gtk.gdk.CONTROL_MASK : nop() # Just fall through to draw_map() elif event.string == "s" : save_as() return True else : #print "Unknown key,", event.keyval return False draw_map(start_long, start_lat, Collection) return True # collection.x_scale is in pixels per degree. def x2long(x, start_long, win_width, scale) : return start_long + float(win_width - x) / scale def y2lat(y, start_long, win_height, scale) : return start_lat + float(win_height - y) / scale click_last_long = 0 click_last_lat = 0 def button_event(widget, event) : global start_long, start_lat global click_last_long, click_last_lat # if (event.state == 0) : # if (event.button != 1) : # return # #print "Button 1 release" # return # Button 3 click near an edge scrolls in that direction. if event.button == 3 : if (event.x > img_width * (num_long-.4)) : start_long -= float(Collection.img_width) / Collection.xscale elif (event.x < img_width * .4) : start_long += float(Collection.img_width) / Collection.xscale if (event.y > img_height * (num_lat-.3)) : start_lat -= float(Collection.img_height) / Collection.yscale elif (event.y < img_height * .3) : start_lat += float(Collection.img_height) / Collection.yscale draw_map(start_long, start_lat, Collection) elif event.button == 1 : win_width, win_height = drawing_area.window.get_size() cur_long = x2long(event.x, start_long, win_width, Collection.xscale) cur_lat = y2lat(event.y, start_lat, win_height, Collection.yscale) print "Click:", \ DecDegToDegMinStr(cur_long), \ DecDegToDegMinStr(cur_lat) # Find angle and distance since last click. # You would think that we could just use the long and lat # differences, but it doesn't work that way, because maps # aren't square: away from the equator, longitude isn't # a great circle so a degree in longitude doesn't equal # a degree in latitude. (Even at the equator that's true, # due to the earth's obateness, but that is probably small # enough to ignore.) # So use the current image aspect ratio to convert from angles # to pixels, then we'll convert from there to miles/km. if click_last_long != 0 and click_last_lat != 0 : xdiff = (cur_long - click_last_long) ydiff = (cur_lat - click_last_lat) dist = math.sqrt(xdiff*xdiff + ydiff*ydiff) # dist is now in degrees. # Convert to miles using latitude and radius of the earth. dist = dist *2. * math.pi / 360.0 * 7926 \ * math.cos(cur_lat*math.pi/180) if UseMetric : print "Distance:", round(dist*1600,2), "meters,", \ round(dist*1.6,2), "km" else : print "Distance:", round(dist*5280,2), "feet,", \ round(dist,2), "miles" angle = int(math.atan2(-ydiff, -xdiff) * 180 / math.pi) angle = angle_to_bearing(angle) print "Bearing:", angle, "=", angle_to_quadrant(angle) click_last_long = cur_long click_last_lat = cur_lat return True def angle_to_bearing(angle) : return (450 - angle) % 360 # Convert an angle (degrees) to the appropriate quadrant string, e.g. N 57 E. def angle_to_quadrant(angle) : if angle > 180 : angle = angle - 360 if angle == 0 : return "N" if angle == -90 : return "W" if angle == 90 : return "E" if angle == 180 : return "S" if angle > -90 and angle < 90 : if angle < 0 : return "N " + str(-angle) + " W" return "N " + str(angle) + " E" if angle < 0 : return "S " + str(180 + angle) + " W" return "S " + str(180 - angle) + " E" def motion_notify_event(widget, event) : global xfrac if event.is_hint: x, y, state = event.window.get_pointer() else: x = event.x; y = event.y state = event.state if state & gtk.gdk.BUTTON1_MASK and pixmap != None: xfrac = event.x / w return True #def null_event(widget, event) : def nop(*args) : return True def print_sites() : for site in KnownSites : print site[0] sys.exit(0) def ErrorOut(errstr) : print "===============" print errstr print "===============\n" Usage() def FindCollection(collname) : print "Looking for a collection named", collname # Make sure collname is a MapCollection we know about: collection = None for coll in Collections : if collname == coll.name : if not os.access(coll.location, os.X_OK) : ErrorOut("Can't access location " + coll.location + " for collection " + collname) collection = coll print "Found the collection", collection.name return collection if collection == None : ErrorOut("I can't find a map collection called " + collname) def ParseArgs() : global Collection, start_long, start_lat global VersionString while len(sys.argv) > 1 and sys.argv[1][0] == '-' : if sys.argv[1] == "-v" or sys.argv[1] == "--version" : print VersionString sys.exit(0) if sys.argv[1] == "-15" : series = 15 if sys.argv[1] == "-p" : print_sites() else : ErrorOut("Unknown flag " + sys.argv[1]) sys.argv = sys.argv[1:] if len(sys.argv) < 2 : Usage() # Check for a named argument if len(sys.argv) == 2 : for site in KnownSites : if sys.argv[1] == site[0] : Collection = FindCollection(site[3]) # site[1] and site[2] are the long and lat in deg.minutes #print site[0], site[1], site[2] centerLong = DegMinToDecDeg(site[1]) centerLat = DegMinToDecDeg(site[2]) #print "Center in decimal degrees:", centerLong, centerLat print site[0] + ":", \ DecDegToDegMinStr(centerLong), \ DecDegToDegMinStr(centerLat) start_long, start_lat = CenterToStart(centerLong, centerLat) return # Doesn't match a known site. Look for coordinates. if len(sys.argv) < 3 : Usage() try : start_long = DegMinToDecDeg(float(sys.argv[1])) start_lat = DegMinToDecDeg(float(sys.argv[2])) Collection = FindCollection(sys.argv[3]) except ValueError, e : Usage() if len(sys.argv) > 3 : MapDir = sys.argv[3] def pytopo() : global drawing_area win = gtk.Window() win.set_name("PyTopo") win.connect("destroy", gtk.main_quit) win.set_border_width(5) vbox = gtk.VBox(spacing=3) win.add(vbox) vbox.show() drawing_area = gtk.DrawingArea() # XXX remove hardwired image size numbers drawing_area.set_size_request(262 * num_long, 328 * num_lat) vbox.pack_start(drawing_area) drawing_area.show() drawing_area.set_events(gtk.gdk.EXPOSURE_MASK | gtk.gdk.BUTTON_RELEASE_MASK ) drawing_area.connect("expose-event", expose_event) drawing_area.connect("button-release-event", button_event) # The default focus in/out handlers on drawing area cause # spurious expose events. Trap the focus events, to block that: drawing_area.connect("focus-in-event", nop) drawing_area.connect("focus-out-event", nop) # Handle key presses on the drawing area. # If seeing spurious expose events, try setting them on win instead, # and comment out gtk.CAN_FOCUS. drawing_area.set_flags(gtk.CAN_FOCUS) drawing_area.connect("key-press-event", key_press_event) win.show() gtk.main() # Check for a user config file named ~/.pytopo. # This has to be outside main(), because of scoping rules. # Format of the user config file: # It is a python script, which can include arbitrary python code, # but the most useful will be KnownSites definitions, # with coordinates specified in degrees.decimal_minutes, # like this: # MapHome = "/cdrom" # KnownSites = [ # # Death Valley # [ "zabriskie", 116.475, 36.245, "dv_data" ], # [ "badwater", 116.445, 36.125, "dv_data" ], # # East Mojave # [ "zzyzyx", 116.05, 35.08, "emj_data" ] # ] userfile = os.environ["HOME"] + "/.pytopo" if os.access(userfile, os.R_OK) : execfile(userfile) # main gc.enable() ParseArgs() pytopo()