Completed
Push — master ( 9a2843...3c69ee )
by Jeff
03:06
created

Screen.hasRanOnce   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
rs 7.551
cc 7
nc 6
nop 0
1
/** global: updateScreenUrl, navigator */
2
3
/**
4
 * Screen class constructor
5
 * @param {string} updateScreenUrl global screen update checks url
6
 */
7
function Screen(updateScreenUrl) {
8
  this.fields = [];
9
  this.url = updateScreenUrl;
10
  this.lastChanges = null;
11
  this.endAt = null;
12
  this.nextUrl = null;
13
  this.cache = new Preload(navigator.userAgent.toLowerCase().indexOf('kweb') == -1);
14
  this.runOnce = false;
15
}
16
17
/**
18
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
19
 */
20
Screen.prototype.checkUpdates = function() {
21
  var s = this;
22
  $.get(this.url, function(j) {
23
    if (j.success) {
24
      if (s.lastChanges == null) {
25
        s.lastChanges = j.data.lastChanges;
26
      } else if (s.lastChanges != j.data.lastChanges) {
27
        // Remote screen updated, we should reload as soon as possible
28
        s.nextUrl = null;
29
        s.reloadIn(0);
30
        return;
31
      }
32
33
      if (j.data.nextScreenUrl != null) {
34
        // Setup next screen
35
        s.nextUrl = j.data.nextScreenUrl;
36
        if (j.data.duration > 0) {
37
          s.reloadIn(j.data.duration * 1000);
38
        } else {
39
          s.runOnce = true;
40
        }
41
      }
42
    } else if (j.message == 'Unauthorized') {
43
      // Cookie/session gone bad, try to refresh with full screen reload
44
      screen.reloadIn(0);
45
    }
46
  });
47
}
48
49
/**
50
 * Start Screen reload procedure, checking for every field timeout
51
 */
52
Screen.prototype.reloadIn = function(minDuration) {
53
  var endAt = Date.now() + minDuration;
54
  if (this.endAt != null && this.endAt < endAt) {
55
    // Already going to reload sooner than asked
56
    return;
57
  }
58
59
  if (this.cache.hasPreloadingContent(true)) {
60
    // Do not break preloading
61
    return;
62
  }
63
64
  this.endAt = Date.now() + minDuration;
65
  for (var i in this.fields) {
66
    if (!this.fields.hasOwnProperty(i)) {
67
      continue;
68
    }
69
    var f = this.fields[i];
70
    if (f.timeout && f.endAt > this.endAt) {
71
      // Always wait for content display end
72
      this.endAt = f.endAt;
73
    }
74
  }
75
76
  this.reloadOnTimeout();
77
}
78
79
/**
80
 * Check if we're past the screen.endAt timeout and reload if necessary
81
 * @return {boolean} going to reload
82
 */
83
Screen.prototype.reloadOnTimeout = function() {
84
  if (screen.runOnce && screen.hasRanOnce() && !this.cache.hasPreloadingContent(true)) {
85
    // Every content has been shown, reload
86
    this.reloadNow();
87
    return true;
88
  }
89
90
  if (this.endAt != null && Date.now() >= this.endAt) {
91
    // No content to delay reload, do it now
92
    this.reloadNow();
93
    return true;
94
  }
95
96
  return false;
97
}
98
99
/**
100
 * Actual Screen reload/change screen action
101
 */
102
Screen.prototype.reloadNow = function() {
103
  if (this.nextUrl) {
104
    window.location = this.nextUrl;
105
  } else {
106
    window.location.reload();
107
  }
108
}
109
110
/**
111
 * Check every field if contents have been all played at least once
112
 * @return {Boolean} has every content been played
113
 */
114
Screen.prototype.hasRanOnce = function() {
115
  for (var f in this.fields) {
116
    if (!this.fields.hasOwnProperty(f)) {
117
      continue;
118
    }
119
    f = this.fields[f];
120
121
    for (var c in f.contents) {
122
      if (!f.contents.hasOwnProperty(c)) {
123
        continue;
124
      }
125
      c = f.contents[c];
126
127
      if (f.playCount[c.id] < 1 && c.isPreloaded()) {
128
        return false;
129
      }
130
    }
131
  }
132
133
  return true;
134
}
135
136
/**
137
 * Check every field for content
138
 * @param  {Content} data
139
 * @return {boolean} content is displayed
140
 */
141
Screen.prototype.displaysData = function(data) {
142
  return this.fields.filter(function(field) {
143
    return field.current && field.current.data == data;
144
  }).length > 0;
145
}
146
147
/**
148
 * Trigger pickNext on all fields
149
 */
150
Screen.prototype.newContentTrigger = function() {
151
  for (var f in this.fields) {
152
    if (!this.fields.hasOwnProperty(f)) {
153
      continue;
154
    }
155
156
    this.fields[f].pickNextIfNecessary();
157
  }
158
}
159
160
/**
161
 * Loop through all fields for stuckiness state
162
 * @return {boolean} are all fields stuck
163
 */
164
Screen.prototype.isAllFieldsStuck = function() {
165
  for (var f in this.fields) {
166
    if (!this.fields.hasOwnProperty(f)) {
167
      continue;
168
    }
169
170
    if (!this.fields[f].stuck && this.fields[f].canUpdate) {
171
      return false;
172
    }
173
  }
174
175
  return true;
176
}
177
178
179
/**
180
 * Content class constructor
181
 * @param {array} c content attributes
182
 */
183
function Content(c) {
184
  this.id = c.id;
185
  this.data = c.data;
186
  this.duration = c.duration * 1000;
187
  this.type = c.type;
188
  this.src = null;
189
190
  if (this.shouldPreload()) {
191
    this.queuePreload();
192
  }
193
}
194
195
/**
196
 * Check if content should be ajax preloaded
197
 * @return {boolean} shoud preload
198
 */
199
Content.prototype.shouldPreload = function() {
200
  return this.canPreload() && !this.isPreloadingOrQueued() && !this.isPreloaded();
201
}
202
203
/**
204
 * Check if content has pre-loadable material
205
 * @return {boolean} can preload
206
 */
207
Content.prototype.canPreload = function() {
208
  return this.getResource() && this.type.search(/Video|Image/) != -1;
209
}
210
211
/**
212
 * Check if content is displayable (preloaded and not too long)
213
 * @return {boolean} can display
214
 */
215
Content.prototype.canDisplay = function() {
216
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded() && this.data;
217
}
218
219
/**
220
 * Extract url from contant data
221
 * @return {string} resource url
222
 */
223
Content.prototype.getResource = function() {
224
  if (this.src) {
225
    return this.src;
226
  }
227
  var srcMatch = this.data.match(/src="([^"]+)"/);
228
  if (!srcMatch) {
229
    // All preloadable content comes with a src attribute
230
    return false;
231
  }
232
  var src = srcMatch[1];
233
  if (src.indexOf('/') === 0) {
234
    src = window.location.origin + src;
235
  }
236
  if (src.indexOf('http') !== 0) {
237
    return false;
238
  }
239
  // Get rid of fragment
240
  src = src.replace(/#.*/g, '');
241
242
  this.src = src;
243
  return src;
244
}
245
246
/**
247
 * Set content cache status
248
 * @param {string} state preload state
249
 */
250
Content.prototype.setPreloadState = function(state) {
251
  screen.cache.setState(this.getResource(), state);
252
}
253
254
/**
255
 * Check cache for preload status of content
256
 * @return {boolean} is preloaded
257
 */
258
Content.prototype.isPreloaded = function() {
259
  if (!this.canPreload()) {
260
    return true;
261
  }
262
263
  return screen.cache.isPreloaded(this.getResource());
264
}
265
266
/**
267
 * Check cache for in progress or future preloading
268
 * @return {boolean} is preloading
269
 */
270
Content.prototype.isPreloadingOrQueued = function() {
271
  return this.isPreloading() || this.isInPreloadQueue();
272
}
273
274
/**
275
 * Check cache for in progress preloading
276
 * @return {boolean} is preloading
277
 */
278
Content.prototype.isPreloading = function() {
279
  return screen.cache.isPreloading(this.getResource());
280
}
281
282
/**
283
 * Check cache for queued preloading
284
 * @return {boolean} is in preload queue
285
 */
286
Content.prototype.isInPreloadQueue = function() {
287
  return screen.cache.isInPreloadQueue(this.getResource());
288
}
289
290
/**
291
 * Call to preload content
292
 */
293
Content.prototype.preload = function() {
294
  var src = this.getResource();
295
  if (!src) {
296
    return;
297
  }
298
299
  screen.cache.preload(src);
300
}
301
302
/**
303
 * Preload content or add to preload queue
304
 */
305
Content.prototype.queuePreload = function() {
306
  var src = this.getResource();
307
  if (!src) {
308
    return;
309
  }
310
311
  if (screen.cache.hasPreloadingContent(false)) {
312
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
313
  } else {
314
    this.preload();
315
  }
316
}
317
318
319
/**
320
 * Preload class constructor
321
 * Build cache map
322
 * @param {boolean} isModernBrowser does browser support modern tags
323
 */
324
function Preload(isModernBrowser) {
325
  this.cache = {};
326
  if (isModernBrowser) {
327
    this.preload = this.preloadPrefetch;
328
  } else {
329
    this.preload = this.preloadExternal;
330
  }
331
}
332
333
/**
334
 * Preload states
335
 */
336
Preload.state = {
337
  ERR: -1,
338
  WAIT_PRELOADER: 1,
339
  PRELOADING: 2,
340
  PRELOADING_QUEUE: 3,
341
  OK: 4,
342
  NO_CONTENT: 5,
343
  HTTP_FAIL: 6,
344
}
345
346
/**
347
 * Set resource cache state
348
 * @param {string} res   resource url
349
 * @param {int}    state preload state
350
 */
351
Preload.prototype.setState = function(res, state) {
352
  this.cache[res] = state;
353
}
354
355
/**
356
 * Check resource cache for readyness state
357
 * @param  {string}  res resource url
358
 * @return {boolean}     is preloaded
359
 */
360
Preload.prototype.isPreloaded = function(res) {
361
  return this.cache[res] === Preload.state.OK;
362
}
363
364
/**
365
 * Check resource cache for preloading state
366
 * @param  {string}  res resource url
367
 * @return {boolean}     is currently preloading
368
 */
369
Preload.prototype.isPreloading = function(res) {
370
  return this.cache[res] === Preload.state.PRELOADING;
371
}
372
373
/**
374
 * Check resource cache for queued preloading state
375
 * @param  {string}  res resource url
376
 * @return {boolean}     is in preload queue
377
 */
378
Preload.prototype.isInPreloadQueue = function(res) {
379
  return this.cache[res] === Preload.state.PRELOADING_QUEUE;
380
}
381
382
/**
383
 * Check resource cache for queued preloading state during preloader pick phase
384
 * @param  {string}  res resource url
385
 * @return {boolean}     is in preload queue
386
 */
387
Preload.prototype.isWaiting = function(res) {
388
  return this.cache[res] === Preload.state.WAIT_PRELOADER;
389
}
390
391
/**
392
 * Scan resource cache for preloading resources
393
 * @param  {boolean} withQueue also check preload queue
394
 * @return {boolean}           has any resource preloading/in preload queue
395
 */
396
Preload.prototype.hasPreloadingContent = function(withQueue) {
397
  for (var res in this.cache) {
398
    if (!this.cache.hasOwnProperty(res)) {
399
      continue;
400
    }
401
402
    if (this.isPreloading(res) || this.isWaiting(res) || (withQueue && this.isInPreloadQueue(res))) {
403
      return true;
404
    }
405
  }
406
407
  return false;
408
}
409
410
/**
411
 * Preload a resource
412
 * Default implementation waits for preloader pick
413
 * @param {string} res resource url
414
 */
415
Preload.prototype.preload = function(res) {
416
  this.setState(res, Preload.state.WAIT_PRELOADER);
417
}
418
419
/**
420
 * Triggered on preloader picked
421
 * Restarts preload queue processing
422
 */
423
Preload.prototype.preloaderReady = function() {
424
  for (var res in this.cache) {
425
    if (!this.cache.hasOwnProperty(res)) {
426
      continue;
427
    }
428
429
    if (this.isWaiting(res)) {
430
      this.preload(res);
431
    }
432
  }
433
}
434
435
/**
436
 * Preload a resource by calling external preloader
437
 * @param {string} res resource url
438
 */
439
Preload.prototype.preloadExternal = function(res) {
440
  this.setState(res, Preload.state.PRELOADING);
441
  $.ajax("http://127.0.0.1:8089/pf?res=" + res).done(function(j) {
442
    switch (j.state) {
443
      case Preload.state.OK:
0 ignored issues
show
Bug introduced by
The variable Preload seems to be never declared. If this is a global, consider adding a /** global: Preload */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
444
      case Preload.state.NO_CONTENT:
445
        screen.cache.setState(res, j.state);
446
        screen.newContentTrigger();
447
        break;
448
      case Preload.state.HTTP_FAIL:
449
        screen.cache.setState(res, j.state);
450
        break;
451
      case Preload.state.ERR:
452
        screen.cache.setState(res, Preload.state.HTTP_FAIL);
453
        break;
454
      default:
455
        return;
456
    }
457
    screen.cache.preloadNext();
458
  }).fail(function() {
459
    screen.cache.preload = screen.cache.preloadAjax;
460
    screen.cache.preload(res);
461
  });
462
}
463
464
/**
465
 * Preload a resource by addinh a <link rel="prefetch"> tag
466
 * @param {string} res resource url
467
 */
468
Preload.prototype.preloadPrefetch = function(res) {
469
  this.setState(res, Preload.state.PRELOADING);
470
  $('body').append(
471
    $('<link>', {
472
      rel: 'prefetch',
473
      href: res
474
    }).load(function() {
475
      screen.cache.setState(res, Preload.state.OK);
476
      screen.newContentTrigger();
477
      screen.cache.preloadNext();
478
    }).error(function() {
479
      screen.cache.setState(res, Preload.state.HTTP_FAIL);
480
      screen.cache.preloadNext();
481
    })
482
  );
483
}
484
485
/**
486
 * Preload a resource by ajax get on the url
487
 * Check HTTP return state to validate proper cache
488
 * @param {string} res resource url
489
 */
490
Preload.prototype.preloadAjax = function(res) {
491
  this.setState(res, Preload.state.PRELOADING);
492
  $.ajax(res).done(function(data) {
493
    // Preload success
494
    if (data === '') {
495
      screen.cache.setState(res, Preload.state.NO_CONTENT);
496
    } else {
497
      screen.cache.setState(res, Preload.state.OK);
498
    }
499
    screen.newContentTrigger();
500
  }).fail(function() {
501
    // Preload failure
502
    screen.cache.setState(res, Preload.state.HTTP_FAIL);
503
  }).always(function() {
504
    screen.cache.preloadNext();
505
  });
506
}
507
508
/**
509
 * Try to preload next resource or trigger preload end event
510
 */
511
Preload.prototype.preloadNext = function() {
512
  var res = screen.cache.next();
513
  if (res) {
514
    // Preload ended, next resource
515
    screen.cache.preload(res);
516
    return;
517
  }
518
  // We've gone through all queued resources
519
  // Check if we should reload early
520
  if (screen.reloadOnTimeout()) {
521
    return;
522
  }
523
  // Trigger another update to calculate a proper screen.endAt value
524
  screen.checkUpdates();
525
}
526
527
/**
528
 * Get next resource to preload from queue
529
 * @return {string|null} next resource url
530
 */
531
Preload.prototype.next = function() {
532
  for (var res in this.cache) {
533
    if (!this.cache.hasOwnProperty(res)) {
534
      continue;
535
    }
536
537
    if (this.isInPreloadQueue(res)) {
538
      return res;
539
    }
540
  }
541
  return null;
542
}
543
544
545
/**
546
 * Field class constructor
547
 * @param {jQuery.Object} $f field object
548
 */
549
function Field($f) {
550
  this.$field = $f;
551
  this.id = $f.attr('data-id');
552
  this.url = $f.attr('data-url');
553
  this.types = $f.attr('data-types').split(' ');
554
  this.randomOrder = $f.attr('data-random') == '1';
555
  this.canUpdate = this.url != null;
556
  this.contents = [];
557
  this.playCount = {};
558
  this.previous = null;
559
  this.current = null;
560
  this.next = null;
561
  this.timeout = null;
562
  this.endAt = null;
563
  this.stuck = false;
564
}
565
566
/**
567
 * Retrieves contents from backend for this field
568
 */
569
Field.prototype.fetchContents = function() {
570
  if (!this.canUpdate) {
571
    return;
572
  }
573
574
  var f = this;
575
  $.get(this.url, function(j) {
576
    if (j.success) {
577
      f.contents = j.next.map(function(c) {
578
        if (!f.playCount[c.id]) {
579
          f.playCount[c.id] = 0;
580
        }
581
        return new Content(c);
582
      });
583
      f.pickNextIfNecessary();
584
    } else {
585
      f.setError(j.message || 'Error');
586
    }
587
  });
588
}
589
590
/**
591
 * Display error in field text
592
 */
593
Field.prototype.setError = function(err) {
594
  this.display(err);
595
}
596
597
/**
598
 * Sort contents
599
 */
600
Field.prototype.sortContents = function() {
601
  this.contents = this.randomOrder ? this.sortContentsByPlayCountRandom() : this.sortContentsByPlayCountOrdered();
602
}
603
604
/**
605
 * Sort by play count this by random
606
 * @return {[]Content} sorted contents
607
 */
608
Field.prototype.sortContentsByPlayCountRandom = function() {
609
  var f = this;
610
  return this.contents.sort(function(a, b) {
611
    var pC = f.playCount[a.id] - f.playCount[b.id];
612
    return pC != 0 ? pC : Math.random() - 0.5;
613
  });
614
}
615
616
/**
617
 * Sort by play count this by content id
618
 * @return {[]Content} sorted contents
619
 */
620
Field.prototype.sortContentsByPlayCountOrdered = function() {
621
  var f = this;
622
  return this.contents.sort(function(a, b) {
623
    var pC = f.playCount[a.id] - f.playCount[b.id];
624
    return pC != 0 ? pC : a.id - b.id;
625
  });
626
}
627
628
/**
629
 * PickNext if no content currently displayed and content is available
630
 */
631
Field.prototype.pickNextIfNecessary = function() {
632
  if (!this.timeout) {
633
    this.pickNext();
634
  }
635
}
636
637
/**
638
 * Loop through field contents to pick next displayable content
639
 */
640
Field.prototype.pickNext = function() {
641
  // Keep track of true previous content
642
  if (this.current != null) {
643
    this.previous = this.current;
644
    this.playCount[this.current.id]++;
645
  }
646
647
  if (screen.reloadOnTimeout()) {
648
    // Currently trying to reload, we're past threshold: reload now
649
    return;
650
  }
651
652
  this.current = null;
653
  var previousData = this.previous && this.previous.data;
654
655
  this.next = this.pickContent(previousData) || this.pickContent(previousData, true);
656
657
  if (this.next) {
658
    // Overwrite field with newly picked content
659
    this.displayNext();
660
    this.stuck = false;
661
  } else {
662
    // I am stuck, don't know what to display
663
    this.stuck = true;
664
    // Check other fields for stuckiness state
665
    if (screen.isAllFieldsStuck() && !screen.cache.hasPreloadingContent(true)) {
666
      // Nothing to do. Give up, reload now
667
      screen.reloadNow();
668
    }
669
  }
670
}
671
672
/**
673
 * Loop through field contents for any displayable content
674
 * @param  {string}  previousData previous content data
675
 * @param  {boolean} anyUsable    ignore constraints
676
 * @return {Content}              random usable content
677
 */
678
Field.prototype.pickContent = function(previousData, anyUsable) {
679
  this.sortContents();
680
681
  for (var i = 0; i < this.contents.length; i++) {
682
    var c = this.contents[i];
683
    // Skip too long, not preloaded or empty content
684
    if (!c.canDisplay()) {
685
      continue;
686
    }
687
688
    if (anyUsable) {
689
      // Ignore repeat & same content constraints if necessary
690
      return c;
691
    }
692
693
    // Avoid repeat same content
694
    if (c.data == previousData) {
695
      // Not enough content, display anyway
696
      if (this.contents.length < 2) {
697
        return c;
698
      }
699
      continue;
700
    }
701
702
    // Avoid same content than already displayed on other fields
703
    if (screen.displaysData(c.data)) {
704
      // Not enough content, display anyway
705
      if (this.contents.length < 3) {
706
        return c;
707
      }
708
      continue;
709
    }
710
711
    // Nice content. Display it.
712
    return c;
713
  }
714
  return null;
715
}
716
717
/**
718
 * Setup next content for field and display it
719
 */
720
Field.prototype.displayNext = function() {
721
  var f = this;
722
  if (this.next && this.next.duration > 0) {
723
    this.current = this.next
724
    this.next = null;
725
    this.display(this.current.data);
726
    if (this.timeout) {
727
      clearTimeout(this.timeout);
728
    }
729
    this.endAt = Date.now() + this.current.duration;
730
    this.timeout = setTimeout(function() {
731
      f.pickNext();
732
    }, this.current.duration);
733
  }
734
}
735
736
/**
737
 * Display data in field HTML
738
 * @param {string} data
739
 */
740
Field.prototype.display = function(data) {
741
  this.$field.html(data);
742
  this.$field.show();
743
  var $bt = this.$field.find('.bigtext');
744
  // Only first data-min/max per field is respected
745
  var minPx = $bt.attr('data-min-px') || 4;
746
  var maxPx = $bt.attr('data-max-px') || 0;
747
  $bt.parent().textfill({
748
    minFontPixels: minPx,
749
    maxFontPixels: maxPx,
750
  });
751
}
752
753
// Global screen instance
754
var screen = null;
755
756
/**
757
 * jQuery.load event
758
 * Initialize Screen and Fields
759
 * Setup updates interval timeouts
760
 */
761
function onLoad() {
762
  screen = new Screen(updateScreenUrl);
763
  // Init
764
  $('.field').each(function() {
765
    var f = new Field($(this));
766
    screen.fields.push(f);
767
    f.fetchContents();
768
  });
769
770
  if (screen.url) {
771
    // Setup content updates loop
772
    setInterval(function() {
773
      for (var f in screen.fields) {
774
        if (screen.fields.hasOwnProperty(f)) {
775
          screen.fields[f].fetchContents();
776
        }
777
      }
778
      screen.checkUpdates();
779
    }, 60000); // 1 minute is enough alongside preload queue end trigger
780
    screen.checkUpdates();
781
  }
782
}
783
784
// Run
785
$(onLoad);
786