Event.vala 33.4 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
4
 * See the COPYING file in this distribution.
5 6
 */

Jim Nelson's avatar
Jim Nelson committed
7
public class EventSourceCollection : ContainerSourceCollection {
8 9 10 11 12 13 14
    public signal void no_event_collection_altered();
    
    private ViewCollection no_event;
    
    private class NoEventViewManager : ViewManager {
        public override bool include_in_view(DataSource source) {
            // Note: this is not threadsafe
15
            return (((MediaSource) source).get_event_id().id != EventID.INVALID) ? false :
16 17 18 19
                base.include_in_view(source);
        }
    
        public override DataView create_view(DataSource source) {
20
            return new ThumbnailView((MediaSource) source);
21 22 23
        }
    }
    
24
    public EventSourceCollection() {
25 26 27 28
        base(Event.TYPENAME, "EventSourceCollection", get_event_key);

        attach_collection(LibraryPhoto.global);
        attach_collection(Video.global);
29
    }
30 31 32 33

    public void init() {
        no_event = new ViewCollection("No Event View Collection");
        
34 35 36 37 38
        NoEventViewManager view_manager = new NoEventViewManager();
        Alteration filter_alteration = new Alteration("metadata", "event");

        no_event.monitor_source_collection(LibraryPhoto.global, view_manager, filter_alteration);
        no_event.monitor_source_collection(Video.global, view_manager, filter_alteration);
39 40 41
        
        no_event.contents_altered.connect(on_no_event_collection_altered);
    }
42
    
43 44 45 46
    public override bool holds_type_of_source(DataSource source) {
        return source is Event;
    }
    
47 48 49
    private static int64 get_event_key(DataSource source) {
        Event event = (Event) source;
        EventID event_id = event.get_event_id();
50
        
51
        return event_id.id;
52
    }
53
    
Jim Nelson's avatar
Jim Nelson committed
54
    public Event? fetch(EventID event_id) {
55
        return (Event) fetch_by_key(event_id.id);
56
    }
Jim Nelson's avatar
Jim Nelson committed
57 58
    
    protected override Gee.Collection<ContainerSource>? get_containers_holding_source(DataSource source) {
59
        Event? event = ((MediaSource) source).get_event();
Jim Nelson's avatar
Jim Nelson committed
60 61 62 63 64 65 66 67 68 69
        if (event == null)
            return null;
        
        Gee.ArrayList<ContainerSource> list = new Gee.ArrayList<ContainerSource>();
        list.add(event);
        
        return list;
    }
    
    protected override ContainerSource? convert_backlink_to_container(SourceBacklink backlink) {
70
        EventID event_id = EventID(backlink.instance_id);
Jim Nelson's avatar
Jim Nelson committed
71 72 73 74 75 76 77 78 79 80 81 82
        
        Event? event = fetch(event_id);
        if (event != null)
            return event;
        
        foreach (ContainerSource container in get_holding_tank()) {
            if (((Event) container).get_event_id().id == event_id.id)
                return container;
        }
        
        return null;
    }
83 84 85 86 87 88 89 90 91
    
    public Gee.Collection<DataObject> get_no_event_objects() {
        return no_event.get_sources();
    }
    
    private void on_no_event_collection_altered(Gee.Iterable<DataObject>? added,
        Gee.Iterable<DataObject>? removed) {
        no_event_collection_altered();
    }
92 93
}

94
public class Event : EventSource, ContainerSource, Proxyable, Indexable {
95
    public const string TYPENAME = "event";
Jim Nelson's avatar
Jim Nelson committed
96
    
97 98 99
    // SHOW_COMMENTS (bool)
    public const string PROP_SHOW_COMMENTS = "show-comments";
    
100 101
    // In 24-hour time.
    public const int EVENT_BOUNDARY_HOUR = 4;
102
    
103 104
    private const time_t TIME_T_DAY = 24 * 60 * 60;
    
105 106
    private class EventSnapshot : SourceSnapshot {
        private EventRow row;
107 108
        private MediaSource primary_source;
        private Gee.ArrayList<MediaSource> attached_sources = new Gee.ArrayList<MediaSource>();
109 110 111 112
        
        public EventSnapshot(Event event) {
            // save current state of event
            row = EventTable.get_instance().get_row(event.get_event_id());
113
            primary_source = event.get_primary_source();
114
            
115 116
            // stash all the media sources in the event ... these are not used when reconstituting
            // the event, but need to know when they're destroyed, as that means the event cannot
117
            // be restored
118 119
            foreach (MediaSource source in event.get_media())
                attached_sources.add(source);
120
            
121 122
            LibraryPhoto.global.item_destroyed.connect(on_attached_source_destroyed);
            Video.global.item_destroyed.connect(on_attached_source_destroyed);
123 124 125
        }
        
        ~EventSnapshot() {
126 127
            LibraryPhoto.global.item_destroyed.disconnect(on_attached_source_destroyed);
            Video.global.item_destroyed.disconnect(on_attached_source_destroyed);
128 129 130 131 132 133 134
        }
        
        public EventRow get_row() {
            return row;
        }
        
        public override void notify_broken() {
135
            row = new EventRow();
136 137
            primary_source = null;
            attached_sources.clear();
138 139 140 141
            
            base.notify_broken();
        }
        
142 143
        private void on_attached_source_destroyed(DataSource source) {
            MediaSource media_source = (MediaSource) source;
144
            
145 146
            // if one of the media sources in the event goes away, reconstitution is impossible
            if (media_source != null && primary_source.equals(media_source))
147
                notify_broken();
148
            else if (attached_sources.contains(media_source))
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
                notify_broken();
        }
    }
    
    private class EventProxy : SourceProxy {
        public EventProxy(Event event) {
            base (event);
        }
        
        public override DataSource reconstitute(int64 object_id, SourceSnapshot snapshot) {
            EventSnapshot event_snapshot = snapshot as EventSnapshot;
            assert(event_snapshot != null);
            
            return Event.reconstitute(object_id, event_snapshot.get_row());
        }
        
    }
    
167
    public static EventSourceCollection global = null;
168 169
    
    private static EventTable event_table = null;
170
    
171
    private EventID event_id;
172
    private string? raw_name;
173
    private MediaSource primary_source;
174
    private ViewCollection view;
175 176
    private bool unlinking = false;
    private bool relinking = false;
177
    private string? indexable_keywords = null;
178
    private string? comment = null;
179
    
180
    private Event(EventRow event_row, int64 object_id = INVALID_OBJECT_ID) {
181 182
        base (object_id);
        
183 184 185
        // normalize user text
        event_row.name = prep_event_name(event_row.name);
        
186 187
        this.event_id = event_row.event_id;
        this.raw_name = event_row.name;
188
        this.comment = event_row.comment;
189
        
190 191 192 193 194 195 196 197
        Gee.Collection<string> event_source_ids =
            MediaCollectionRegistry.get_instance().get_source_ids_for_event_id(event_id);
        Gee.ArrayList<ThumbnailView> event_thumbs = new Gee.ArrayList<ThumbnailView>();
        foreach (string current_source_id in event_source_ids) {
            MediaSource? media =
                MediaCollectionRegistry.get_instance().fetch_media(current_source_id);
            if (media != null)
                event_thumbs.add(new ThumbnailView(media));
198
        }
199
        
200
        view = new ViewCollection("ViewCollection for Event %s".printf(event_id.id.to_string()));
201
        view.set_comparator(view_comparator, view_comparator_predicate);
202
        view.add_many(event_thumbs);
203
        
Jim Nelson's avatar
Jim Nelson committed
204 205
        // need to do this manually here because only want to monitor ViewCollection contents after
        // initial batch has been added, but need to keep EventSourceCollection apprised
206
        if (event_thumbs.size > 0) {
207 208
            global.notify_container_contents_added(this, event_thumbs, false);
            global.notify_container_contents_altered(this, event_thumbs, false, null, false);
Jim Nelson's avatar
Jim Nelson committed
209 210
        }
        
211 212
        // get the primary source for monitoring; if not available, use the first unrejected
        // source in the event
213 214
        primary_source = MediaCollectionRegistry.get_instance().fetch_media(event_row.primary_source_id);
        if (primary_source == null && view.get_count() > 0) {
215
            primary_source = (MediaSource) ((DataView) view.get_first_unrejected()).get_source();
216
            event_table.set_primary_source_id(event_id, primary_source.get_source_id());
217
        }
218
        
219 220 221
        // watch the primary source to reflect thumbnail changes
        if (primary_source != null)
            primary_source.thumbnail_altered.connect(on_primary_thumbnail_altered);
222

223 224 225 226
        // watch for for addition, removal, and alteration of photos and videos
        view.items_added.connect(on_media_added);
        view.items_removed.connect(on_media_removed);
        view.items_altered.connect(on_media_altered);
227 228
        
        // because we're no longer using source monitoring (for performance reasons), need to watch
229 230 231
        // for media destruction (but not removal, which is handled automatically in any case)
        LibraryPhoto.global.item_destroyed.connect(on_media_destroyed);
        Video.global.item_destroyed.connect(on_media_destroyed);
232 233
        
        update_indexable_keywords();
234
    }
235

236
    ~Event() {
237 238
        if (primary_source != null)
            primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered);
239
        
240 241 242
        view.items_altered.disconnect(on_media_altered);
        view.items_removed.disconnect(on_media_removed);
        view.items_added.disconnect(on_media_added);
243
        
244 245
        LibraryPhoto.global.item_destroyed.disconnect(on_media_destroyed);
        Video.global.item_destroyed.disconnect(on_media_destroyed);
246 247
    }
    
248 249 250 251 252 253 254 255 256
    public override string get_typename() {
        return TYPENAME;
    }
    
    public override int64 get_instance_id() {
        return get_event_id().id;
    }
    
    public override string get_representative_id() {
257
        return (primary_source != null) ? primary_source.get_source_id() : get_source_id();
258 259 260
    }
    
    public override PhotoFileFormat get_preferred_thumbnail_format() {
261
        return (primary_source != null) ? primary_source.get_preferred_thumbnail_format() :
262 263 264 265
            PhotoFileFormat.get_system_default_format();
    }

    public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error {
266
        return (primary_source != null) ? primary_source.create_thumbnail(scale) : null;
267 268
    }

269
    public static void init(ProgressMonitor? monitor = null) {
270
        event_table = EventTable.get_instance();
271
        global = new EventSourceCollection();
272
        global.init();
273
        
274
        // add all events to the global collection
275
        Gee.ArrayList<Event> events = new Gee.ArrayList<Event>();
Jim Nelson's avatar
Jim Nelson committed
276 277
        Gee.ArrayList<Event> unlinked = new Gee.ArrayList<Event>();

278 279
        Gee.ArrayList<EventRow?> event_rows = event_table.get_events();
        int count = event_rows.size;
Jim Nelson's avatar
Jim Nelson committed
280
        for (int ctr = 0; ctr < count; ctr++) {
281
            Event event = new Event(event_rows[ctr]);
282 283
            if (monitor != null)
                monitor(ctr, count);
Jim Nelson's avatar
Jim Nelson committed
284
            
285
            if (event.get_media_count() != 0) {
Jim Nelson's avatar
Jim Nelson committed
286 287 288 289 290
                events.add(event);
                
                continue;
            }
            
291 292 293 294 295
            // TODO: If event has no backlinks, destroy (empty Event stored in database) ... this
            // is expensive to check at startup time, however, should happen in background or
            // during a "clean" operation
            event.rehydrate_backlinks(global, null);
            unlinked.add(event);
Jim Nelson's avatar
Jim Nelson committed
296
        }
297
        
298
        global.add_many(events);
Jim Nelson's avatar
Jim Nelson committed
299
        global.init_add_many_unlinked(unlinked);
300 301
    }
    
302
    public static void terminate() {
303
    }
304

305
    private static int64 view_comparator(void *a, void *b) {
306 307
        return ((MediaSource) ((ThumbnailView *) a)->get_source()).get_exposure_time()
            - ((MediaSource) ((ThumbnailView *) b)->get_source()).get_exposure_time() ;
308 309
    }
    
310 311 312 313
    private static bool view_comparator_predicate(DataObject object, Alteration alteration) {
        return alteration.has_detail("metadata", "exposure-time");
    }
    
314
    public static string? prep_event_name(string? name) {
315 316 317 318 319 320
        // Ticket #3218 - we tell prepare_input_text to 
        // allow empty strings, and if the rest of the app sees
        // one, it already knows to rename it to  
        // one of the default event names.
        return prepare_input_text(name, 
            PrepareInputTextOptions.NORMALIZE | PrepareInputTextOptions.VALIDATE | 
321
            PrepareInputTextOptions.INVALID_IS_NULL | PrepareInputTextOptions.STRIP |
322
            PrepareInputTextOptions.STRIP_CRLF, DEFAULT_USER_TEXT_INPUT_LENGTH);
323 324
    }
    
325 326 327 328
    // This is used by MediaSource to notify Event when it's joined.  Don't use this to manually attach a
    // a photo or video to an Event, use MediaSource.set_event().
    public void attach(MediaSource source) {
        view.add(new ThumbnailView(source));
329 330
    }
    
331 332 333 334
    public void attach_many(Gee.Collection<MediaSource> media) {
        Gee.ArrayList<ThumbnailView> views = new Gee.ArrayList<ThumbnailView>();
        foreach (MediaSource current_source in media)
            views.add(new ThumbnailView(current_source));
335 336 337 338
        
        view.add_many(views);
    }
    
339 340 341 342 343
    // This is used by internally by Photos and Videos to notify their parent Event as to when
    // they're leaving.  Don't use this manually to detach a MediaSource; instead use
    // MediaSource.set_event( )
    public void detach(MediaSource source) {
        view.remove_marked(view.mark(view.get_view_for_source(source)));
344 345
    }
    
346 347 348 349
    public void detach_many(Gee.Collection<MediaSource> media) {
        Gee.ArrayList<ThumbnailView> views = new Gee.ArrayList<ThumbnailView>();
        foreach (MediaSource current_source in media) {
            ThumbnailView? view = (ThumbnailView?) view.get_view_for_source(current_source);
350 351 352 353 354 355 356
            if (view != null)
                views.add(view);
        }
        
        view.remove_marked(view.mark_many(views));
    }
    
357 358 359 360 361 362 363 364 365 366 367 368
    // TODO: A preferred way to do this is for ContainerSource to have an abstract interface for
    // obtaining the DataCollection in the ContainerSource of all the media objects.  Then,
    // ContinerSource could offer this helper class.
    public bool contains_media_type(string media_type) {
        foreach (MediaSource media in get_media()) {
            if (media.get_typename() == media_type)
                return true;
        }
        
        return false;
    }
    
369 370
    private Gee.ArrayList<MediaSource> views_to_media(Gee.Iterable<DataObject> views) {
        Gee.ArrayList<MediaSource> media = new Gee.ArrayList<MediaSource>();
Jim Nelson's avatar
Jim Nelson committed
371
        foreach (DataObject object in views)
372
            media.add((MediaSource) ((DataView) object).get_source());
Jim Nelson's avatar
Jim Nelson committed
373
        
374
        return media;
Jim Nelson's avatar
Jim Nelson committed
375 376
    }
    
377 378
    private void on_media_added(Gee.Iterable<DataObject> added) {
        Gee.Collection<MediaSource> media = views_to_media(added);
379 380
        global.notify_container_contents_added(this, media, relinking);
        global.notify_container_contents_altered(this, media, relinking, null, false);
Jim Nelson's avatar
Jim Nelson committed
381
        
382
        notify_altered(new Alteration.from_list("contents:added, metadata:time"));
383
    }
Jim Nelson's avatar
Jim Nelson committed
384
    
385 386 387
    // Event needs to know whenever a media source is removed from the system to update the event
    private void on_media_removed(Gee.Iterable<DataObject> removed) {
        Gee.ArrayList<MediaSource> media = views_to_media(removed);
Jim Nelson's avatar
Jim Nelson committed
388
        
389 390
        global.notify_container_contents_removed(this, media, unlinking);
        global.notify_container_contents_altered(this, null, false, media, unlinking);
Jim Nelson's avatar
Jim Nelson committed
391
        
392 393 394 395
        // update primary source if it's been removed (and there's one to take its place)
        foreach (MediaSource current_source in media) {
            if (current_source == primary_source) {
                if (get_media_count() > 0)
396
                    set_primary_source((MediaSource) view.get_first_unrejected().get_source());
Jim Nelson's avatar
Jim Nelson committed
397
                else
398
                    release_primary_source();
Jim Nelson's avatar
Jim Nelson committed
399 400 401 402 403
                
                break;
            }
        }
        
404 405
        // evaporate event if no more media in it; do not touch thereafter
        if (get_media_count() == 0) {
Jim Nelson's avatar
Jim Nelson committed
406
            global.evaporate(this);
407 408 409 410
            
            // as it's possible (highly likely, in fact) that all refs to the Event object have
            // gone out of scope now, do NOT touch this, but exit immediately
            return;
411
        }
412
        
413
        notify_altered(new Alteration.from_list("contents:removed, metadata:time"));
414 415
    }
    
416 417 418 419
    private void on_media_destroyed(DataSource source) {
        ThumbnailView? thumbnail_view = (ThumbnailView) view.get_view_for_source(source);
        if (thumbnail_view != null)
            view.remove_marked(view.mark(thumbnail_view));
420 421
    }
    
Jim Nelson's avatar
Jim Nelson committed
422
    public override void notify_relinking(SourceCollection sources) {
423
        assert(get_media_count() > 0);
Jim Nelson's avatar
Jim Nelson committed
424
        
425 426
        // If the primary source was lost in the unlink, reestablish it now.
        if (primary_source == null)
427
            set_primary_source((MediaSource) view.get_first_unrejected().get_source());
Jim Nelson's avatar
Jim Nelson committed
428 429 430
        
        base.notify_relinking(sources);
    }
431

432 433 434 435 436 437 438 439 440 441 442 443
    /** @brief This gets called when one or more media items inside this
     *  event gets modified in some fashion. If the media item's date changes
     *  and the event was previously undated, the name of the event needs to
     *  change as well; all of that happens automatically in here.
     *
     *  In addition, if the _rating_ of one or more media items has changed,
     *  the thumbnail of this event may need to change, as the primary
     *  image may have been rejected and should not be the thumbnail anymore.
     */
    private void on_media_altered(Gee.Map<DataObject, Alteration> items) {
        bool should_remake_thumb = false;
         
444
        foreach (Alteration alteration in items.values) {
445 446 447 448 449 450 451 452
            if (alteration.has_detail("metadata", "exposure-time")) {
                
                string alt_list = "metadata:time";
                
                if(!has_name())
                    alt_list += (", metadata:name");

                notify_altered(new Alteration.from_list(alt_list));
453 454 455
                
                break;
            }
456 457 458
            
            if (alteration.has_detail("metadata", "rating"))
                should_remake_thumb = true;
459
        }
460 461 462 463 464 465 466 467 468 469 470
        
        if (should_remake_thumb) {
            // check whether we actually need to remake this thumbnail...
            if ((get_primary_source() == null) || (get_primary_source().get_rating() == Rating.REJECTED)) {
                // yes, rejected - drop it and get a new one...
                set_primary_source((MediaSource) view.get_first_unrejected().get_source());
            }

            // ...otherwise, if the primary source wasn't rejected, just leave it alone.
        }
    }
471
    
472
    // This creates an empty event with a primary source.  NOTE: This does not add the source to
473
    // the event.  That must be done manually.
474
    public static Event? create_empty_event(MediaSource source) {
475
        try {
476
            Event event = new Event(EventTable.get_instance().create(source.get_source_id(), null));
477 478 479 480 481 482 483 484 485 486
            global.add(event);
            
            debug("Created empty event %s", event.to_string());
            
            return event;
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
            
            return null;
        }
487
    }
488 489 490
    
    // This will create an event using the fields supplied in EventRow.  The event_id is ignored.
    private static Event reconstitute(int64 object_id, EventRow row) {
491 492
        row.event_id = EventTable.get_instance().create_from_row(row);
        Event event = new Event(row, object_id);
493 494 495 496 497 498 499 500
        global.add(event);
        assert(global.contains(event));
        
        debug("Reconstituted event %s", event.to_string());
        
        return event;
    }
    
Jim Nelson's avatar
Jim Nelson committed
501
    public bool has_links() {
502 503
        return (LibraryPhoto.global.has_backlink(get_backlink()) ||
            Video.global.has_backlink(get_backlink()));
Jim Nelson's avatar
Jim Nelson committed
504 505 506
    }
    
    public SourceBacklink get_backlink() {
507
        return new SourceBacklink.from_source(this);
Jim Nelson's avatar
Jim Nelson committed
508 509 510
    }
    
    public void break_link(DataSource source) {
511 512
        unlinking = true;
        
513
        ((MediaSource) source).set_event(null);
514 515
        
        unlinking = false;
Jim Nelson's avatar
Jim Nelson committed
516 517
    }
    
518
    public void break_link_many(Gee.Collection<DataSource> sources) {
519 520
        unlinking = true;
        
521 522 523
        Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
        Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>();
        MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) sources, photos, videos);
524 525 526 527 528 529 530 531 532 533 534 535
        
        try {
            MediaSource.set_many_to_event(photos, null, LibraryPhoto.global.transaction_controller);
        } catch (Error err) {
            AppWindow.error_message("%s".printf(err.message));
        }
        
        try {
            MediaSource.set_many_to_event(videos, null, Video.global.transaction_controller);
        } catch (Error err) {
            AppWindow.error_message("%s".printf(err.message));
        }
536 537
        
        unlinking = false;
538 539
    }
    
Jim Nelson's avatar
Jim Nelson committed
540
    public void establish_link(DataSource source) {
541 542
        relinking = true;
        
543
        ((MediaSource) source).set_event(this);
544 545
        
        relinking = false;
Jim Nelson's avatar
Jim Nelson committed
546 547
    }
    
548
    public void establish_link_many(Gee.Collection<DataSource> sources) {
549 550
        relinking = true;
        
551 552 553 554
        Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
        Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>();
        MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) sources, photos, videos);
        
555 556 557 558 559 560 561 562 563 564 565
        try {
            MediaSource.set_many_to_event(photos, this, LibraryPhoto.global.transaction_controller);
        } catch (Error err) {
            AppWindow.error_message("%s".printf(err.message));
        }
        
        try {
            MediaSource.set_many_to_event(videos, this, Video.global.transaction_controller);
        } catch (Error err) {
            AppWindow.error_message("%s".printf(err.message));
        }
566 567
        
        relinking = false;
568 569
    }
    
570
    private void update_indexable_keywords() {
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
        string[] components = new string[3];
        int i = 0;

        string? rawname = get_raw_name();
        if (rawname != null)
            components[i++] = rawname;

        string? comment = get_comment();
        if (comment != null)
            components[i++] = comment;

        if (i == 0)
            indexable_keywords = null;
        else {
            components[i] = null;
            indexable_keywords = prepare_indexable_string(string.joinv(" ", components));
        }
588 589 590 591 592 593
    }
    
    public unowned string? get_indexable_keywords() {
        return indexable_keywords;
    }
    
594
    public bool is_in_starting_day(time_t time) {
595 596 597 598 599 600 601
        // it's possible the Event ref is held although it's been emptied
        // (such as the user removing items during an import, when events
        // are being generate on-the-fly) ... return false here and let
        // the caller make a new one
        if (view.get_count() == 0)
            return false;
        
602 603 604
        // media sources are stored in ViewCollection from earliest to latest
        MediaSource earliest_media = (MediaSource) ((DataView) view.get_at(0)).get_source();
        Time earliest_tm = Time.local(earliest_media.get_exposure_time());
605
        
606 607 608 609 610 611 612 613
        // use earliest to generate the boundary hour for that day
        Time start_boundary_tm = Time();
        start_boundary_tm.second = 0;
        start_boundary_tm.minute = 0;
        start_boundary_tm.hour = EVENT_BOUNDARY_HOUR;
        start_boundary_tm.day = earliest_tm.day;
        start_boundary_tm.month = earliest_tm.month;
        start_boundary_tm.year = earliest_tm.year;
614
        start_boundary_tm.isdst = -1;
615 616 617 618 619
        
        time_t start_boundary = start_boundary_tm.mktime();
        
        // if the earliest's exposure time was on the day but *before* the boundary hour,
        // step it back a day to the prior day's boundary
620 621
        if (earliest_tm.hour < EVENT_BOUNDARY_HOUR) {
            debug("Hour before boundary, shifting back one day");
622
            start_boundary -= TIME_T_DAY;
623
        }
624 625 626 627 628 629
        
        time_t end_boundary = (start_boundary + TIME_T_DAY - 1);
        
        return time >= start_boundary && time <= end_boundary;
    }
    
630
    // This method attempts to add a media source to an event in the supplied list that it would
631
    // naturally fit into (i.e. its exposure is within the boundary day of the earliest event
632
    // photo).  Otherwise, a new Event is generated and the source is added to it and the list.
633 634 635 636
    private static Event? generate_event(MediaSource media, ViewCollection events_so_far,
        string? event_name, out bool new_event) {
        time_t exposure_time = media.get_exposure_time();
        
637
        if (exposure_time == 0 && event_name == null) {
638
            debug("Skipping event assignment to %s: no exposure time and no event name", media.to_string());
639
            new_event = false;
640

641
            return null;
642
        }
643

644 645 646
        int count = events_so_far.get_count();
        for (int ctr = 0; ctr < count; ctr++) {
            Event event = (Event) ((EventView) events_so_far.get_at(ctr)).get_source();
647
            
648 649 650
            if ((event_name != null && event.has_name() && event_name == event.get_name())
                || event.is_in_starting_day(exposure_time)) {
                new_event = false;
651
                
652
                return event;
653 654
            }
        }
655
        
656
        // no Event so far fits the bill for this photo or video, so create a new one
657
        try {
658
            Event event = new Event(EventTable.get_instance().create(media.get_source_id(), null));
659 660 661 662
            if (event_name != null)
                event.rename(event_name);
            
            events_so_far.add(new EventView(event));
663 664 665
            
            new_event = true;
            return event;
666 667 668
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
669
        
670 671
        new_event = false;
        
672 673 674 675 676 677 678 679 680 681 682 683 684
        return null;
    }
    
    public static void generate_single_event(MediaSource media, ViewCollection events_so_far,
        string? event_name = null) {
        // do not replace existing assignments
        if (media.get_event() != null)
            return;
        
        bool new_event;
        Event? event = generate_event(media, events_so_far, event_name, out new_event);
        if (event == null)
            return;
685

686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711
        media.set_event(event);
        
        if (new_event)
            global.add(event);
    }
    
    public static void generate_many_events(Gee.Collection<MediaSource> sources, ViewCollection events_so_far) {
        Gee.Collection<Event> to_add = new Gee.ArrayList<Event>();
        foreach (MediaSource media in sources) {
            // do not replace existing assignments
            if (media.get_event() != null)
                continue;
            
            bool new_event;
            Event? event = generate_event(media, events_so_far, null, out new_event);
            if (event == null)
                continue;
            
            media.set_event(event);
            
            if (new_event)
                to_add.add(event);
        }
        
        if (to_add.size > 0)
            global.add_many(to_add);
712
    }
713
    
714 715 716 717
    public EventID get_event_id() {
        return event_id;
    }
    
718 719 720 721 722 723 724 725
    public override SourceSnapshot? save_snapshot() {
        return new EventSnapshot(this);
    }
    
    public SourceProxy get_proxy() {
        return new EventProxy(this);
    }
    
726 727 728 729 730 731 732
    public override bool equals(DataSource? source) {
        // Validate primary key is unique, which is vital to all this working
        Event? event = source as Event;
        if (event != null) {
            if (this != event) {
                assert(event_id.id != event.event_id.id);
            }
733 734
        }
        
735
        return base.equals(source);
736 737
    }
    
738
    public override string to_string() {
739
        return "Event [%s/%s] %s".printf(event_id.id.to_string(), get_object_id().to_string(), get_name());
740 741
    }
    
742 743 744 745
    public bool has_name() {
        return raw_name != null && raw_name.length > 0;
    }
    
746
    public override string get_name() {
747 748
        if (has_name())
            return get_raw_name();
749
        
750
        // if no name, pretty up the start time
751 752 753 754 755 756
        string? datestring = get_formatted_daterange();
        
        return !is_string_empty(datestring) ? datestring : _("Event %s").printf(event_id.id.to_string());
    }
    
    public string? get_formatted_daterange() {
757
        time_t start_time = get_start_time();
758 759 760 761
        time_t end_time = get_end_time();
        
        if (end_time == 0 && start_time == 0)
            return null;
762
        
763 764 765 766 767 768 769 770 771 772
        if (end_time == 0 && start_time != 0)
            return format_local_date(Time.local(start_time));
        
        Time start = Time.local(start_time);
        Time end = Time.local(end_time);
        
        if (start.day == end.day && start.month == end.month && start.day == end.day)
            return format_local_date(Time.local(start_time));
        
        return format_local_datespan(start, end);
773 774 775
    }
    
    public string? get_raw_name() {
776
        return raw_name;
777 778
    }
    
779 780 781 782
    public override string? get_comment() {
        return comment;
    }
    
783
    public bool rename(string? name) {
784 785
        string? new_name = prep_event_name(name);
        
Jonas Bushart's avatar
Jonas Bushart committed
786 787 788 789 790
        // Allow rename to date but it should go dynamic, so set name to ""
        if (new_name == get_formatted_daterange()) {
            new_name = "";
        }
        
791
        bool renamed = event_table.rename(event_id, new_name);
792
        if (renamed) {
793
            raw_name = new_name;
794 795
            update_indexable_keywords();
            notify_altered(new Alteration.from_list("metadata:name, indexable:keywords"));
796
        }
797 798 799 800
        
        return renamed;
    }
    
801 802 803 804 805 806
    public override bool set_comment(string? comment) {
        string? new_comment = MediaSource.prep_comment(comment);
        
        bool committed = event_table.set_comment(event_id, new_comment);
        if (committed) {
            this.comment = new_comment;
807
            update_indexable_keywords();
808 809 810 811 812 813
            notify_altered(new Alteration.from_list("metadata:comment, indexable:keywords"));
        }
        
        return committed;
    }
    
814 815 816 817
    public time_t get_creation_time() {
        return event_table.get_time_created(event_id);
    }
    
818
    public override time_t get_start_time() {
819
        // Because the ViewCollection is sorted by a DateComparator, the start time is the
820
        // first item.  However, we keep looking if it has no start time.
821 822
        int count = view.get_count();
        for (int i = 0; i < count; i++) {
823
            time_t time = ((MediaSource) (((DataView) view.get_at(i)).get_source())).get_exposure_time();
824 825 826 827 828
            if (time != 0)
                return time;
        }

        return 0;
829 830
    }
    
831
    public override time_t get_end_time() {
832 833
        int count = view.get_count();
        
834 835
        // Because the ViewCollection is sorted by a DateComparator, the end time is the
        // last item--no matter what.
836
        if (count == 0)
837
            return 0;
838 839
       
        return  ((MediaSource) (((DataView) view.get_at(count - 1)).get_source())).get_exposure_time();
840 841
    }
    
842
    public override uint64 get_total_filesize() {
843
        uint64 total = 0;
844 845
        foreach (MediaSource current_source in get_media()) {
            total += current_source.get_filesize();
846 847 848
        }
        
        return total;
849 850
    }
    
851
    public override int get_media_count() {
852
        return view.get_count();
853 854
    }
    
855 856
    public override Gee.Collection<MediaSource> get_media() {
        return (Gee.Collection<MediaSource>) view.get_sources();
857 858
    }
    
859
    public void mirror_photos(ViewCollection view, CreateView mirroring_ctor) {
860
        view.mirror(this.view, mirroring_ctor, null);
861 862
    }
    
863 864 865 866
    private void on_primary_thumbnail_altered() {
        notify_thumbnail_altered();
    }

867 868
    public MediaSource get_primary_source() {
        return primary_source;
869 870
    }
    
871 872
    public bool set_primary_source(MediaSource source) {
        assert(view.has_view_for_source(source));
Jim Nelson's avatar
Jim Nelson committed
873
        
874
        bool committed = event_table.set_primary_source_id(event_id, source.get_source_id());
875
        if (committed) {
876 877 878
            // switch to the new media source
            if (primary_source != null)
                primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered);
879

880 881
            primary_source = source;
            primary_source.thumbnail_altered.connect(on_primary_thumbnail_altered);
882 883 884
            
            notify_thumbnail_altered();
        }
885 886 887 888
        
        return committed;
    }
    
889 890
    private void release_primary_source() {
        if (primary_source == null)
Jim Nelson's avatar
Jim Nelson committed
891 892
            return;
        
893 894
        primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered);
        primary_source = null;
Jim Nelson's avatar
Jim Nelson committed
895 896
    }
    
897
    public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
898
        return primary_source != null ? primary_source.get_thumbnail(scale) : null;
899 900
    }
    
901
    public Gdk.Pixbuf? get_preview_pixbuf(Scaling scaling) {
902
        try {
903
            return get_primary_source().get_preview_pixbuf(scaling);
904 905 906
        } catch (Error err) {
            return null;
        }
907 908
    }

909
    public override void destroy() {
910
        // stop monitoring the photos collection
911
        view.halt_all_monitoring();
912
        
913
        // remove from the database
914 915 916 917 918
        try {
            event_table.remove(event_id);
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
919
        
920
        // mark all photos and videos for this event as now event-less
921
        PhotoTable.get_instance().drop_event(event_id);
922
        VideoTable.get_instance().drop_event(event_id);
923 924
        
        base.destroy();
925 926
   }
}