diff --git a/js/helper/AJAX.js b/js/helper/AJAX.js
new file mode 100644
index 0000000000000000000000000000000000000000..80c62b44e8231fe12766cbfc27b9f51c39128c56
--- /dev/null
+++ b/js/helper/AJAX.js
@@ -0,0 +1,138 @@
+AJAX = new (function AJAX() {
+		
+	function newXHR() {
+		return !!self.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
+	}
+
+    function serialize(obj, prefix) {
+        var str = [];
+        for (var p in obj) {
+          if (obj.hasOwnProperty(p)) {
+            var k = prefix ? prefix + "[" + p + "]" : p,
+                  v = obj[p];
+              str.push(typeof v == "object" ? serialize(v, k) : encodeURIComponent(k) + "=" + encodeURIComponent(v));
+          }
+        }
+        return str.join("&");
+    }
+	
+	function decodeResponse(type, response) {
+		try {			
+			switch (type) {
+				case "application/json":
+					return JSON.parse(response);
+				default:
+					return response;
+			}
+		} catch(e) {
+			
+		}
+		return response;
+	}
+	
+	function doReject(strict, result){
+		return !!strict ? Promise.reject(result) : Promise.resolve(result);
+	}
+
+    function doRequest(url, method, params, strict) {
+        return new Promise(function(resolve, reject) {
+			try {
+				if (typeof url != 'string') {
+					return doReject(strict, {
+						success: false,
+						response: 'url parameter must be string',
+						status: 0
+					}).then(resolve, reject);
+				}
+				if (typeof method != 'string') {
+					return doReject(strict, {
+						success: false,
+						response: 'method parameter must be string',
+						status: 0
+					}).then(resolve, reject);
+				}
+				method = method.toUpperCase();
+				var xhr = newXHR();
+				xhr.onload = function(e) {
+					if (e.target.status == 200) {
+						var response = decodeResponse(e.target.getResponseHeader("Content-Type"), e.target.response);
+						resolve({
+							success: true,
+							response: response,
+							status: e.target.status
+						});	
+					} else {
+						var statusText = e.target.statusText;
+						if (!statusText.length) statusText = "unexpected error";
+						doReject(strict, {
+							success: false,
+							response: statusText, 
+							status: e.target.status
+						}).then(resolve, reject);
+					}
+				}
+				xhr.onerror = function(e) {
+					var statusText = e.target.statusText;
+					if (!statusText.length) statusText = "unexpected error";
+					doReject(strict, {
+						success: false,
+						response: statusText,
+						status: e.target.status
+					}).then(resolve, reject);
+				}
+				xhr.onabort = function(e) {
+					doReject(strict, {
+						success: false,
+						response: 'download aborted',
+						status: 0
+					}).then(resolve, reject);
+				}
+				params = typeof params == 'object' ? serialize(params) : '';
+				if (method === 'GET') {
+					if (!!params.length) params = "?" + params;
+					xhr.open(method, url+params);
+					xhr.send();
+				} else {
+					xhr.open(method, url);
+					xhr.send(!!params.length ? params : undefined);
+				}
+			} catch(e) {
+					doReject(strict, {
+						success: false,
+						response: e.message,
+						status: 0
+					}).then(resolve, reject);
+			}
+        });
+    }
+	
+	this.request = function(requests, strict) {
+		if (typeof strict != 'boolean') strict = true;
+		try {
+			if (typeof requests == 'string') {
+				return doRequest(requests, 'GET', [], strict);
+			} else {
+				if (typeof requests == 'object') {
+					if (Array.isArray(requests)) {
+						var proms = [];
+						for (var i = 0; i < requests.length; ++i)
+							proms.push(this.request(requests[i], strict));
+						return Promise.all(proms);
+					} else {
+						return doRequest(requests.url, requests.method, requests.params, strict);
+					}
+				}
+				throw new Error('wrong type (string/array/dict expected, '+(typeof requests)+' detected)');
+			}
+		} catch(e) {
+			return doReject(strict, {
+				success: false,
+				response: e.message,
+				status: 0
+			});
+		}
+	}
+
+	return Object.freeze(this);
+	
+});
\ No newline at end of file
diff --git a/js/helper/AbstractWorker.js b/js/helper/AbstractWorker.js
new file mode 100644
index 0000000000000000000000000000000000000000..32db1f5dbbfa671560f8ea70e114839786ea21e4
--- /dev/null
+++ b/js/helper/AbstractWorker.js
@@ -0,0 +1,75 @@
+class AbstractWorker {
+	
+	constructor() {
+		this.config = {};
+		
+		var
+			ival = 1000,
+			timer,
+			cmds = [],
+			execRun = false;
+		
+		var update = function() {
+			if (!!timer) self.clearTimeout(timer);
+			this.tick(function(result) {
+				self.postMessage(result);
+			});
+			if (ival > 0) timer = self.setTimeout(update, ival);
+		}.bind(this);
+	
+		var exec = function() {
+			if (!!cmds.length) {
+				execRun = true;
+				var cmd = cmds.shift();
+				if (typeof this[cmd.cmd] == "function") {
+					this[cmd.cmd].apply(this,cmd.params);
+				} else {
+					exec();
+				}
+			} else {
+				execRun = false;
+			}
+		}.bind(this);
+	
+		this.start = function() {
+			update();
+			exec();
+		}
+		
+		this.stop = function() {
+			if (!!timer) self.clearTimeout(timer);
+			exec();
+		}
+		
+		this.loadConfig = function(name) {
+			AJAX.request("../../panels/departure/config/"+name+".json").then(function(data) {
+				this.config = this.processConfig(data.response);
+				exec();
+			}.bind(this),function() {
+				exec();
+			});
+		}
+	
+		self.onmessage = function(e) {
+			cmds.push(e.data);
+			if (!execRun) exec();
+		}.bind(this);
+		
+		this.setSpeed = function(val) {
+			if (isNaN(val = parseInt(val))) {
+				val = 0;
+			}
+			ival = val;
+		}
+		
+	}
+	
+	tick(callback) {
+		callback({});
+	}
+	
+	processConfig(config) {
+		return config;
+	}
+		
+}
\ No newline at end of file
diff --git a/js/helper/check.js b/js/helper/check.js
new file mode 100644
index 0000000000000000000000000000000000000000..cce4070e500cbc46592efb7f582f13e307b8e615
--- /dev/null
+++ b/js/helper/check.js
@@ -0,0 +1,64 @@
+function isEmpty(value) {
+	switch (typeof value) {
+		case 'undefined':
+			return true;
+			break;
+		case 'object':
+			if (Array.isArray(value)) return value.length <= 0;
+			return value === null || Object.getOwnPropertyNames(value).length <= 0;
+			break;
+		case 'string':
+			return value == "";
+			break;
+		case 'number':
+			return isNaN(value);
+			break;
+		default:
+			return false;
+			break;
+	}
+}
+
+function isFunction(value) {
+	return typeof(value) == 'function';
+}
+
+function isObject(value) {
+	return typeof(value) == 'object' && !Array.isArray(value);
+}
+
+function isObjectNotEmpty(value) {
+	return typeof(value) == 'object' && !Array.isArray(value) && Object.getOwnPropertyNames(value).length > 0;
+}
+
+function isArray(value) {
+	return Array.isArray(value);
+}
+
+function isArrayNotEmpty(value) {
+	return Array.isArray(value) && value.length > 0;
+}
+
+function isString(value) {
+	return typeof(value) == 'string';
+}
+
+function isStringNotEmpty(value) {
+	return typeof(value) == 'string' && value != "";
+}
+
+function isNumber(value) {
+	return typeof(value) == 'number';
+}
+
+function isInteger(value) {
+	return Number.isSafeInteger(value);
+}
+
+function isBoolean(value) {
+	return typeof(value) == 'boolean';
+}
+
+function arrayContains(array, value) {
+	return isArray(array) && !!(array.indexOf(value)+1);
+}
\ No newline at end of file