def main(): # Parse the options parser = ArgumentParser() parser.add_argument("args", metavar="PHOTO", nargs='*', help='photos to be processed') parser.add_argument("-g", "--gps", dest="gps", required=True, help="The input GPS track file in .gpx format", metavar="FILE") parser.add_argument("-p", "--photos", dest="photos", help="The directory of photos", metavar="DIR") # MPickering added next option; this offset is added to the JPG values (which don't have # native timezone information) parser.add_argument("-t", "--timediff", dest="timediff", type=int, default=0, help="Add this number of hours to the JPEG times") parser.add_argument("-o", "--output", dest="output", help="The output filename for the GPX file", metavar="FILE") parser.add_argument("-u", "--update-photos", action="store_true", dest="updatephotos", help="Update the photos with GPS information") parser.add_argument("-v", "--verbose", action="store_true", dest="verbose") # not used; could be useful parser.add_argument("-i", "--interpolate", action="store_true", dest="interpolate", help="interpolate coordinates linearily between closest track points") parser.add_argument("--threshold", dest="threshold", type=int, default=5*60, help="threshold in seconds that a track point may differ from a photos timestamp still allowing them to get associated; set to -1 to allow arbitrary threshold.") options = parser.parse_args() args = options.args if options.threshold==-1: options.threshold = float("inf") # Load and Parse the GPX file to retrieve all the trackpoints xmldoc = minidom.parse(options.gps) gpx = xmldoc.getElementsByTagName("gpx") # get all trackpoints, irrespective of their track trackpointElements = gpx[0].getElementsByTagName("trkpt") photos = [] trackpoints = [] # Iterate over the trackpoints; put them in a list sorted by time for pt in trackpointElements: timeElement = pt.getElementsByTagName("time") time = "" if timeElement: timeString = timeElement[0].firstChild.data # times are in xsd:dateTime: <time>2006-12-20T15:01:06Z</time> time = mktime(strptime(timeString[0:len(timeString)-1], "%Y-%m-%dT%H:%M:%S")) trackpoint = Trackpoint() trackpoint.lat = float(pt.attributes["lat"].value) trackpoint.lon = float(pt.attributes["lon"].value) trackpoint.ele = float(pt.getElementsByTagName("ele")[0].firstChild.data) trackpoint.time = time trackpoints.append(trackpoint) trackpoints.sort(key=lambda obj:obj.time) # prepare the list of photos if options.photos: photolist = filter(lambda x: os.path.isfile(x) and \ os.path.splitext(x)[1].lower() == '.jpg', \ [os.path.join(options.photos, photo) for photo in os.listdir(options.photos)]) else: photolist = args photolist.sort() for file in photolist: photo = Photo() photo.filename = file photo.shortfilename = os.path.split(file)[1] # Parse the EXIF data and find the closest matching trackpoint tags = getExif(photo) try: photo.time = mktime(strptime(bytes.decode(tags[b'Image timestamp']), "%Y:%m:%d %H:%M:%S")) # account for time difference (GPX uses UTC; EXIF uses local time) photo.time += options.timediff * 3600 photo.trackpoint = findNearestTrackpoint(trackpoints, photo.time, options.interpolate, options.threshold) if photo.trackpoint: photos.append(photo) except: # picture may have been unreadable, may not have had timestamp, etc. print(photo.filename, traceback.format_exc()) # ready to output the photo listing impl = getDOMImplementation() gpxdoc = impl.createDocument(None, "gpx", None) doc_element = gpxdoc.documentElement # <gpx> (top-level) element attributes doc_element.setAttribute("version", "1.0") doc_element.setAttribute("creator", "gpspoint_to_gpx.py") doc_element.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") doc_element.setAttribute("xmlns", "http://www.topografix.com/GPX/1/0") doc_element.setAttribute("xsi:schemaLocation", \ "http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd") # <gpx> contains <time> element time_element = gpxdoc.createElement("time") time_element.appendChild(gpxdoc.createTextNode( \ strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()))) doc_element.appendChild(time_element) # <bounds> element needs to go next; we'll fill in the attributes later bounds_element = gpxdoc.createElement("bounds") doc_element.appendChild(bounds_element) # header is complete; now need to iterate over the photomap dictionary # and add waypoints minlat = 90.0 maxlat = -90.0 minlon = 180.0 maxlon = -180.0 for photo in photos: lat = photo.trackpoint.lat lon = photo.trackpoint.lon ele = photo.trackpoint.ele # track minimum and maximum for the <bounds> element if (lat < minlat): minlat = lat if (lat > maxlat): maxlat = lat if (lon < minlon): minlon = lon if (lon > maxlon): maxlon = lon wpt_element = gpxdoc.createElement("wpt") doc_element.appendChild(wpt_element) wpt_element.setAttribute("lat", str(lat)) wpt_element.setAttribute("lon", str(lon)) ele_element = gpxdoc.createElement("ele") wpt_element.appendChild(ele_element) ele_element.appendChild(gpxdoc.createTextNode(str(ele))) name_element = gpxdoc.createElement("name") wpt_element.appendChild(name_element) name_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) cmt_element = gpxdoc.createElement("cmt") wpt_element.appendChild(cmt_element) cmt_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) # use filename as the description # we could check the photo comment, if it exists, and use that... desc_element = gpxdoc.createElement("desc") wpt_element.appendChild(desc_element) desc_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) # now, assemble and execute the exiv2 command if options.updatephotos: setExif(photo); # finish the bounds element bounds_element.setAttribute("minlat", str(minlat)) bounds_element.setAttribute("minlon", str(minlon)) bounds_element.setAttribute("maxlat", str(maxlat)) bounds_element.setAttribute("maxlon", str(maxlon)) # dump the document if options.output: outfile = open(options.output, "w") else: outfile = sys.stdout try: PrettyPrint(gpxdoc, outfile) except: outfile.write(gpxdoc.toprettyxml(" ")) outfile.close()