diff options
Diffstat (limited to 'src/faces/Face.vala')
-rw-r--r-- | src/faces/Face.vala | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/src/faces/Face.vala b/src/faces/Face.vala new file mode 100644 index 0000000..9be33c9 --- /dev/null +++ b/src/faces/Face.vala @@ -0,0 +1,681 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +#if ENABLE_FACES +public class FaceSourceCollection : ContainerSourceCollection { + private Gee.HashMap<string, Face> name_map = new Gee.HashMap<string, Face> + ((Gee.HashDataFunc)Face.hash_name_string, (Gee.EqualDataFunc)Face.equal_name_strings); + private Gee.HashMap<MediaSource, Gee.List<Face>> source_map = + new Gee.HashMap<MediaSource, Gee.List<Face>>(); + + public FaceSourceCollection() { + base (Face.TYPENAME, "FaceSourceCollection", get_face_key); + + attach_collection(LibraryPhoto.global); + } + + public override bool holds_type_of_source(DataSource source) { + return source is Face; + } + + private static int64 get_face_key(DataSource source) { + return ((Face) source).get_instance_id(); + } + + protected override Gee.Collection<ContainerSource>? get_containers_holding_source(DataSource source) { + return fetch_for_source((MediaSource) source); + } + + public override ContainerSource? convert_backlink_to_container(SourceBacklink backlink) { + FaceID face_id = FaceID(backlink.instance_id); + + Face? face = fetch(face_id); + if (face != null) + return face; + + foreach (ContainerSource container in get_holding_tank()) { + face = (Face) container; + if (face.get_face_id().id == face_id.id) + return face; + } + + return null; + } + + public Face? fetch(FaceID face_id) { + return (Face) fetch_by_key(face_id.id); + } + + public bool exists(string name) { + return name_map.has_key(name); + } + + public Gee.Collection<string> get_all_names() { + return name_map.keys; + } + + // Returns a list of all Faces associated with the media source in no particular order. + // + // NOTE: As a search optimization, this returns the list that is maintained by Faces.global. + // Do NOT modify this list. + public Gee.List<Face>? fetch_for_source(MediaSource source) { + return source_map.get(source); + } + + // Returns null if not Face with name exists. + public Face? fetch_by_name(string name) { + return name_map.get(name); + } + + public Face? restore_face_from_holding_tank(string name) { + Face? found = null; + foreach (ContainerSource container in get_holding_tank()) { + Face face = (Face) container; + if (face.get_name() == name) { + found = face; + + break; + } + } + + if (found != null) { + bool relinked = relink_from_holding_tank(found); + assert(relinked); + } + + return found; + } + + protected override void notify_items_added(Gee.Iterable<DataObject> added) { + foreach (DataObject object in added) { + Face face = (Face) object; + + assert(!name_map.has_key(face.get_name())); + name_map.set(face.get_name(), face); + } + + base.notify_items_added(added); + } + + protected override void notify_items_removed(Gee.Iterable<DataObject> removed) { + foreach (DataObject object in removed) { + Face face = (Face) object; + + bool unset = name_map.unset(face.get_name()); + assert(unset); + } + + base.notify_items_removed(removed); + } + + protected override void notify_items_altered(Gee.Map<DataObject, Alteration> map) { + foreach (DataObject object in map.keys) { + Face face = (Face) object; + + string? old_name = null; + + // look for this face being renamed + Gee.MapIterator<string, Face> iter = name_map.map_iterator(); + while (iter.next()) { + if (!iter.get_value().equals(face)) + continue; + + old_name = iter.get_key(); + + break; + } + + assert(old_name != null); + + if (face.get_name() != old_name) { + name_map.unset(old_name); + name_map.set(face.get_name(), face); + } + } + + base.notify_items_altered(map); + } + + protected override void notify_container_contents_added(ContainerSource container, + Gee.Collection<DataObject> added, bool relinking) { + Face face = (Face) container; + Gee.Collection<MediaSource> sources = (Gee.Collection<MediaSource>) added; + + foreach (MediaSource source in sources) { + Gee.List<Face>? faces = source_map.get(source); + if (faces == null) { + faces = new Gee.ArrayList<Face>(); + source_map.set(source, faces); + } + + bool is_added = faces.add(face); + assert(is_added); + } + + base.notify_container_contents_added(container, added, relinking); + } + + protected override void notify_container_contents_removed(ContainerSource container, + Gee.Collection<DataObject> removed, bool unlinking) { + Face face = (Face) container; + Gee.Collection<MediaSource> sources = (Gee.Collection<MediaSource>) removed; + + foreach (MediaSource source in sources) { + Gee.List<Face>? faces = source_map.get(source); + assert(faces != null); + + bool is_removed = faces.remove(face); + assert(is_removed); + + if (faces.size == 0) + source_map.unset(source); + } + + base.notify_container_contents_removed(container, removed, unlinking); + } +} + +public class Face : DataSource, ContainerSource, Proxyable, Indexable { + public const string TYPENAME = "face"; + + private class FaceSnapshot : SourceSnapshot { + private FaceRow row; + private Gee.HashSet<MediaSource> sources = new Gee.HashSet<MediaSource>(); + + public FaceSnapshot(Face face) { + // stash current state of Face + row = face.row; + + // stash photos attached to this face ... if any are destroyed, the face + // cannot be reconstituted + foreach (MediaSource source in face.get_sources()) + sources.add(source); + + LibraryPhoto.global.item_destroyed.connect(on_source_destroyed); + } + + ~FaceSnapshot() { + LibraryPhoto.global.item_destroyed.disconnect(on_source_destroyed); + } + + public FaceRow get_row() { + return row; + } + + public override void notify_broken() { + row = new FaceRow(); + sources.clear(); + + base.notify_broken(); + } + + private void on_source_destroyed(DataSource source) { + if (sources.contains((MediaSource) source)) + notify_broken(); + } + } + + private class FaceProxy : SourceProxy { + public FaceProxy(Face face) { + base (face); + } + + public override DataSource reconstitute(int64 object_id, SourceSnapshot snapshot) { + return Face.reconstitute(object_id, ((FaceSnapshot) snapshot).get_row()); + } + } + + public static FaceSourceCollection global = null; + + private FaceRow row; + private ViewCollection media_views; + private string? name_collation_key = null; + private bool unlinking = false; + private bool relinking = false; + private string? indexable_keywords = null; + + private Face(FaceRow row, int64 object_id = INVALID_OBJECT_ID) { + base (object_id); + + this.row = row; + + // normalize user text + this.row.name = prep_face_name(this.row.name); + + Gee.Set<PhotoID?> photo_id_list = FaceLocation.get_photo_ids_by_face(this); + Gee.ArrayList<Photo> photo_list = new Gee.ArrayList<Photo>(); + Gee.ArrayList<ThumbnailView> thumbnail_views = new Gee.ArrayList<ThumbnailView>(); + if (photo_id_list != null) { + foreach (PhotoID photo_id in photo_id_list) { + MediaSource? current_source = + LibraryPhoto.global.fetch_by_source_id(PhotoID.upgrade_photo_id_to_source_id(photo_id)); + if (current_source == null) + continue; + + photo_list.add((Photo) current_source); + thumbnail_views.add(new ThumbnailView(current_source)); + } + } + + // add to internal ViewCollection, which maintains media sources associated with this face + media_views = new ViewCollection("ViewCollection for face %s".printf(row.face_id.id.to_string())); + media_views.add_many(thumbnail_views); + + // need to do this manually here because only want to monitor photo_contents_altered + // after add_many() here; but need to keep the FaceSourceCollection apprised + if (photo_list.size > 0) { + global.notify_container_contents_added(this, photo_list, false); + global.notify_container_contents_altered(this, photo_list, false, null, false); + } + + // monitor ViewCollection to (a) keep the in-memory list of source ids up-to-date, and + // (b) update the database whenever there's a change; + media_views.contents_altered.connect(on_media_views_contents_altered); + + // monitor the global collections to trap when photos are destroyed, then + // automatically remove from the face + LibraryPhoto.global.items_destroyed.connect(on_sources_destroyed); + + update_indexable_keywords(); + } + + ~Face() { + media_views.contents_altered.disconnect(on_media_views_contents_altered); + LibraryPhoto.global.items_destroyed.disconnect(on_sources_destroyed); + } + + public static void init(ProgressMonitor? monitor) { + global = new FaceSourceCollection(); + + // scoop up all the rows at once + Gee.List<FaceRow?> rows = null; + try { + rows = FaceTable.get_instance().get_all_rows(); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + // turn them into Face objects + Gee.ArrayList<Face> faces = new Gee.ArrayList<Face>(); + Gee.ArrayList<Face> unlinked = new Gee.ArrayList<Face>(); + int count = rows.size; + for (int ctr = 0; ctr < count; ctr++) { + FaceRow row = rows.get(ctr); + + // make sure the face name is valid + string? name = prep_face_name(row.name); + if (name == null) { + // TODO: More graceful handling of this situation would be to rename the face or + // alert the user. + warning("Invalid face name \"%s\": removing from database", row.name); + try { + FaceTable.get_instance().remove(row.face_id); + } catch (DatabaseError err) { + warning("Unable to delete face \"%s\": %s", row.name, err.message); + } + + continue; + } + + row.name = name; + + Face face = new Face(row); + if (monitor != null) + monitor(ctr, count); + + if (face.get_sources_count() != 0) { + faces.add(face); + + continue; + } + + if (face.has_links()) { + face.rehydrate_backlinks(global, null); + unlinked.add(face); + + continue; + } + + warning("Empty face %s found with no backlinks, destroying", face.to_string()); + face.destroy_orphan(true); + } + + // add them all at once to the SourceCollection + global.add_many(faces); + global.init_add_many_unlinked(unlinked); + } + + public static void terminate() { + } + + public static int compare_names(void *a, void *b) { + Face *aface = (Face *) a; + Face *bface = (Face *) b; + + return String.precollated_compare(aface->get_name(), aface->get_name_collation_key(), + bface->get_name(), bface->get_name_collation_key()); + } + + public static uint hash_name_string(void *a) { + return String.collated_hash(a); + } + + public static bool equal_name_strings(void *a, void *b) { + return String.collated_equals(a, b); + } + + // Returns a Face for the name, creating a new empty one if it does not already exist. + // name should have already been prepared by prep_face_name. + public static Face for_name(string name) { + Face? face = global.fetch_by_name(name); + if (face == null) + face = global.restore_face_from_holding_tank(name); + + if (face != null) + return face; + + // create a new Face for this name + try { + face = new Face(FaceTable.get_instance().add(name)); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + global.add(face); + + return face; + } + + // Utility function to cleanup a face name that comes from user input and prepare it for use + // in the system and storage in the database. Returns null if the name is unacceptable. + public static string? prep_face_name(string name) { + return prepare_input_text(name, PrepareInputTextOptions.DEFAULT, DEFAULT_USER_TEXT_INPUT_LENGTH); + } + + public override string get_typename() { + return TYPENAME; + } + + public override int64 get_instance_id() { + return get_face_id().id; + } + + public override string get_name() { + return row.name; + } + + public string get_name_collation_key() { + if (name_collation_key == null) + name_collation_key = row.name.collate_key(); + + return name_collation_key; + } + + public override string to_string() { + return "Face %s (%d sources)".printf(row.name, media_views.get_count()); + } + + public override bool equals(DataSource? source) { + // Validate uniqueness of primary key + Face? face = source as Face; + if (face != null) { + if (face != this) { + assert(face.row.face_id.id != row.face_id.id); + } + } + + return base.equals(source); + } + + public FaceID get_face_id() { + return row.face_id; + } + + public override SourceSnapshot? save_snapshot() { + return new FaceSnapshot(this); + } + + public SourceProxy get_proxy() { + return new FaceProxy(this); + } + + private static Face reconstitute(int64 object_id, FaceRow row) { + // fill in the row with the new FaceID for this reconstituted face + try { + row.face_id = FaceTable.get_instance().create_from_row(row); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + Face face = new Face(row, object_id); + global.add(face); + + debug("Reconstituted %s", face.to_string()); + + return face; + } + + public bool has_links() { + return LibraryPhoto.global.has_backlink(get_backlink()); + } + + public SourceBacklink get_backlink() { + return new SourceBacklink.from_source(this); + } + + public void break_link(DataSource source) { + unlinking = true; + + detach((LibraryPhoto) source); + + unlinking = false; + } + + public void break_link_many(Gee.Collection<DataSource> sources) { + unlinking = true; + + detach_many((Gee.Collection<LibraryPhoto>) sources); + + unlinking = false; + } + + public void establish_link(DataSource source) { + relinking = true; + + attach((LibraryPhoto) source); + + relinking = false; + } + + public void establish_link_many(Gee.Collection<DataSource> sources) { + relinking = true; + + attach_many((Gee.Collection<LibraryPhoto>) sources); + + relinking = false; + } + + private void update_indexable_keywords() { + indexable_keywords = prepare_indexable_string(get_name()); + } + + public unowned string? get_indexable_keywords() { + return indexable_keywords; + } + + public void attach(MediaSource source) { + if (!media_views.has_view_for_source(source)) + media_views.add(new ThumbnailView(source)); + } + + public void attach_many(Gee.Collection<MediaSource> sources) { + Gee.ArrayList<ThumbnailView> view_list = new Gee.ArrayList<ThumbnailView>(); + foreach (MediaSource source in sources) { + if (!media_views.has_view_for_source(source)) + view_list.add(new ThumbnailView(source)); + } + + if (view_list.size > 0) + media_views.add_many(view_list); + } + + public bool detach(MediaSource source) { + DataView? view = media_views.get_view_for_source(source); + if (view == null) + return false; + + media_views.remove_marked(media_views.mark(view)); + + return true; + } + + public int detach_many(Gee.Collection<MediaSource> sources) { + int count = 0; + + Marker marker = media_views.start_marking(); + foreach (MediaSource source in sources) { + DataView? view = media_views.get_view_for_source(source); + if (view == null) + continue; + + marker.mark(view); + count++; + } + + media_views.remove_marked(marker); + + return count; + } + + // Returns false if the name already exists or a bad name. + public bool rename(string name) { + string? new_name = prep_face_name(name); + if (new_name == null) + return false; + + if (Face.global.exists(new_name)) + return false; + + try { + FaceTable.get_instance().rename(row.face_id, new_name); + } catch (DatabaseError err) { + AppWindow.database_error(err); + return false; + } + + row.name = new_name; + name_collation_key = null; + + update_indexable_keywords(); + + notify_altered(new Alteration.from_list("metadata:name, indexable:keywords")); + + return true; + } + + public bool contains(MediaSource source) { + return media_views.has_view_for_source(source); + } + + public int get_sources_count() { + return media_views.get_count(); + } + + public Gee.Collection<MediaSource> get_sources() { + return (Gee.Collection<MediaSource>) media_views.get_sources(); + } + + public void mirror_sources(ViewCollection view, CreateView mirroring_ctor) { + view.mirror(media_views, mirroring_ctor, null); + } + + private void on_media_views_contents_altered(Gee.Iterable<DataView>? added, + Gee.Iterable<DataView>? removed) { + Gee.Set<PhotoID?>? photo_id_list = FaceLocation.get_photo_ids_by_face(this); + + Gee.Collection<Photo> added_photos = null; + if (added != null) { + added_photos = new Gee.ArrayList<Photo>(); + foreach (DataView view in added) { + Photo photo = (Photo) view.get_source(); + + if (photo_id_list != null) + assert(!photo_id_list.contains(photo.get_photo_id())); + + bool is_added = added_photos.add(photo); + assert(is_added); + } + } + + Gee.Collection<Photo> removed_photos = null; + if (removed != null) { + assert(photo_id_list != null); + + removed_photos = new Gee.ArrayList<Photo>(); + foreach (DataView view in removed) { + Photo photo = (Photo) view.get_source(); + + assert(photo_id_list.contains(photo.get_photo_id())); + + bool is_added = removed_photos.add(photo); + assert(is_added); + } + } + + if (removed_photos != null) + foreach (Photo photo in removed_photos) + FaceLocation.destroy(get_face_id(), photo.get_photo_id()); + + // notify of changes to this face + if (added_photos != null) + global.notify_container_contents_added(this, added_photos, relinking); + + if (removed_photos != null) + global.notify_container_contents_removed(this, removed_photos, unlinking); + + if (added_photos != null || removed_photos != null) { + global.notify_container_contents_altered(this, added_photos, relinking, removed_photos, + unlinking); + } + + // if no more sources, face evaporates; do not touch "this" afterwards + if (media_views.get_count() == 0) + global.evaporate(this); + } + + private void on_sources_destroyed(Gee.Collection<DataSource> sources) { + detach_many((Gee.Collection<MediaSource>) sources); + } + + public override void destroy() { + // detach all remaining sources from the face, so observers are informed ... need to detach + // the contents_altered handler because it will destroy this object when sources is empty, + // which is bad reentrancy mojo (but hook it back up for the dtor's sake) + if (media_views.get_count() > 0) { + media_views.contents_altered.disconnect(on_media_views_contents_altered); + + Gee.ArrayList<MediaSource> removed = new Gee.ArrayList<MediaSource>(); + removed.add_all((Gee.Collection<MediaSource>) media_views.get_sources()); + + media_views.clear(); + + global.notify_container_contents_removed(this, removed, false); + global.notify_container_contents_altered(this, null, false, removed, false); + + media_views.contents_altered.connect(on_media_views_contents_altered); + } + + try { + FaceTable.get_instance().remove(row.face_id); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + base.destroy(); + } +} + +#endif |