Exemple #1
0
 def __init__(self, camera_id, make, model):
     """Generate Gtk widgets and bind their properties to GSettings."""
     self.photos = set()
     
     empty_camera_label.hide()
     
     builder = Builder('camera')
     builder.get_object('camera_label').set_text(model)
     
     # GtkScale allows the user to correct the camera's clock.
     offset = builder.get_object('offset')
     offset.connect('value-changed', self.offset_handler)
     offset.connect('format-value', display_offset,
         _('Add %dm, %ds to clock.'),
         _('Subtract %dm, %ds from clock.'))
     
     # These two ComboBoxTexts are used for choosing the timezone manually.
     # They're hidden to reduce clutter when not needed.
     tz_region = builder.get_object('timezone_region')
     tz_cities = builder.get_object('timezone_cities')
     for name in tz_regions:
         tz_region.append(name, name)
     tz_region.connect('changed', self.region_handler, tz_cities)
     tz_cities.connect('changed', self.cities_handler)
     
     # TODO we're gonna need some on screen help to explain what it even
     # means to select the method of determining the timezone.
     # Back when this was radio button in a preferences window we had more
     # room for verbosity, but this combobox is *so terse* that I don't
     # really expect anybody to understand it at all.
     timezone = builder.get_object('timezone_method')
     timezone.connect('changed', self.method_handler, tz_region, tz_cities)
     
     # Push all the widgets into the UI
     get_obj('cameras_view').attach_next_to(
         builder.get_object('camera_settings'), None, BOTTOM, 1, 1)
     
     self.offset    = offset
     self.tz_method = timezone
     self.tz_region = tz_region
     self.tz_cities = tz_cities
     self.camera_id = camera_id
     self.make      = make
     self.model     = model
     
     self.gst = GSettings('camera', camera_id)
     
     self.gst.set_string('make', make)
     self.gst.set_string('model', model)
     
     self.gst.bind('offset', offset.get_adjustment(), 'value')
     self.gst.bind('timezone-method', timezone, 'active-id')
     self.gst.bind('timezone-region', tz_region, 'active')
     self.gst.bind('timezone-cities', tz_cities, 'active')
 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 IOError('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)
Exemple #4
0
 def __init__(self, filename, root, watch):
     self.filename = filename
     self.progress = get_obj('progressbar')
     self.clock    = clock()
     self.append   = None
     self.tracks   = {}
     self.polygons = set()
     
     self.parser = XMLSimpleParser(root, watch)
     self.parser.parse(filename, self.element_start, self.element_end)
     
     empty_trackfile_label.hide()
     
     points.update(self.tracks)
     keys = self.tracks.keys()
     self.alpha = min(keys)
     self.omega = max(keys)
     self.latitude = self.tracks[self.alpha].lat
     self.longitude = self.tracks[self.alpha].lon
     
     # TODO find some kind of parent widget that can group these together
     # to make it easier to get them and insert them into places.
     builder = Builder('trackfile')
     self.colorpicker = builder.get_object('colorpicker')
     self.trash = builder.get_object('unload')
     self.label = builder.get_object('trackfile_label')
     
     self.label.set_text(basename(filename))
     self.colorpicker.set_title(basename(filename))
     self.colorpicker.connect('color-set', track_color_changed, self.polygons)
     self.trash.connect('clicked', self.destroy)
     
     get_obj('trackfiles_view').attach_next_to(
         builder.get_object('trackfile_settings'), None, BOTTOM, 1, 1)
     
     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.set_string('start-timezone', self.lookup_geoname())
     self.gst.bind_with_convert('track-color', self.colorpicker, 'color',
         lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue))
     self.colorpicker.emit('color-set')
Exemple #5
0
class Camera():
    """Store per-camera configuration in GSettings."""
    
    def __init__(self, camera_id, make, model):
        """Generate Gtk widgets and bind their properties to GSettings."""
        self.photos = set()
        
        empty_camera_label.hide()
        
        builder = Builder('camera')
        builder.get_object('camera_label').set_text(model)
        
        # GtkScale allows the user to correct the camera's clock.
        offset = builder.get_object('offset')
        offset.connect('value-changed', self.offset_handler)
        offset.connect('format-value', display_offset,
            _('Add %dm, %ds to clock.'),
            _('Subtract %dm, %ds from clock.'))
        
        # These two ComboBoxTexts are used for choosing the timezone manually.
        # They're hidden to reduce clutter when not needed.
        tz_region = builder.get_object('timezone_region')
        tz_cities = builder.get_object('timezone_cities')
        for name in tz_regions:
            tz_region.append(name, name)
        tz_region.connect('changed', self.region_handler, tz_cities)
        tz_cities.connect('changed', self.cities_handler)
        
        # TODO we're gonna need some on screen help to explain what it even
        # means to select the method of determining the timezone.
        # Back when this was radio button in a preferences window we had more
        # room for verbosity, but this combobox is *so terse* that I don't
        # really expect anybody to understand it at all.
        timezone = builder.get_object('timezone_method')
        timezone.connect('changed', self.method_handler, tz_region, tz_cities)
        
        # Push all the widgets into the UI
        get_obj('cameras_view').attach_next_to(
            builder.get_object('camera_settings'), None, BOTTOM, 1, 1)
        
        self.offset    = offset
        self.tz_method = timezone
        self.tz_region = tz_region
        self.tz_cities = tz_cities
        self.camera_id = camera_id
        self.make      = make
        self.model     = model
        
        self.gst = GSettings('camera', camera_id)
        
        self.gst.set_string('make', make)
        self.gst.set_string('model', model)
        
        self.gst.bind('offset', offset.get_adjustment(), 'value')
        self.gst.bind('timezone-method', timezone, 'active-id')
        self.gst.bind('timezone-region', tz_region, 'active')
        self.gst.bind('timezone-cities', tz_cities, 'active')
    
    def method_handler(self, method, region, cities):
        """Only show manual tz selectors when necessary."""
        visible = method.get_active_id() == 'custom'
        region.set_visible(visible)
        cities.set_visible(visible)
        self.set_timezone()
    
    def region_handler(self, region, cities):
        """Populate the list of cities when a continent is selected."""
        cities.remove_all()
        for city in get_timezone(region.get_active_id(), []):
            cities.append(city, city)
    
    def cities_handler(self, cities):
        """When a city is selected, update the chosen timezone."""
        if cities.get_active_id() is not None:
            self.set_timezone()
    
    def set_found_timezone(self, found):
        """Store discovered timezone in GSettings."""
        self.gst.set_string('found-timezone', found)
    
    def set_timezone(self):
        """Set the timezone to the chosen zone and update all photos."""
        environ['TZ'] = ''
        case = lambda x, y=self.tz_method.get_active_id(): x == y
        if case('lookup'):
            # Note that this will gracefully fallback on system timezone
            # if no timezone has actually been found yet.
            environ['TZ'] = self.gst.get_string('found-timezone')
        elif case('custom'):
            region = self.tz_region.get_active_id()
            city   = self.tz_cities.get_active_id()
            if region is not None and city is not None:
                environ['TZ'] = '/'.join([region, city])
        tzset()
        self.offset_handler()
    
    def offset_handler(self, offset=None):
        """When the offset is changed, update the loaded photos."""
        for photo in self.photos:
            photo.calculate_timestamp()
    
    def get_offset(self):
        """Return the currently selected clock offset value."""
        return int(self.offset.get_value())
Exemple #6
0
class TrackFile(Coordinates):
    """Parent class for all types of GPS track files.
    
    Subclasses must implement element_start and element_end, and call them in
    the base class.
    """
    
    def __init__(self, filename, root, watch):
        self.filename = filename
        self.progress = get_obj('progressbar')
        self.clock    = clock()
        self.append   = None
        self.tracks   = {}
        self.polygons = set()
        
        self.parser = XMLSimpleParser(root, watch)
        self.parser.parse(filename, self.element_start, self.element_end)
        
        empty_trackfile_label.hide()
        
        points.update(self.tracks)
        keys = self.tracks.keys()
        self.alpha = min(keys)
        self.omega = max(keys)
        self.latitude = self.tracks[self.alpha].lat
        self.longitude = self.tracks[self.alpha].lon
        
        # TODO find some kind of parent widget that can group these together
        # to make it easier to get them and insert them into places.
        builder = Builder('trackfile')
        self.colorpicker = builder.get_object('colorpicker')
        self.trash = builder.get_object('unload')
        self.label = builder.get_object('trackfile_label')
        
        self.label.set_text(basename(filename))
        self.colorpicker.set_title(basename(filename))
        self.colorpicker.connect('color-set', track_color_changed, self.polygons)
        self.trash.connect('clicked', self.destroy)
        
        get_obj('trackfiles_view').attach_next_to(
            builder.get_object('trackfile_settings'), None, BOTTOM, 1, 1)
        
        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.set_string('start-timezone', self.lookup_geoname())
        self.gst.bind_with_convert('track-color', self.colorpicker, 'color',
            lambda x: Gdk.Color(*x), lambda x: (x.red, x.green, x.blue))
        self.colorpicker.emit('color-set')
    
    def element_start(self, name, attributes):
        """Placeholder for a method that gets overridden in subclasses."""
        return False
    
    def element_end(self, name, state):
        """Occasionally redraw the screen so the user can see what's happening."""
        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:
            map_view.remove_layer(polygon)
        for timestamp in self.tracks:
            del points[timestamp]
        self.polygons.clear()
        for widget in (self.label, self.colorpicker, self.trash):
            widget.destroy()
        del known_trackfiles[self.filename]
        if not known_trackfiles:
            empty_trackfile_label.show()
 def test_timezone_lookups(self):
     """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.
     gst = GSettings('camera', 'canon_canon_powershot_a590_is')
     gst.reset('found-timezone')
     gst.reset('offset')
     gst.set_string('timezone-method', 'lookup')
     known_cameras.clear()
     
     # Open just the GPX
     gui.open_files([DEMOFILES[3]])
     
     # At this point the camera hasn't been informed of the timezone
     self.assertEqual(gst.get_string('found-timezone'), '')
     
     # Opening a photo should place it on the map.
     gui.open_files([DEMOFILES[0]])
     self.assertEqual(gst.get_string('found-timezone'), 'America/Edmonton')
     photo = photos.values()[0]
     self.assertAlmostEqual(photo.latitude, 53.530476, 5)
     self.assertAlmostEqual(photo.longitude, -113.450635, 5)
     
     # Manually specify the timezone to be not-Edmonton and confirm that
     # the photo clamps to the end of the gpx track.
     gst.set_string('timezone-method', 'custom')
     gst.set_int('timezone-region', 1)
     gst.set_int('timezone-cities', 43)
     self.assertAlmostEqual(photo.latitude, 53.52263, 5)
     self.assertAlmostEqual(photo.longitude, -113.44898, 5)
class Camera(GObject.GObject):
    """Store per-camera configuration in GSettings.
    
    >>> from common import Dummy 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=int, minimum=-24, maximum=24)
    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':
            environ['TZ'] = 'UTC%+d' % -self.utc_offset
        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 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 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 IOError 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 IOError
        
        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 IOError('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()