Completed
Push — master ( 618bf0...ad9644 )
by Jeff
04:46
created

Content.shouldPreload   A

Complexity

Conditions 1
Paths 3

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 3
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.stopping = false;
14
  this.cache = {};
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
        s.reload();
28
        s.nextUrl = null;
29
        return;
30
      }
31
32
      if (j.data.duration > 0) {
33
        // Setup next screen
34
        s.reload(j.data.duration * 1000);
35
        s.nextUrl = j.data.nextScreenUrl;
36
      }
37
    }
38
  });
39
}
40
41
/**
42
 * Start Screen reload procedure, checking for every field timeout
43
 */
44
Screen.prototype.reload = function(minDuration) {
45
  var endAt = Date.now() + (minDuration ? minDuration : 0);
46
  if (this.stopping && this.endAt < endAt) {
47
    return;
48
  }
49
50
  this.endAt = minDuration ? Date.now() + minDuration : 0;
51
  this.stopping = true;
52
  for (var i in this.fields) {
53
    if (!this.fields.hasOwnProperty(i)) {
54
      continue;
55
    }
56
    var f = this.fields[i];
57
    if (f.timeout && f.endAt > this.endAt) {
58
      this.endAt = f.endAt;
59
    }
60
  }
61
62
  if (this.endAt === 0) {
63
    this.doReload();
64
  }
65
}
66
67
/**
68
 * Actual Screen reload action
69
 */
70
Screen.prototype.doReload = function() {
71
  if (this.nextUrl) {
72
    window.location = this.nextUrl;
73
  } else {
74
    window.location.reload();
75
  }
76
}
77
78
/**
79
 * Check every field for content
80
 * @param  {Content} data 
81
 * @return {boolean} content is displayed
82
 */
83
Screen.prototype.displaysData = function(data) {
84
  return this.fields.filter(function(field) {
85
    return field.current && field.current.data == data;
86
  }).length > 0;
87
}
88
89
/**
90
 * Content class constructor
91
 * @param {array} c content attributes
92
 */
93
function Content(c) {
94
  this.id = c.id;
95
  this.data = c.data;
96
  this.duration = c.duration * 1000;
97
  this.type = c.type;
98
  this.displayCount = 0;
99
  this.src = null;
100
101
  if (this.shouldPreload()) {
102
    this.preload();
103
  }
104
}
105
106
Content.prototype.shouldPreload = function() {
107
  return this.canPreload() && !this.isPreloading() && !this.isPreloaded();
108
}
109
110
Content.prototype.canPreload = function() {
111
  return this.getResource() && this.type.search(/Video|Image|Agenda/) != -1;
112
}
113
114
Content.prototype.getResource = function() {
115
  if (this.src) {
116
    return this.src;
117
  }
118
  var srcMatch = this.data.match(/src="([^"]+)"/);
119
  if (!srcMatch) {
120
    return false;
121
  }
122
  var src = srcMatch[1];
123
  if (src.indexOf('/') === 0) {
124
    src = window.location.origin + src;
125
  }
126
  if (src.indexOf('http') !== 0) {
127
    return false;
128
  }
129
130
  this.src = src;
131
  return src;
132
}
133
134
Content.prototype.isPreloaded = function(expires) {
135
  if (!this.canPreload()) {
136
    return true;
137
  }
138
139
  if (expires === undefined) {
140
    var cache = screen.cache[this.getResource()]
141
    switch (cache) {
142
      case undefined: // unset
143
      case false: // preloading
144
        return false;
145
      case true: // preloaded without expire
146
        return true;
147
      default: // check expire
148
        return (new Date()).valueOf() < cache;
149
    }
150
  } else if (expires === null) {
151
    console.log(this.getResource() + ' has no Expires header');
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
152
    screen.cache[this.getResource()] = true;
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...
153
  } else if (expires) {
154
    var exp = new Date(expires).valueOf();
155
    var diff = exp - (new Date()).valueOf();
156
    if (diff < 10000) {
157
      console.log(this.getResource() + ' should\'t have Expires header, too short: ' + diff / 1000 + ' sec');
158
      screen.cache[this.getResource()] = true;
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...
159
    } else {
160
      console.log(this.getResource() + ' cached for: ' + diff / 1000 + ' sec');
161
      screen.cache[this.getResource()] = exp + 5000;
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...
162
    }
163
  } else {
164
    console.log(this.getResource() + ' has been discarded');
165
    delete screen.cache[this.getResource()];
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...
166
  }
167
}
168
169
Content.prototype.isPreloading = function(state) {
170
  if (state === undefined) {
171
    return screen.cache[this.getResource()] === false;
172
  } else if (state && !this.isPreloading()) {
173
    screen.cache[this.getResource] = false;
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...
174
  } else if (this.isPreloading()) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if this.isPreloading() is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
175
    delete screen.cache[this.getResource()];
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...
176
  }
177
}
178
179
Content.prototype.preload = function() {
180
  var src = this.getResource();
181
  if (!src) {
182
    this.isPreloaded(true);
183
    return;
184
  }
185
  this.isPreloading(true);
186
187
  console.log('Preloading ' + src);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
188
  var c = this;
189
  $.ajax({
190
    method: 'GET',
191
    url: src,
192
  }).done(function(data, textStatus, jqXHR) {
193
    c.isPreloaded(jqXHR.getResponseHeader('Expires'));
194
  }).fail(function(jqXHR, textStatus, errorThrown) {
0 ignored issues
show
Unused Code introduced by
The parameter jqXHR is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter textStatus is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter errorThrown is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
195
    c.isPreloaded(false); // Discard until next Content init
196
  });
197
}
198
199
/**
200
 * Field class constructor
201
 * @param {jQuery.Object} $f field object
202
 * @param {Screen} screen parent screen object
0 ignored issues
show
Documentation introduced by
The parameter screen does not exist. Did you maybe forget to remove this comment?
Loading history...
203
 */
204
function Field($f) {
205
  this.$field = $f;
206
  this.id = $f.attr('data-id');
207
  this.url = $f.attr('data-url');
208
  this.types = $f.attr('data-types').split(' ');
209
  this.canUpdate = this.url != null;
210
  this.contents = [];
211
  this.previous = null;
212
  this.current = null;
213
  this.next = null;
214
  this.timeout = null;
215
  this.endAt = null;
216
}
217
218
/**
219
 * Retrieves contents from backend for this field
220
 */
221
Field.prototype.getContents = function() {
222
  if (!this.canUpdate) {
223
    return;
224
  }
225
226
  var f = this;
227
  $.get(this.url, function(j) {
228
    if (j.success) {
229
      f.contents = j.next.map(function(c) {
230
        return new Content(c);
231
      });
232
      if (!f.timeout && f.contents.length) {
233
        f.pickNext();
234
      }
235
    } else {
236
      f.setError(j.message || 'Error');
237
    }
238
  });
239
}
240
241
/**
242
 * Display error in field text
243
 */
244
Field.prototype.setError = function(err) {
245
  this.$field.text(err);
246
}
247
248
/**
249
 * Sort by displayCount and randomize order when equal displayCount
250
 */
251
Field.prototype.randomizeSortContents = function() {
252
  this.contents = this.contents.sort(function(a, b) {
253
    if (a.displayCount === b.displayCount) {
254
      return Math.random() - 0.5;
255
    }
256
    return a.displayCount - b.displayCount;
257
  });
258
}
259
260
/**
261
 * Loop through field contents to pick next displayable content
262
 */
263
Field.prototype.pickNext = function() {
264
  if (screen.stopping) { // Stoping screen
265
    if (screen.endAt < Date.now()) {
266
      screen.doReload();
267
      return;
268
    }
269
  }
270
271
  this.previous = this.current;
272
  this.current = null;
273
  var pData = this.previous && this.previous.data;
274
  // Avoid repeat & other field same content
275
  this.randomizeSortContents();
276
  for (var i = 0; i < this.contents.length; i++) {
277
    var c = this.contents[i];
278
    // Skip too long content
279
    if (screen.endAt != null && c.duration + Date.now() > screen.endAt) {
280
      continue;
281
    }
282
283
    if (c.data == pData) {
284
      // Will repeat, avoid if enough content
285
      if (this.contents.length < 2) {
286
        this.next = c;
287
        break;
288
      }
289
      continue;
290
    }
291
292
    if (screen.displaysData(c.data)) {
293
      // Same content already displayed on other field, avoid if enough content
294
      if (this.contents.length < 3) {
295
        this.next = c;
296
        break;
297
      }
298
      continue;
299
    }
300
301
    // Wait for resource preload
302
    if (!c.isPreloaded()) {
303
      continue;
304
    }
305
306
    this.next = c;
307
  }
308
309
  this.display();
310
}
311
312
/**
313
 * Display next content in field html
314
 */
315
Field.prototype.display = function() {
316
  if (this.next && this.next.duration > 0) {
317
    this.current = this.next
318
    this.current.displayCount++;
319
    this.next = null;
320
    this.$field.html(this.current.data);
321
    this.$field.show();
322
    if (this.$field.text() != '') {
323
      this.$field.textfill({
324
        maxFontPixels: 0,
325
      });
326
    }
327
    if (this.timeout) {
328
      clearTimeout(this.timeout);
329
    }
330
    var f = this;
331
    this.timeout = setTimeout(function() {
332
      f.pickNext();
333
    }, this.current.duration);
334
    this.endAt = this.current.duration + Date.now()
335
  } else {
336
    var f = this;
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable f already seems to be declared on line 330. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
337
    this.timeout = setTimeout(function() {
338
      f.pickNext();
339
    }, 2000);
340
    console.error('No content to display for', this);
341
  }
342
}
343
344
/**
345
 * jQuery.load event
346
 * Initialize Screen and Fields
347
 * Setup updates interval timeouts
348
 */
349
var screen = null;
350
351
function onLoad() {
352
  screen = new Screen(updateScreenUrl);
353
  // Init
354
  $('.field').each(function() {
355
    var f = new Field($(this));
356
    f.getContents();
357
    screen.fields.push(f);
358
  });
359
360
  // Setup content updates loop
361
  setInterval(function() {
362
    for (var f in screen.fields) {
363
      if (screen.fields.hasOwnProperty(f)) {
364
        screen.fields[f].getContents();
365
      }
366
    }
367
    screen.checkUpdates();
368
  }, 60000);
369
  screen.checkUpdates();
370
}
371
372
// Run
373
$(onLoad);
374