def __init__(self, filename, root, watch): self.watchlist = watch self.filename = filename self.progress = Widgets.progressbar self.polygons = set() self.widgets = Builder('trackfile') self.append = None self.tracks = {} self.clock = clock() self.gst = GSettings('trackfile', basename(filename)) if self.gst.get_string('start-timezone') is '': # Then this is the first time this file has been loaded # and we should honor the user-selected global default # track color instead of using the schema-defined default self.gst.set_value('track-color', Gst.get_value('track-color')) self.gst.bind_with_convert( 'track-color', self.widgets.colorpicker, 'color', lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue)) self.widgets.trackfile_label.set_text(basename(filename)) self.widgets.unload.connect('clicked', self.destroy) self.widgets.colorpicker.set_title(basename(filename)) self.widgets.colorpicker.connect('color-set', track_color_changed, self.polygons) Widgets.trackfile_unloads_group.add_widget(self.widgets.unload) Widgets.trackfile_colors_group.add_widget(self.widgets.colorpicker) Widgets.trackfiles_group.add_widget(self.widgets.trackfile_label) self.parse(filename, root, watch, self.element_start, self.element_end) if not self.tracks: raise OSError('No points found') points.update(self.tracks) keys = self.tracks.keys() self.alpha = min(keys) self.omega = max(keys) self.start = Coordinates(latitude = self.tracks[self.alpha].lat, longitude = self.tracks[self.alpha].lon) self.gst.set_string('start-timezone', self.start.lookup_geodata()) Widgets.trackfiles_view.add(self.widgets.trackfile_settings)
def __init__(self, camera_id): GObject.GObject.__init__(self) self.id = camera_id self.photos = set() # Bind properties to settings self.gst = GSettings('camera', camera_id) for prop in self.gst.list_keys(): self.gst.bind(prop, self) # Get notifications when properties are changed self.connect('notify::offset', self.offset_handler) self.connect('notify::timezone-method', self.timezone_handler) self.connect('notify::timezone-city', self.timezone_handler) self.connect('notify::utc-offset', self.timezone_handler)
def __init__(self, filename, root, watch): self.watchlist = watch self.filename = filename self.progress = Widgets.progressbar self.polygons = set() self.widgets = Builder('trackfile') self.append = None self.tracks = {} self.clock = clock() self.gst = GSettings('trackfile', basename(filename)) if self.gst.get_string('start-timezone') is '': # Then this is the first time this file has been loaded # and we should honor the user-selected global default # track color instead of using the schema-defined default self.gst.set_value('track-color', Gst.get_value('track-color')) self.gst.bind_with_convert( 'track-color', self.widgets.colorpicker, 'color', lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue)) self.widgets.trackfile_label.set_text(basename(filename)) self.widgets.unload.connect('clicked', self.destroy) self.widgets.colorpicker.set_title(basename(filename)) self.widgets.colorpicker.connect('color-set', track_color_changed, self.polygons) Widgets.trackfile_unloads_group.add_widget(self.widgets.unload) Widgets.trackfile_colors_group.add_widget(self.widgets.colorpicker) Widgets.trackfiles_group.add_widget(self.widgets.trackfile_label) self.parse(filename, root, watch, self.element_start, self.element_end) if not self.tracks: raise OSError('No points found') points.update(self.tracks) keys = self.tracks.keys() self.alpha = min(keys) self.omega = max(keys) self.start = Coordinates( latitude=self.tracks[self.alpha].lat, longitude=self.tracks[self.alpha].lon) self.gst.set_string('start-timezone', self.start.lookup_geodata()) Widgets.trackfiles_view.add(self.widgets.trackfile_settings)
class Camera(GObject.GObject): """Store per-camera configuration in GSettings. >>> from mock import Mock as Photo >>> cam = Camera('unknown_camera') >>> cam.add_photo(Photo()) >>> cam.num_photos 1 >>> photo = Photo() >>> cam.add_photo(photo) >>> cam.num_photos 2 >>> cam.remove_photo(photo) >>> cam.num_photos 1 >>> Camera.generate_id({'Make': 'Nikon', ... 'Model': 'Wonder Cam', ... 'Serial': '12345'}) ('12345_nikon_wonder_cam', 'Nikon Wonder Cam') >>> Camera.generate_id({}) ('unknown_camera', 'Unknown Camera') >>> cam = Camera('canon_canon_powershot_a590_is') >>> cam.timezone_method = 'lookup' >>> environ['TZ'] 'America/Edmonton' >>> cam.timezone_method = 'offset' >>> environ['TZ'].startswith('UTC') True """ offset = GObject.property(type=int, minimum=-3600, maximum=3600) utc_offset = GObject.property(type=str) found_timezone = GObject.property(type=str) timezone_method = GObject.property(type=str) timezone_region = GObject.property(type=str) timezone_city = GObject.property(type=str) @GObject.property(type=int) def num_photos(self): """Read-only count of the loaded photos taken by this camera.""" return len(self.photos) @staticmethod def generate_id(info): """Identifies a camera by serial number, make, and model.""" maker = info.get('Make', '').capitalize() model = info.get('Model', '') # Some makers put their name twice model = model if model.startswith(maker) else maker + ' ' + model camera_id = '_'.join(sorted(info.values())).lower().replace(' ', '_') return (camera_id.strip(' _') or 'unknown_camera', model.strip() or _('Unknown Camera')) @staticmethod def set_all_found_timezone(timezone): """"Set all cameras to the given timezone.""" for camera in Camera.instances: camera.found_timezone = timezone @staticmethod def timezone_handler_all(): """Update all of the photos from all of the cameras.""" for camera in Camera.instances: camera.timezone_handler() def __init__(self, camera_id): GObject.GObject.__init__(self) self.id = camera_id self.photos = set() # Bind properties to settings self.gst = GSettings('camera', camera_id) for prop in self.gst.list_keys(): self.gst.bind(prop, self) # Get notifications when properties are changed self.connect('notify::offset', self.offset_handler) self.connect('notify::timezone-method', self.timezone_handler) self.connect('notify::timezone-city', self.timezone_handler) self.connect('notify::utc-offset', self.timezone_handler) def timezone_handler(self, *ignore): """Set the timezone to the chosen zone and update all photos.""" environ['TZ'] = '' if self.timezone_method == 'lookup': # Note that this will gracefully fallback on system timezone # if no timezone has actually been found yet. environ['TZ'] = self.found_timezone elif self.timezone_method == 'offset': minutes, hours = split_float(-float(self.utc_offset)) environ['TZ'] = 'UTC{:+}:{:02}'.format( int(hours), int(abs(minutes) * 60)) elif self.timezone_method == 'custom' and \ self.timezone_region and self.timezone_city: environ['TZ'] = '/'.join( [self.timezone_region, self.timezone_city]) tzset() self.offset_handler() def get_offset_from_clock_photo(self, btn, orig, tz): """Subtract the camera's clock from the photographed clock.""" delta_s = Widgets.clock_photo_seconds.get_value_as_int() - orig.tm_sec delta_m = Widgets.clock_photo_minutes.get_value_as_int() - orig.tm_min delta_h = Widgets.clock_photo_hours.get_value_as_int() - orig.tm_hour delta_h += float(Widgets.clock_photo_tz.get_active_id()) self.offset = delta_m * 60 + delta_s utc_offset = int(tz[1:3]) + int(tz[-2:]) / 60 utc_offset *= -1 if tz.startswith('-') else 1 self.utc_offset = str(utc_offset + delta_h) self.timezone_method = 'offset' def offset_handler(self, *ignore): """When the offset is changed, update the loaded photos.""" for i, photo in enumerate(self.photos): if not i % 10: Widgets.redraw_interface() photo.calculate_timestamp(self.offset) def add_photo(self, photo): """Adds photo to the list of photos taken by this camera.""" photo.camera = self self.photos.add(photo) self.notify('num_photos') def remove_photo(self, photo): """Removes photo from the list of photos taken by this camera.""" photo.camera = None self.photos.discard(photo) self.notify('num_photos')
class Camera(GObject.GObject): """Store per-camera configuration in GSettings. >>> from mock import Mock as Photo >>> cam = Camera('unknown_camera') >>> cam.add_photo(Photo()) >>> cam.num_photos 1 >>> photo = Photo() >>> cam.add_photo(photo) >>> cam.num_photos 2 >>> cam.remove_photo(photo) >>> cam.num_photos 1 >>> Camera.generate_id({'Make': 'Nikon', ... 'Model': 'Wonder Cam', ... 'Serial': '12345'}) ('12345_nikon_wonder_cam', 'Nikon Wonder Cam') >>> Camera.generate_id({}) ('unknown_camera', 'Unknown Camera') >>> cam = Camera('canon_canon_powershot_a590_is') >>> cam.timezone_method = 'lookup' >>> environ['TZ'] 'America/Edmonton' >>> cam.timezone_method = 'offset' >>> environ['TZ'].startswith('UTC') True """ offset = GObject.property(type=int, minimum=-3600, maximum=3600) utc_offset = GObject.property(type=str) found_timezone = GObject.property(type=str) timezone_method = GObject.property(type=str) timezone_region = GObject.property(type=str) timezone_city = GObject.property(type=str) @GObject.property(type=int) def num_photos(self): """Read-only count of the loaded photos taken by this camera.""" return len(self.photos) @staticmethod def generate_id(info): """Identifies a camera by serial number, make, and model.""" maker = info.get('Make', '').capitalize() model = info.get('Model', '') # Some makers put their name twice model = model if model.startswith(maker) else maker + ' ' + model camera_id = '_'.join(sorted(info.values())).lower().replace(' ', '_') return (camera_id.strip(' _') or 'unknown_camera', model.strip() or _('Unknown Camera')) @staticmethod def set_all_found_timezone(timezone): """"Set all cameras to the given timezone.""" for camera in Camera.instances: camera.found_timezone = timezone @staticmethod def timezone_handler_all(): """Update all of the photos from all of the cameras.""" for camera in Camera.instances: camera.timezone_handler() def __init__(self, camera_id): GObject.GObject.__init__(self) self.id = camera_id self.photos = set() # Bind properties to settings self.gst = GSettings('camera', camera_id) for prop in self.gst.list_keys(): self.gst.bind(prop, self) # Get notifications when properties are changed self.connect('notify::offset', self.offset_handler) self.connect('notify::timezone-method', self.timezone_handler) self.connect('notify::timezone-city', self.timezone_handler) self.connect('notify::utc-offset', self.timezone_handler) def timezone_handler(self, *ignore): """Set the timezone to the chosen zone and update all photos.""" environ['TZ'] = '' if self.timezone_method == 'lookup': # Note that this will gracefully fallback on system timezone # if no timezone has actually been found yet. environ['TZ'] = self.found_timezone elif self.timezone_method == 'offset': minutes, hours = split_float(-float(self.utc_offset)) environ['TZ'] = 'UTC{:+}:{:02}'.format(int(hours), int(abs(minutes) * 60)) elif self.timezone_method == 'custom' and \ self.timezone_region and self.timezone_city: environ['TZ'] = '/'.join( [self.timezone_region, self.timezone_city]) tzset() self.offset_handler() def get_offset_from_clock_photo(self, btn, orig, tz): """Subtract the camera's clock from the photographed clock.""" delta_s = Widgets.clock_photo_seconds.get_value_as_int() - orig.tm_sec delta_m = Widgets.clock_photo_minutes.get_value_as_int() - orig.tm_min delta_h = Widgets.clock_photo_hours.get_value_as_int() - orig.tm_hour delta_h += float(Widgets.clock_photo_tz.get_active_id()) self.offset = delta_m * 60 + delta_s utc_offset = int(tz[1:3]) + int(tz[-2:]) / 60 utc_offset *= -1 if tz.startswith('-') else 1 self.utc_offset = str(utc_offset + delta_h) self.timezone_method = 'offset' def offset_handler(self, *ignore): """When the offset is changed, update the loaded photos.""" for i, photo in enumerate(self.photos): if not i % 10: Widgets.redraw_interface() photo.calculate_timestamp(self.offset) def add_photo(self, photo): """Adds photo to the list of photos taken by this camera.""" photo.camera = self self.photos.add(photo) self.notify('num_photos') def remove_photo(self, photo): """Removes photo from the list of photos taken by this camera.""" photo.camera = None self.photos.discard(photo) self.notify('num_photos')
def test_timezone_lookups(): """Ensure that the timezone can be discovered from the map""" # Be very careful to reset everything so that we're sure that # we're not just finding the timezone from gsettings. teardown() gst = GSettings('camera', 'canon_canon_powershot_a590_is') gst.reset('found-timezone') gst.reset('offset') gst.set_string('timezone-method', 'lookup') Camera.cache.clear() # Open just the GPX gui.open_files(GPXFILES) # At this point the camera hasn't been informed of the timezone assert gst.get_string('found-timezone') == '' # Opening a photo should place it on the map. gui.open_files([IMGFILES[0]]) print(Camera.instances) print(gst.get_string('found-timezone')) assert gst.get_string('found-timezone') == 'America/Edmonton' assert Photograph.instances assert Camera.instances photo = list(Photograph.instances).pop() assert photo.latitude == 53.530476 assert photo.longitude == -113.450635
class TrackFile(): """Parent class for all types of GPS track files. Subclasses must implement at least element_end. """ range = [] parse = XMLSimpleParser instances = set() @staticmethod def update_range(): """Ensure that TrackFile.range contains the correct info.""" while TrackFile.range: TrackFile.range.pop() if not TrackFile.instances: Widgets.empty_trackfile_list.show() else: Widgets.empty_trackfile_list.hide() TrackFile.range.extend([min(points), max(points)]) @staticmethod def get_bounding_box(): """Determine the smallest box that contains all loaded polygons.""" bounds = Champlain.BoundingBox.new() for trackfile in TrackFile.instances: for polygon in trackfile.polygons: bounds.compose(polygon.get_bounding_box()) return bounds @staticmethod def query_all_timezones(): """Try to determine the most likely timezone the user is in. First we check all TrackFiles for the timezone at their starting point, and if they are all identical, we report it. If they do not match, then the user must travel a lot, and then we simply have no idea what timezone is likely to be the one that their camera is set to. """ zones = set() for trackfile in TrackFile.instances: zones.add(trackfile.start.geotimezone) return None if len(zones) != 1 else zones.pop() @staticmethod def clear_all(*ignore): """Forget all GPX data, start over with a clean slate.""" for trackfile in list(TrackFile.instances): trackfile.destroy() points.clear() @staticmethod def load_from_file(uri): """Determine the correct subclass to instantiate. Also time everything and report how long it took. Raises OSError if the file extension is unknown, or no track points were found. """ start_time = clock() try: gpx = globals()[uri[-3:].upper() + 'File'](uri) except KeyError: raise OSError Widgets.status_message( _('%d points loaded in %.2fs.') % (len(gpx.tracks), clock() - start_time), True) if len(gpx.tracks) < 2: return TrackFile.instances.add(gpx) MapView.emit('realize') MapView.set_zoom_level(MapView.get_max_zoom_level()) MapView.ensure_visible(TrackFile.get_bounding_box(), False) TrackFile.update_range() Camera.set_all_found_timezone(gpx.start.geotimezone) def __init__(self, filename, root, watch): self.watchlist = watch self.filename = filename self.progress = Widgets.progressbar self.polygons = set() self.widgets = Builder('trackfile') self.append = None self.tracks = {} self.clock = clock() self.gst = GSettings('trackfile', basename(filename)) if self.gst.get_string('start-timezone') is '': # Then this is the first time this file has been loaded # and we should honor the user-selected global default # track color instead of using the schema-defined default self.gst.set_value('track-color', Gst.get_value('track-color')) self.gst.bind_with_convert('track-color', self.widgets.colorpicker, 'color', lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue)) self.widgets.trackfile_label.set_text(basename(filename)) self.widgets.unload.connect('clicked', self.destroy) self.widgets.colorpicker.set_title(basename(filename)) self.widgets.colorpicker.connect('color-set', track_color_changed, self.polygons) Widgets.trackfile_unloads_group.add_widget(self.widgets.unload) Widgets.trackfile_colors_group.add_widget(self.widgets.colorpicker) Widgets.trackfiles_group.add_widget(self.widgets.trackfile_label) self.parse(filename, root, watch, self.element_start, self.element_end) if not self.tracks: raise OSError('No points found') points.update(self.tracks) keys = self.tracks.keys() self.alpha = min(keys) self.omega = max(keys) self.start = Coordinates(latitude=self.tracks[self.alpha].lat, longitude=self.tracks[self.alpha].lon) self.gst.set_string('start-timezone', self.start.lookup_geodata()) Widgets.trackfiles_view.add(self.widgets.trackfile_settings) def element_start(self, name, attributes=None): """Determine when new tracks start and create a new Polygon.""" if name == self.watchlist[0]: polygon = Polygon() self.polygons.add(polygon) self.append = polygon.append_point self.widgets.colorpicker.emit('color-set') return False return True def element_end(self, name=None, state=None): """Occasionally redraw the screen so the user can see activity.""" if clock() - self.clock > .2: self.progress.pulse() while Gtk.events_pending(): Gtk.main_iteration() self.clock = clock() def destroy(self, button=None): """Die a horrible death.""" for polygon in self.polygons: MapView.remove_layer(polygon) self.polygons.clear() self.widgets.trackfile_settings.destroy() del self.cache[self.filename] TrackFile.instances.discard(self) points.clear() for trackfile in TrackFile.instances: points.update(trackfile.tracks) TrackFile.update_range()
class TrackFile(): """Parent class for all types of GPS track files. Subclasses must implement at least element_end. """ range = [] parse = XMLSimpleParser instances = set() @staticmethod def update_range(): """Ensure that TrackFile.range contains the correct info.""" while TrackFile.range: TrackFile.range.pop() if not TrackFile.instances: Widgets.empty_trackfile_list.show() else: Widgets.empty_trackfile_list.hide() TrackFile.range.extend([min(points), max(points)]) @staticmethod def get_bounding_box(): """Determine the smallest box that contains all loaded polygons.""" bounds = Champlain.BoundingBox.new() for trackfile in TrackFile.instances: for polygon in trackfile.polygons: bounds.compose(polygon.get_bounding_box()) return bounds @staticmethod def query_all_timezones(): """Try to determine the most likely timezone the user is in. First we check all TrackFiles for the timezone at their starting point, and if they are all identical, we report it. If they do not match, then the user must travel a lot, and then we simply have no idea what timezone is likely to be the one that their camera is set to. """ zones = set() for trackfile in TrackFile.instances: zones.add(trackfile.start.geotimezone) return None if len(zones) != 1 else zones.pop() @staticmethod def clear_all(*ignore): """Forget all GPX data, start over with a clean slate.""" for trackfile in list(TrackFile.instances): trackfile.destroy() points.clear() @staticmethod def load_from_file(uri): """Determine the correct subclass to instantiate. Also time everything and report how long it took. Raises OSError if the file extension is unknown, or no track points were found. """ start_time = clock() try: gpx = globals()[uri[-3:].upper() + 'File'](uri) except KeyError: raise OSError Widgets.status_message(_('%d points loaded in %.2fs.') % (len(gpx.tracks), clock() - start_time), True) if len(gpx.tracks) < 2: return TrackFile.instances.add(gpx) MapView.emit('realize') MapView.set_zoom_level(MapView.get_max_zoom_level()) MapView.ensure_visible(TrackFile.get_bounding_box(), False) TrackFile.update_range() Camera.set_all_found_timezone(gpx.start.geotimezone) def __init__(self, filename, root, watch): self.watchlist = watch self.filename = filename self.progress = Widgets.progressbar self.polygons = set() self.widgets = Builder('trackfile') self.append = None self.tracks = {} self.clock = clock() self.gst = GSettings('trackfile', basename(filename)) if self.gst.get_string('start-timezone') is '': # Then this is the first time this file has been loaded # and we should honor the user-selected global default # track color instead of using the schema-defined default self.gst.set_value('track-color', Gst.get_value('track-color')) self.gst.bind_with_convert( 'track-color', self.widgets.colorpicker, 'color', lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue)) self.widgets.trackfile_label.set_text(basename(filename)) self.widgets.unload.connect('clicked', self.destroy) self.widgets.colorpicker.set_title(basename(filename)) self.widgets.colorpicker.connect('color-set', track_color_changed, self.polygons) Widgets.trackfile_unloads_group.add_widget(self.widgets.unload) Widgets.trackfile_colors_group.add_widget(self.widgets.colorpicker) Widgets.trackfiles_group.add_widget(self.widgets.trackfile_label) self.parse(filename, root, watch, self.element_start, self.element_end) if not self.tracks: raise OSError('No points found') points.update(self.tracks) keys = self.tracks.keys() self.alpha = min(keys) self.omega = max(keys) self.start = Coordinates(latitude = self.tracks[self.alpha].lat, longitude = self.tracks[self.alpha].lon) self.gst.set_string('start-timezone', self.start.lookup_geodata()) Widgets.trackfiles_view.add(self.widgets.trackfile_settings) def element_start(self, name, attributes=None): """Determine when new tracks start and create a new Polygon.""" if name == self.watchlist[0]: polygon = Polygon() self.polygons.add(polygon) self.append = polygon.append_point self.widgets.colorpicker.emit('color-set') return False return True def element_end(self, name=None, state=None): """Occasionally redraw the screen so the user can see activity.""" if clock() - self.clock > .2: self.progress.pulse() while Gtk.events_pending(): Gtk.main_iteration() self.clock = clock() def destroy(self, button=None): """Die a horrible death.""" for polygon in self.polygons: MapView.remove_layer(polygon) self.polygons.clear() self.widgets.trackfile_settings.destroy() del self.cache[self.filename] TrackFile.instances.discard(self) points.clear() for trackfile in TrackFile.instances: points.update(trackfile.tracks) TrackFile.update_range()