Completed
Push — master ( d2f8b5...5961b2 )
by Jeff
02:57
created

Field.fetchContents   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
102
    for (var c in this.fields[f].contents) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
103
      if (this.fields[f].contents[c].isPreloading()) {
104
        return true;
105
      }
106
    }
107
  }
108
109
  return false;
110
}
111
112
/**
113
 * Pick next content to preload from cache queue
114
 */
115
Screen.prototype.nextPreloadQueue = function() {
116
  for (var f in this.fields) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
117
    for (var c in this.fields[f].contents) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

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