Completed
Push — master ( d8b05f...8a2992 )
by Jeff
02:59
created

Field.pickRandomContent   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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