Completed
Push — master ( 5961b2...50381b )
by Jeff
02:58
created

Screen.hasPreloadingContent   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
nc 4
nop 0
dl 0
loc 17
rs 8.8571
c 3
b 0
f 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 = {};
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
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
      screen.reloadIn(0);
39
    }
40
  });
41
}
42
43
/**
44
 * Start Screen reload procedure, checking for every field timeout
45
 */
46
Screen.prototype.reloadIn = function(minDuration) {
47
  var endAt = Date.now() + minDuration;
48
  if (this.endAt != null && this.endAt < endAt) {
49
    return;
50
  }
51
52
  if (this.hasPreloadingContent()) {
53
    // Do not break preloading
54
    return;
55
  }
56
57
  this.endAt = Date.now() + minDuration;
58
  for (var i in this.fields) {
59
    if (!this.fields.hasOwnProperty(i)) {
60
      continue;
61
    }
62
    var f = this.fields[i];
63
    if (f.timeout && f.endAt > this.endAt) {
64
      // Always wait for content display end
65
      this.endAt = f.endAt;
66
    }
67
  }
68
69
  if (Date.now() >= this.endAt) {
70
    this.reloadNow();
71
  }
72
}
73
74
/**
75
 * Actual Screen reload action
76
 */
77
Screen.prototype.reloadNow = function() {
78
  if (this.nextUrl) {
79
    window.location = this.nextUrl;
80
  } else {
81
    window.location.reload();
82
  }
83
}
84
85
/**
86
 * Check every field for content
87
 * @param  {Content} data 
88
 * @return {boolean} content is displayed
89
 */
90
Screen.prototype.displaysData = function(data) {
91
  return this.fields.filter(function(field) {
92
    return field.current && field.current.data == data;
93
  }).length > 0;
94
}
95
96
/**
97
 * Search in cache for current preloading content
98
 * @return {Boolean} has preloading content
99
 */
100
Screen.prototype.hasPreloadingContent = function() {
101
  for (var f in this.fields) {
102
    if (!this.fields.hasOwnProperty(f)) {
103
      continue
104
    }
105
    for (var c in this.fields[f].contents) {
106
      if (!this.fields[f].contents.hasOwnProperty(c)) {
107
        continue
108
      }
109
      if (this.fields[f].contents[c].isPreloading()) {
110
        return true;
111
      }
112
    }
113
  }
114
115
  return false;
116
}
117
118
/**
119
 * Pick next content to preload from cache queue
120
 */
121
Screen.prototype.nextPreloadQueue = function() {
122
  for (var f in this.fields) {
123
    if (!this.fields.hasOwnProperty(f)) {
124
      continue
125
    }
126
    for (var c in this.fields[f].contents) {
127
      if (!this.fields[f].contents.hasOwnProperty(c)) {
128
        continue
129
      }
130
      if (this.fields[f].contents[c].isInPreloadQueue()) {
131
        this.fields[f].contents[c].preload();
132
      }
133
    }
134
  }
135
}
136
137
138
/**
139
 * Content class constructor
140
 * @param {array} c content attributes
141
 */
142
function Content(c) {
143
  this.id = c.id;
144
  this.data = c.data;
145
  this.duration = c.duration * 1000;
146
  this.type = c.type;
147
  this.src = null;
148
149
  if (this.shouldPreload()) {
150
    this.queuePreload();
151
  }
152
}
153
154
/**
155
 * Check if content should be ajax preloaded
156
 * @return {boolean}
157
 */
158
Content.prototype.shouldPreload = function() {
159
  return this.canPreload() && !this.isPreloading() && !this.isPreloaded();
160
}
161
162
/**
163
 * Check if content has pre-loadable material
164
 * @return {boolean} 
165
 */
166
Content.prototype.canPreload = function() {
167
  return this.getResource() && this.type.search(/Video|Image|Agenda/) != -1;
168
}
169
170
/**
171
 * Check if content is displayable (preloaded and not too long)
172
 * @return {Boolean} can display
173
 */
174
Content.prototype.canDisplay = function() {
175
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded();
176
}
177
178
/**
179
 * Extract url from contant data
180
 * @return {string} resource url
181
 */
182
Content.prototype.getResource = function() {
183
  if (this.src) {
184
    return this.src;
185
  }
186
  var srcMatch = this.data.match(/src="([^"]+)"/);
187
  if (!srcMatch) {
188
    return false;
189
  }
190
  var src = srcMatch[1];
191
  if (src.indexOf('/') === 0) {
192
    src = window.location.origin + src;
193
  }
194
  if (src.indexOf('http') !== 0) {
195
    return false;
196
  }
197
  src = src.replace(/#.*/g, '');
198
199
  this.src = src;
200
  return src;
201
}
202
203
/**
204
 * Set content cache status
205
 * @param {string} expires header
206
 */
207
Content.prototype.setPreloadState = function(expires) {
208
  if (expires === null || expires == '') {
209
    expires = Preload.state.NO_EXPIRE_HEADER;
210
  }
211
212
  screen.cache[this.getResource()] = expires < -1 ? expires : Preload.state.OK
213
}
214
215
/**
216
 * Check cache for preload status of content
217
 * @return {Boolean} 
218
 */
219
Content.prototype.isPreloaded = function() {
220
  if (!this.canPreload()) {
221
    return true;
222
  }
223
224
  var state = screen.cache[this.getResource()];
225
226
  return state === Preload.state.OK || state === Preload.state.NO_EXPIRE_HEADER;
227
}
228
229
/**
230
 * Check cache for in progress preloading
231
 * @return {Boolean} is preloading
232
 */
233
Content.prototype.isPreloading = function() {
234
  return screen.cache[this.getResource()] === Preload.state.PRELOADING;
235
}
236
237
/**
238
 * Check cache for queued preloading
239
 * @return {Boolean} is in preload queue
240
 */
241
Content.prototype.isInPreloadQueue = function() {
242
  return screen.cache[this.getResource()] === Preload.state.PRELOADING_QUEUE;
243
}
244
245
/**
246
 * Ajax call to preload content
247
 */
248
Content.prototype.preload = function() {
249
  var src = this.getResource();
250
  if (!src) {
251
    return;
252
  }
253
254
  this.setPreloadState(Preload.state.PRELOADING);
255
256
  var c = this;
257
  $.ajax(src).done(function(data, textStatus, jqXHR) {
258
    c.setPreloadState(jqXHR.getResponseHeader('Expires'));
259
  }).fail(function() {
260
    c.setPreloadState(Preload.state.HTTP_FAIL);
261
  }).always(function() {
262
    screen.nextPreloadQueue();
263
  });
264
}
265
266
/**
267
 * Preload content or add to preload queue
268
 */
269
Content.prototype.queuePreload = function() {
270
  var src = this.getResource();
271
  if (!src) {
272
    return;
273
  }
274
275
  if (screen.hasPreloadingContent()) {
276
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
277
  } else {
278
    this.preload();
279
  }
280
}
281
282
283
/**
284
 * Preload class constructor
285
 * Mostly used to store constants
286
 */
287
function Preload() {}
288
289
/**
290
 * Preload states
291
 */
292
Preload.state = {
293
  PRELOADING: -2,
294
  PRELOADING_QUEUE: -3,
295
  HTTP_FAIL: -4,
296
  NO_EXPIRE_HEADER: -5,
297
  OK: -6,
298
}
299
300
301
/**
302
 * Field class constructor
303
 * @param {jQuery.Object} $f field object
304
 */
305
function Field($f) {
306
  this.$field = $f;
307
  this.id = $f.attr('data-id');
308
  this.url = $f.attr('data-url');
309
  this.types = $f.attr('data-types').split(' ');
310
  this.canUpdate = this.url != null;
311
  this.contents = [];
312
  this.previous = null;
313
  this.current = null;
314
  this.next = null;
315
  this.timeout = null;
316
  this.endAt = null;
317
}
318
319
/**
320
 * Retrieves contents from backend for this field
321
 */
322
Field.prototype.fetchContents = function() {
323
  if (!this.canUpdate) {
324
    return;
325
  }
326
327
  var f = this;
328
  $.get(this.url, function(j) {
329
    if (j.success) {
330
      f.contents = j.next.map(function(c) {
331
        return new Content(c);
332
      });
333
      if (!f.timeout && f.contents.length) {
334
        f.pickNext();
335
      }
336
    } else {
337
      f.setError(j.message || 'Error');
338
    }
339
  });
340
}
341
342
/**
343
 * Display error in field text
344
 */
345
Field.prototype.setError = function(err) {
346
  this.display(err);
347
}
348
349
/**
350
 * Randomize order
351
 */
352
Field.prototype.randomizeSortContents = function() {
353
  this.contents = this.contents.sort(function() {
354
    return Math.random() - 0.5;
355
  });
356
}
357
358
/**
359
 * Loop through field contents to pick next displayable content
360
 */
361
Field.prototype.pickNext = function() {
362
  if (screen.endAt != null && Date.now() >= screen.endAt) { // Stoping screen
363
    screen.reloadNow();
364
    return;
365
  }
366
367
  var f = this;
368
  this.previous = this.current;
369
  this.current = null;
370
  var pData = this.previous && this.previous.data;
371
  // Avoid repeat & other field same content
372
  this.randomizeSortContents();
373
  for (var i = 0; i < this.contents.length; i++) {
374
    var c = this.contents[i];
375
    // Skip too long or not preloaded content 
376
    if (!c.canDisplay()) {
377
      continue;
378
    }
379
380
    if (c.data == pData) {
381
      // Will repeat, avoid if enough content
382
      if (this.contents.length < 2) {
383
        this.next = c;
384
        break;
385
      }
386
      continue;
387
    }
388
389
    if (screen.displaysData(c.data)) {
390
      // Same content already displayed on other field, avoid if enough content
391
      if (this.contents.length < 3) {
392
        this.next = c;
393
        break;
394
      }
395
      continue;
396
    }
397
398
    this.next = c;
399
    break
400
  }
401
402
  if (this.next) {
403
    this.displayNext();
404
  } else {
405
    setTimeout(function() {
406
      f.pickNext();
407
    }, 200);
408
  }
409
}
410
411
/**
412
 * Setup next content for field and display it
413
 */
414
Field.prototype.displayNext = function() {
415
  var f = this;
416
  if (this.next && this.next.duration > 0) {
417
    this.current = this.next
418
    this.next = null;
419
    this.display(this.current.data);
420
    if (this.timeout) {
421
      clearTimeout(this.timeout);
422
    }
423
    this.endAt = Date.now() + this.current.duration;
424
    this.timeout = setTimeout(function() {
425
      f.pickNext();
426
    }, this.current.duration);
427
  }
428
}
429
430
/**
431
 * Display data in field HTML
432
 * @param  {string} data 
433
 */
434
Field.prototype.display = function(data) {
435
  this.$field.html(data);
436
  this.$field.show();
437
  if (this.$field.text() != '') {
438
    this.$field.textfill({
439
      maxFontPixels: 0,
440
    });
441
  }
442
}
443
444
// Global screen instance
445
var screen = null;
446
447
/**
448
 * jQuery.load event
449
 * Initialize Screen and Fields
450
 * Setup updates interval timeouts
451
 */
452
function onLoad() {
453
  screen = new Screen(updateScreenUrl);
454
  // Init
455
  $('.field').each(function() {
456
    var f = new Field($(this));
457
    f.fetchContents();
458
    screen.fields.push(f);
459
  });
460
461
  if (screen.url) {
462
    // Setup content updates loop
463
    setInterval(function() {
464
      for (var f in screen.fields) {
465
        if (screen.fields.hasOwnProperty(f)) {
466
          screen.fields[f].fetchContents();
467
        }
468
      }
469
      screen.checkUpdates();
470
    }, 30000);
471
    screen.checkUpdates();
472
  }
473
}
474
475
// Run
476
$(onLoad);
477