Completed
Push — master ( 6586ad...db31af )
by Jeff
03:22
created

Field.pickNextIfNecessary   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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