Completed
Push — master ( cd8ebc...9a2843 )
by Jeff
05:47
created

Screen.checkUpdates   C

Complexity

Conditions 8
Paths 15

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
c 7
b 0
f 0
dl 0
loc 26
rs 5.3846
cc 8
nc 15
nop 1
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
        }
39
      }
40
      if (j.data.duration === 0) {
41
        s.runOnce = true;
42
      }
43
    } else if (j.message == 'Unauthorized') {
44
      // Cookie/session gone bad, try to refresh with full screen reload
45
      screen.reloadIn(0);
46
    }
47
  });
48
}
49
50
/**
51
 * Start Screen reload procedure, checking for every field timeout
52
 */
53
Screen.prototype.reloadIn = function(minDuration) {
54
  var endAt = Date.now() + minDuration;
55
  if (this.endAt != null && this.endAt < endAt) {
56
    // Already going to reload sooner than asked
57
    return;
58
  }
59
60
  if (this.cache.hasPreloadingContent(true)) {
61
    // Do not break preloading
62
    return;
63
  }
64
65
  this.endAt = Date.now() + minDuration;
66
  for (var i in this.fields) {
67
    if (!this.fields.hasOwnProperty(i)) {
68
      continue;
69
    }
70
    var f = this.fields[i];
71
    if (f.timeout && f.endAt > this.endAt) {
72
      // Always wait for content display end
73
      this.endAt = f.endAt;
74
    }
75
  }
76
77
  this.reloadOnTimeout();
78
}
79
80
/**
81
 * Check if we're past the screen.endAt timeout and reload if necessary
82
 * @return {boolean} going to reload
83
 */
84
Screen.prototype.reloadOnTimeout = function() {
85
  if (this.endAt != null && Date.now() >= this.endAt) {
86
    // No content to delay reload, do it now
87
    this.reloadNow();
88
    return true;
89
  }
90
91
  return false;
92
}
93
94
/**
95
 * Actual Screen reload/change screen action
96
 */
97
Screen.prototype.reloadNow = function() {
98
  if (this.nextUrl) {
99
    window.location = this.nextUrl;
100
  } else {
101
    window.location.reload();
102
  }
103
}
104
105
/**
106
 * Check every field for content
107
 * @param  {Content} data
108
 * @return {boolean} content is displayed
109
 */
110
Screen.prototype.displaysData = function(data) {
111
  return this.fields.filter(function(field) {
112
    return field.current && field.current.data == data;
113
  }).length > 0;
114
}
115
116
/**
117
 * Trigger pickNext on all fields
118
 */
119
Screen.prototype.newContentTrigger = function() {
120
  for (var f in this.fields) {
121
    if (!this.fields.hasOwnProperty(f)) {
122
      continue;
123
    }
124
125
    this.fields[f].pickNextIfNecessary();
126
  }
127
}
128
129
/**
130
 * Loop through all fields for stuckiness state
131
 * @return {boolean} are all fields stuck
132
 */
133
Screen.prototype.isAllFieldsStuck = function() {
134
  for (var f in this.fields) {
135
    if (!this.fields.hasOwnProperty(f)) {
136
      continue;
137
    }
138
139
    if (!this.fields[f].stuck && this.fields[f].canUpdate) {
140
      return false;
141
    }
142
  }
143
144
  return true;
145
}
146
147
148
/**
149
 * Content class constructor
150
 * @param {array} c content attributes
151
 */
152
function Content(c) {
153
  this.id = c.id;
154
  this.data = c.data;
155
  this.duration = c.duration * 1000;
156
  this.type = c.type;
157
  this.src = null;
158
159
  if (this.shouldPreload()) {
160
    this.queuePreload();
161
  }
162
}
163
164
/**
165
 * Check if content should be ajax preloaded
166
 * @return {boolean} shoud preload
167
 */
168
Content.prototype.shouldPreload = function() {
169
  return this.canPreload() && !this.isPreloadingOrQueued() && !this.isPreloaded();
170
}
171
172
/**
173
 * Check if content has pre-loadable material
174
 * @return {boolean} can preload
175
 */
176
Content.prototype.canPreload = function() {
177
  return this.getResource() && this.type.search(/Video|Image/) != -1;
178
}
179
180
/**
181
 * Check if content is displayable (preloaded and not too long)
182
 * @return {boolean} can display
183
 */
184
Content.prototype.canDisplay = function() {
185
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded() && this.data;
186
}
187
188
/**
189
 * Extract url from contant data
190
 * @return {string} resource url
191
 */
192
Content.prototype.getResource = function() {
193
  if (this.src) {
194
    return this.src;
195
  }
196
  var srcMatch = this.data.match(/src="([^"]+)"/);
197
  if (!srcMatch) {
198
    // All preloadable content comes with a src attribute
199
    return false;
200
  }
201
  var src = srcMatch[1];
202
  if (src.indexOf('/') === 0) {
203
    src = window.location.origin + src;
204
  }
205
  if (src.indexOf('http') !== 0) {
206
    return false;
207
  }
208
  // Get rid of fragment
209
  src = src.replace(/#.*/g, '');
210
211
  this.src = src;
212
  return src;
213
}
214
215
/**
216
 * Set content cache status
217
 * @param {string} state preload state
218
 */
219
Content.prototype.setPreloadState = function(state) {
220
  screen.cache.setState(this.getResource(), state);
221
}
222
223
/**
224
 * Check cache for preload status of content
225
 * @return {boolean} is preloaded
226
 */
227
Content.prototype.isPreloaded = function() {
228
  if (!this.canPreload()) {
229
    return true;
230
  }
231
232
  return screen.cache.isPreloaded(this.getResource());
233
}
234
235
/**
236
 * Check cache for in progress or future preloading
237
 * @return {boolean} is preloading
238
 */
239
Content.prototype.isPreloadingOrQueued = function() {
240
  return this.isPreloading() || this.isInPreloadQueue();
241
}
242
243
/**
244
 * Check cache for in progress preloading
245
 * @return {boolean} is preloading
246
 */
247
Content.prototype.isPreloading = function() {
248
  return screen.cache.isPreloading(this.getResource());
249
}
250
251
/**
252
 * Check cache for queued preloading
253
 * @return {boolean} is in preload queue
254
 */
255
Content.prototype.isInPreloadQueue = function() {
256
  return screen.cache.isInPreloadQueue(this.getResource());
257
}
258
259
/**
260
 * Call to preload content
261
 */
262
Content.prototype.preload = function() {
263
  var src = this.getResource();
264
  if (!src) {
265
    return;
266
  }
267
268
  screen.cache.preload(src);
269
}
270
271
/**
272
 * Preload content or add to preload queue
273
 */
274
Content.prototype.queuePreload = function() {
275
  var src = this.getResource();
276
  if (!src) {
277
    return;
278
  }
279
280
  if (screen.cache.hasPreloadingContent(false)) {
281
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
282
  } else {
283
    this.preload();
284
  }
285
}
286
287
288
/**
289
 * Preload class constructor
290
 * Build cache map
291
 * @param {boolean} isModernBrowser does browser support modern tags
292
 */
293
function Preload(isModernBrowser) {
294
  this.cache = {};
295
  if (isModernBrowser) {
296
    this.preload = this.preloadPrefetch;
297
  } else {
298
    this.preload = this.preloadExternal;
299
  }
300
}
301
302
/**
303
 * Preload states
304
 */
305
Preload.state = {
306
  ERR: -1,
307
  WAIT_PRELOADER: 1,
308
  PRELOADING: 2,
309
  PRELOADING_QUEUE: 3,
310
  OK: 4,
311
  NO_CONTENT: 5,
312
  HTTP_FAIL: 6,
313
}
314
315
/**
316
 * Set resource cache state
317
 * @param {string} res   resource url
318
 * @param {int}    state preload state
319
 */
320
Preload.prototype.setState = function(res, state) {
321
  this.cache[res] = state;
322
}
323
324
/**
325
 * Check resource cache for readyness state
326
 * @param  {string}  res resource url
327
 * @return {boolean}     is preloaded
328
 */
329
Preload.prototype.isPreloaded = function(res) {
330
  return this.cache[res] === Preload.state.OK;
331
}
332
333
/**
334
 * Check resource cache for preloading state
335
 * @param  {string}  res resource url
336
 * @return {boolean}     is currently preloading
337
 */
338
Preload.prototype.isPreloading = function(res) {
339
  return this.cache[res] === Preload.state.PRELOADING;
340
}
341
342
/**
343
 * Check resource cache for queued preloading state
344
 * @param  {string}  res resource url
345
 * @return {boolean}     is in preload queue
346
 */
347
Preload.prototype.isInPreloadQueue = function(res) {
348
  return this.cache[res] === Preload.state.PRELOADING_QUEUE;
349
}
350
351
/**
352
 * Check resource cache for queued preloading state during preloader pick phase
353
 * @param  {string}  res resource url
354
 * @return {boolean}     is in preload queue
355
 */
356
Preload.prototype.isWaiting = function(res) {
357
  return this.cache[res] === Preload.state.WAIT_PRELOADER;
358
}
359
360
/**
361
 * Scan resource cache for preloading resources
362
 * @param  {boolean} withQueue also check preload queue
363
 * @return {boolean}           has any resource preloading/in preload queue
364
 */
365
Preload.prototype.hasPreloadingContent = function(withQueue) {
366
  for (var res in this.cache) {
367
    if (!this.cache.hasOwnProperty(res)) {
368
      continue;
369
    }
370
371
    if (this.isPreloading(res) || this.isWaiting(res) || (withQueue && this.isInPreloadQueue(res))) {
372
      return true;
373
    }
374
  }
375
376
  return false;
377
}
378
379
/**
380
 * Preload a resource
381
 * Default implementation waits for preloader pick
382
 * @param {string} res resource url
383
 */
384
Preload.prototype.preload = function(res) {
385
  this.setState(res, Preload.state.WAIT_PRELOADER);
386
}
387
388
/**
389
 * Triggered on preloader picked
390
 * Restarts preload queue processing
391
 */
392
Preload.prototype.preloaderReady = function() {
393
  for (var res in this.cache) {
394
    if (!this.cache.hasOwnProperty(res)) {
395
      continue;
396
    }
397
398
    if (this.isWaiting(res)) {
399
      this.preload(res);
400
    }
401
  }
402
}
403
404
/**
405
 * Preload a resource by calling external preloader
406
 * @param {string} res resource url
407
 */
408
Preload.prototype.preloadExternal = function(res) {
409
  this.setState(res, Preload.state.PRELOADING);
410
  $.ajax("http://127.0.0.1:8089/pf?res=" + res).done(function(j) {
411
    switch (j.state) {
412
      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...
413
      case Preload.state.NO_CONTENT:
414
        screen.cache.setState(res, j.state);
415
        screen.newContentTrigger();
416
        break;
417
      case Preload.state.HTTP_FAIL:
418
        screen.cache.setState(res, j.state);
419
        break;
420
      case Preload.state.ERR:
421
        screen.cache.setState(res, Preload.state.HTTP_FAIL);
422
        break;
423
      default:
424
        return;
425
    }
426
    screen.cache.preloadNext();
427
  }).fail(function() {
428
    screen.cache.preload = screen.cache.preloadAjax;
429
    screen.cache.preload(res);
430
  });
431
}
432
433
/**
434
 * Preload a resource by addinh a <link rel="prefetch"> tag
435
 * @param {string} res resource url
436
 */
437
Preload.prototype.preloadPrefetch = function(res) {
438
  this.setState(res, Preload.state.PRELOADING);
439
  $('body').append(
440
    $('<link>', {
441
      rel: 'prefetch',
442
      href: res
443
    }).load(function() {
444
      screen.cache.setState(res, Preload.state.OK);
445
      screen.newContentTrigger();
446
      screen.cache.preloadNext();
447
    }).error(function() {
448
      screen.cache.setState(res, Preload.state.HTTP_FAIL);
449
      screen.cache.preloadNext();
450
    })
451
  );
452
}
453
454
/**
455
 * Preload a resource by ajax get on the url
456
 * Check HTTP return state to validate proper cache
457
 * @param {string} res resource url
458
 */
459
Preload.prototype.preloadAjax = function(res) {
460
  this.setState(res, Preload.state.PRELOADING);
461
  $.ajax(res).done(function(data) {
462
    // Preload success
463
    if (data === '') {
464
      screen.cache.setState(res, Preload.state.NO_CONTENT);
465
    } else {
466
      screen.cache.setState(res, Preload.state.OK);
467
    }
468
    screen.newContentTrigger();
469
  }).fail(function() {
470
    // Preload failure
471
    screen.cache.setState(res, Preload.state.HTTP_FAIL);
472
  }).always(function() {
473
    screen.cache.preloadNext();
474
  });
475
}
476
477
/**
478
 * Try to preload next resource or trigger preload end event
479
 */
480
Preload.prototype.preloadNext = function() {
481
  var res = screen.cache.next();
482
  if (res) {
483
    // Preload ended, next resource
484
    screen.cache.preload(res);
485
    return;
486
  }
487
  // We've gone through all queued resources
488
  // Check if we should reload early
489
  if (screen.reloadOnTimeout()) {
490
    return;
491
  }
492
  // Trigger another update to calculate a proper screen.endAt value
493
  screen.checkUpdates();
494
}
495
496
/**
497
 * Get next resource to preload from queue
498
 * @return {string|null} next resource url
499
 */
500
Preload.prototype.next = function() {
501
  for (var res in this.cache) {
502
    if (!this.cache.hasOwnProperty(res)) {
503
      continue;
504
    }
505
506
    if (this.isInPreloadQueue(res)) {
507
      return res;
508
    }
509
  }
510
  return null;
511
}
512
513
514
/**
515
 * Field class constructor
516
 * @param {jQuery.Object} $f field object
517
 */
518
function Field($f) {
519
  this.$field = $f;
520
  this.id = $f.attr('data-id');
521
  this.url = $f.attr('data-url');
522
  this.types = $f.attr('data-types').split(' ');
523
  this.canUpdate = this.url != null;
524
  this.contents = [];
525
  this.previous = null;
526
  this.current = null;
527
  this.next = null;
528
  this.timeout = null;
529
  this.endAt = null;
530
  this.stuck = false;
531
}
532
533
/**
534
 * Retrieves contents from backend for this field
535
 */
536
Field.prototype.fetchContents = function() {
537
  if (!this.canUpdate) {
538
    return;
539
  }
540
541
  var f = this;
542
  $.get(this.url, function(j) {
543
    if (j.success) {
544
      f.contents = j.next.map(function(c) {
545
        return new Content(c);
546
      });
547
      f.pickNextIfNecessary();
548
    } else {
549
      f.setError(j.message || 'Error');
550
    }
551
  });
552
}
553
554
/**
555
 * Display error in field text
556
 */
557
Field.prototype.setError = function(err) {
558
  this.display(err);
559
}
560
561
/**
562
 * Randomize order
563
 */
564
Field.prototype.randomizeSortContents = function() {
565
  this.contents = this.contents.sort(function() {
566
    return Math.random() - 0.5;
567
  });
568
}
569
570
/**
571
 * PickNext if no content currently displayed and content is available
572
 */
573
Field.prototype.pickNextIfNecessary = function() {
574
  if (!this.timeout) {
575
    this.pickNext();
576
  }
577
}
578
579
/**
580
 * Loop through field contents to pick next displayable content
581
 */
582
Field.prototype.pickNext = function() {
583
  if (screen.reloadOnTimeout()) {
584
    // Currently trying to reload, we're past threshold: reload now
585
    return;
586
  }
587
588
  // Keep track of true previous content
589
  if (this.current != null) {
590
    this.previous = this.current;
591
  }
592
  this.current = null;
593
  var previousData = this.previous && this.previous.data;
594
595
  this.next = this.pickRandomContent(previousData) || this.pickRandomContent(previousData, true);
596
597
  if (this.next) {
598
    // Overwrite field with newly picked content
599
    this.displayNext();
600
    this.stuck = false;
601
  } else {
602
    // I am stuck, don't know what to display
603
    this.stuck = true;
604
    // Check other fields for stuckiness state
605
    if (screen.isAllFieldsStuck() && !screen.cache.hasPreloadingContent(true)) {
606
      // Nothing to do. Give up, reload now
607
      screen.reloadNow();
608
    }
609
  }
610
}
611
612
/**
613
 * Loop through field contents for any displayable content
614
 * @param  {string}  previousData previous content data
615
 * @param  {boolean} anyUsable    ignore constraints
616
 * @return {Content}              random usable content
617
 */
618
Field.prototype.pickRandomContent = function(previousData, anyUsable) {
619
  this.randomizeSortContents();
620
  for (var i = 0; i < this.contents.length; i++) {
621
    var c = this.contents[i];
622
    // Skip too long, not preloaded or empty content
623
    if (!c.canDisplay()) {
624
      continue;
625
    }
626
627
    if (anyUsable) {
628
      // Ignore repeat & same content constraints if necessary
629
      return c;
630
    }
631
632
    // Avoid repeat same content
633
    if (c.data == previousData) {
634
      // Not enough content, display anyway
635
      if (this.contents.length < 2) {
636
        return c;
637
      }
638
      continue;
639
    }
640
641
    // Avoid same content than already displayed on other fields
642
    if (screen.displaysData(c.data)) {
643
      // Not enough content, display anyway
644
      if (this.contents.length < 3) {
645
        return c;
646
      }
647
      continue;
648
    }
649
650
    // Nice content. Display it.
651
    return c;
652
  }
653
  return null;
654
}
655
656
/**
657
 * Setup next content for field and display it
658
 */
659
Field.prototype.displayNext = function() {
660
  var f = this;
661
  if (this.next && this.next.duration > 0) {
662
    this.current = this.next
663
    this.next = null;
664
    this.display(this.current.data);
665
    if (this.timeout) {
666
      clearTimeout(this.timeout);
667
    }
668
    this.endAt = Date.now() + this.current.duration;
669
    this.timeout = setTimeout(function() {
670
      f.pickNext();
671
    }, this.current.duration);
672
  }
673
}
674
675
/**
676
 * Display data in field HTML
677
 * @param {string} data
678
 */
679
Field.prototype.display = function(data) {
680
  this.$field.html(data);
681
  this.$field.show();
682
  var $bt = this.$field.find('.bigtext');
683
  // Only first data-min/max per field is respected
684
  var minPx = $bt.attr('data-min-px') || 4;
685
  var maxPx = $bt.attr('data-max-px') || 0;
686
  $bt.parent().textfill({
687
    minFontPixels: minPx,
688
    maxFontPixels: maxPx,
689
  });
690
}
691
692
// Global screen instance
693
var screen = null;
694
695
/**
696
 * jQuery.load event
697
 * Initialize Screen and Fields
698
 * Setup updates interval timeouts
699
 */
700
function onLoad() {
701
  screen = new Screen(updateScreenUrl);
702
  // Init
703
  $('.field').each(function() {
704
    var f = new Field($(this));
705
    screen.fields.push(f);
706
    f.fetchContents();
707
  });
708
709
  if (screen.url) {
710
    // Setup content updates loop
711
    setInterval(function() {
712
      for (var f in screen.fields) {
713
        if (screen.fields.hasOwnProperty(f)) {
714
          screen.fields[f].fetchContents();
715
        }
716
      }
717
      screen.checkUpdates();
718
    }, 60000); // 1 minute is enough alongside preload queue end trigger
719
    screen.checkUpdates();
720
  }
721
}
722
723
// Run
724
$(onLoad);
725