Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
datacontroller.js 43.68 KiB
// $Id$

angular.module('raeApp').factory("dataService", ['$http', '$q', function($http, $q){
    var self = this;
    self.tasks = [];
    self.resources = [];
    self.resourceGroups = [];
    self.resourceClaims = [];
    self.tasktypes = [];
    self.taskstatustypes = [];
    self.editableTaskStatusIds = [];

    self.taskDict = {};
    self.resourceDict = {};
    self.resourceGroupsDict = {};
    self.resourceGroupMemberships = {};
    self.resourceClaimDict = {};
    self.resourceUsagesDict = {};
    self.tasktypesDict = {};

    self.momProjects = [];
    self.momProjectsDict = {};

    self.resourcesWithClaims = [];

    self.filteredTasks = [];
    self.filteredTaskDict = {};

    self.taskTimes = {};
    self.resourceClaimTimes = {};

    self.config = {};

    self.selected_resource_id;
    self.selected_resourceGroup_id;
    self.selected_task_ids = [];
    self.selected_project_id;
    self.selected_resourceClaim_id;

    self.initialLoadComplete = false;
    self.taskChangeCntr = 0;
    self.filteredTaskChangeCntr = 0;
    self.claimChangeCntr = 0;
    self.resourceUsagesChangeCntr = 0;

    self.loadedHours = {};

    self.viewTimeSpan = {from: new Date(), to: new Date() };
    self.autoFollowNow = true;


    self.humanreadablesize = function(num) {
        var units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z'];
        for(unit of units) {
            if(Math.abs(num) < 1000.0) {
                return num.toPrecision(4).toString() + unit;
            }
            num /= 1000.0;
        }
        return num.toPrecision(5).toString() + 'Y';
    }

    self.isTaskIdSelected = function(task_id) {
        return self.selected_task_ids.indexOf(task_id) != -1;
    }

    self.toggleTaskSelection = function(task_id) {
        if(self.isTaskIdSelected(task_id)) {
            self.removeSelectedTaskId(task_id);
        } else {
            self.addSelectedTaskId(task_id);
        }
    }

    self.addSelectedTaskId = function(task_id) {
        if(self.selected_task_ids.indexOf(task_id) == -1) {
            self.selected_task_ids.push(task_id);
        }
    }

    self.removeSelectedTaskId = function(task_id) {
        var idx = self.selected_task_ids.indexOf(task_id);
        if(idx != -1) {
            self.selected_task_ids.splice(idx, 1);
        }
    }

    self.setSelectedTaskId = function(task_id) {
        self.selected_task_ids.splice(0, self.selected_task_ids.length);
        self.selected_task_ids.push(task_id);
    }

    self.setSelectedTaskIds = function(task_ids) {
        self.selected_task_ids.splice(0, self.selected_task_ids.length);
        for(var task_id of task_ids) {
            self.selected_task_ids.push(task_id);
        }
    }

    self.selectTasksInSameGroup = function(task) {
        self.selected_task_ids.splice(0, self.selected_task_ids.length);
        var groupTasks = self.filteredTasks.filter(function(t) { return t.mom_object_group_id == task.mom_object_group_id; });
        for(var t of groupTasks) {
            self.selected_task_ids.push(t.id);
        }
    }

    self.floorDate = function(date, hourMod=1, minMod=1) {
        var min = date.getMinutes();
        min = date.getMinutes()/minMod;
        min = Math.floor(date.getMinutes()/minMod);
        min = minMod*Math.floor(date.getMinutes()/minMod);
        return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hourMod*Math.floor(date.getHours()/hourMod), minMod*Math.floor(date.getMinutes()/minMod));
    };

    self.ceilDate = function(date, hourMod=1, minMod=1) {
        return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hourMod*Math.ceil(date.getHours()/hourMod), minMod*Math.ceil(date.getMinutes()/minMod));
    };

    self.resourceClaimStatusColors = {'claimed':'#ffa64d',
                                      'conflict':'#ff0000',
                                      'allocated': '#66ff66',
                                      'mixed': '#bfbfbf'}

    self.taskStatusColors = {'prepared':'#cccccc',
                             'approved':'#8cb3d9',
                             'on_hold':'#b34700',
                             'conflict':'#ff0000',
                             'prescheduled': '#6666ff',
                             'scheduled': '#0000ff',
                             'queued': '#ccffff',
                             'active': '#ffff00',
                             'completing': '#ffdd99',
                             'finished': '#00ff00',
                             'aborted': '#cc0000',
                             'error': '#990033',
                             'obsolete': '#555555',
                             'opened': '#d9e5f2',
                             'suspended': '#996666'};


    //-- IMPORTANT REMARKS ABOUT UTC/LOCAL DATE --
    //Dates (datetimes) across javascript/angularjs/3rd party modules are a mess!
    //every module uses their own parsing and displaying
    //some care about utc/local date, others don't
    //So, to be consistent in this schedular client app, we chose the following conventions:
    //1) All received/sent timestamps are strings in iso format 'yyyy-MM-ddTHH:mm:ssZ'
    //2) All displayed timestamps should display the time in UTC, regardless of the timezone of the client.
    //All javascript/angularjs/3rd party modules display local times correct and the same...
    //So, to make 1&2 happen, we convert all received timestamps to 'local-with-utc-diff-correction', and then treat them as local.
    //And if we send them to the web server, we convert them back to real utc.
    //It's a stupid solution, but it works.
    //-- IMPORTANT REMARKS ABOUT UTC/LOCAL DATE --

    self.convertDatestringToLocalUTCDate = function(dateString) {
        //first convert the dateString to proper Date
        var date = new Date(dateString)
        //then do our trick to offset the timestamp with the utcOffset, see explanation above.
        return new Date(date.getTime() - self.utcOffset)
    };

    self.convertLocalUTCDateToISOString = function(local_utc_date) {
        if(local_utc_date) {
            //reverse trick to offset the timestamp with the utcOffset, see explanation above.
            var real_utc = new Date(local_utc_date.getTime() + self.utcOffset)
            return real_utc.toISOString();
        }
        return undefined;
    };

    //local client time offset to utc in milliseconds
    self.utcOffset = moment().utcOffset()*60000;

    self.toIdBasedDict = function(list) {
        var dict = {}
        if(list) {
            for(var i = list.length-1; i >=0; i--) {
                var item = list[i];
                dict[item.id] = item;
            }
        }
        return dict;
    };

    self.applyChanges = function(existingObj, changedObj) {
        for(var prop in changedObj) {
            if(existingObj.hasOwnProperty(prop) &&
            changedObj.hasOwnProperty(prop) &&
            existingObj[prop] != changedObj[prop]) {
                if(existingObj[prop] instanceof Date && typeof changedObj[prop] === "string") {
                    existingObj[prop] = self.convertDatestringToLocalUTCDate(changedObj[prop]);
                } else {
                    existingObj[prop] = changedObj[prop];
                }
            }
        }
    };

    self.getTasksAndClaimsForViewSpan = function() {
        var from = self.floorDate(self.viewTimeSpan.from, 1, 60);
        var until = self.ceilDate(self.viewTimeSpan.to, 1, 60);
        var lowerTS = from.getTime();
        var upperTS = until.getTime();

        for (var timestamp = lowerTS; timestamp < upperTS; ) {
            if(self.loadedHours.hasOwnProperty(timestamp)) {
                timestamp += 3600000;
            }
            else {
                var chuckUpperLimit = Math.min(upperTS, timestamp + 24*3600000);
                for (var chunkTimestamp = timestamp; chunkTimestamp < chuckUpperLimit; chunkTimestamp += 3600000) {
                    if(self.loadedHours.hasOwnProperty(chunkTimestamp))
                        break;

                    self.loadedHours[chunkTimestamp] = null;
                }

                var hourLower = new Date(timestamp);
                var hourUpper = new Date(chunkTimestamp);
                if(hourUpper > hourLower) {
                    self.getTasks(hourLower, hourUpper);
                    self.getResourceClaims(hourLower, hourUpper);
                }
                timestamp = chunkTimestamp;
            }
        }
    };

    self.clearTasksAndClaimsOutsideViewSpan = function() {
        var from = self.floorDate(self.viewTimeSpan.from, 1, 60);
        var until = self.ceilDate(self.viewTimeSpan.to, 1, 60);

        var numTasks = self.tasks.length;
        var visibleTasks = [];
        for(var i = 0; i < numTasks; i++) {
            var task = self.tasks[i];
            if(task.endtime >= from && task.starttime <= until)
                visibleTasks.push(task);
        }

        self.tasks = visibleTasks;
        self.taskDict = self.toIdBasedDict(visibleTasks);
        self.taskChangeCntr++;

        self.computeMinMaxTaskTimes();

        var numClaims = self.resourceClaims.length;
        var visibleClaims = [];
        for(var i = 0; i < numClaims; i++) {
            var claim = self.resourceClaims[i];
            if(claim.endtime >= from && claim.starttime <= until)
                visibleClaims.push(claim);
        }

        self.resourceClaims = visibleClaims;
        self.resourceClaimDict = self.toIdBasedDict(self.resourceClaims);

        self.computeMinMaxResourceClaimTimes();

        var newLoadedHours = {};
        var fromTS = from.getTime();
        var untilTS = until.getTime();
        for(var hourTS in self.loadedHours) {
            if(hourTS >= fromTS && hourTS <= untilTS)
                newLoadedHours[hourTS] = null;
        }
        self.loadedHours = newLoadedHours;
    };

    self.getTasks = function(from, until) {
        var defer = $q.defer();
        var url = '/rest/tasks';
        if(from) {
            url += '/' + self.convertLocalUTCDateToISOString(from);

            if(until) {
                url += '/' + self.convertLocalUTCDateToISOString(until);
            }
        }

        $http.get(url).success(function(result) {
            //convert datetime strings to Date objects
            for(var i in result.tasks) {
                var task = result.tasks[i];
                task.starttime = self.convertDatestringToLocalUTCDate(task.starttime);
                task.endtime = self.convertDatestringToLocalUTCDate(task.endtime);
            }

            var initialTaskLoad = self.tasks.length == 0;

            var newTaskDict = self.toIdBasedDict(result.tasks);
            var newTaskIds = Object.keys(newTaskDict);

            for(var i = newTaskIds.length-1; i >= 0; i--) {
                var task_id = newTaskIds[i];
                if(!self.taskDict.hasOwnProperty(task_id)) {
                    var task = newTaskDict[task_id];
                    self.tasks.push(task);
                    self.taskDict[task_id] = task;
                }
            }

            self.taskChangeCntr++;
            self.computeMinMaxTaskTimes();

            if(initialTaskLoad && self.tasks.length > 0) {
                setTimeout(function() {
                    self.selected_task_ids.splice(0,self.selected_task_ids.length);

                    //try to select current task
                    var currentTasks = self.tasks.filter(function(t) { return t.starttime <= self.lofarTime && t.endtime >= self.lofarTime; });

                    if(currentTasks.length > 0) {
                        self.selected_task_ids.push(currentTasks[0].id);
                    } else {
                        //try to select next task
                        var nextTasks = self.tasks.filter(function(t) { return t.starttime >= self.lofarTime; }).sort();

                        if(nextTasks.length > 0) {
                            self.selected_task_ids.push(nextTasks[0].id);
                        } else {
                            //try to select most recent task
                            var prevTasks = self.tasks.filter(function(t) { return t.endtime <= self.lofarTime; }).sort();

                            if(prevTasks.length > 0) {
                                self.selected_task_ids.push(prevTasks[prevTasks.length-1].id);
                            } else {
                                self.selected_task_ids.push(self.tasks[0].id);
                            }
                        }
                    }
                }, 1000);
            }

            defer.resolve();
        });

        return defer.promise;
    };

    self.putTask = function(task) {
        task.starttime = self.convertLocalUTCDateToISOString(task.starttime);
        task.endtime = self.convertLocalUTCDateToISOString(task.endtime);
        $http.put('/rest/tasks/' + task.id, task).error(function(result) {
            console.log("Error. Could not update task. " + result);
            //TODO: revert to old state
        })
    };
    var _getTaskBy = function(id_name, id) {
        var defer = $q.defer();

        if(typeof(id) === 'string') {
            id = parseInt(id);
        }

        var foundTask = id_name == 'id' ? self.taskDict[id] : self.tasks.find(function(t) { return t[id_name] == id; });

        if(foundTask) {
            defer.resolve(foundTask);
        } else {
            var url;
            switch(id_name) {
                case 'id': url = '/rest/tasks/' + id; break;
                case 'otdb_id': url = '/rest/tasks/otdb/' + id; break;
                case 'mom_id': url = '/rest/tasks/mom/' + id; break;
            }

            if(url) {
                $http.get(url).success(function(result) {
                    var task = result.task;
                    if(task) {
                        task.starttime = self.convertDatestringToLocalUTCDate(task.starttime);
                        task.endtime = self.convertDatestringToLocalUTCDate(task.endtime);

                        if(!self.taskDict.hasOwnProperty(task.id)) {
                            self.tasks.push(task);
                            self.taskDict[task.id] = task;
                            self.taskChangeCntr++;
                        }
                    }
                    defer.resolve(task);
                }).error(function(result) {
                    defer.resolve(undefined);
                })
            } else {
                defer.resolve(undefined);
            }
        }

        return defer.promise;
    };

    self.getTask= function(id) {
        return _getTaskBy('id', id);
    };

    self.getTaskByOTDBId = function(otdb_id) {
        return _getTaskBy('otdb_id', otdb_id);
    };

    self.getTaskByMoMId = function(mom_id) {
        return _getTaskBy('mom_id', mom_id);
    };

    self.getTasksByMoMGroupId = function(mom_object_group_id) {
        var defer = $q.defer();
        var url = '/rest/tasks/mom/group/' + mom_object_group_id;

        $http.get(url).success(function(result) {
            //convert datetime strings to Date objects
            for(var i in result.tasks) {
                var task = result.tasks[i];
                task.starttime = self.convertDatestringToLocalUTCDate(task.starttime);
                task.endtime = self.convertDatestringToLocalUTCDate(task.endtime);
            }

            var newTaskDict = self.toIdBasedDict(result.tasks);
            var newTaskIds = Object.keys(newTaskDict);

            for(var i = newTaskIds.length-1; i >= 0; i--) {
                var task_id = newTaskIds[i];
                if(!self.taskDict.hasOwnProperty(task_id)) {
                    var task = newTaskDict[task_id];
                    self.tasks.push(task);
                    self.taskDict[task_id] = task;
                }
            }

            self.taskChangeCntr++;
            self.computeMinMaxTaskTimes();

            defer.resolve(result.tasks);
        }).error(function(result) {
            defer.resolve(undefined);
        });

        return defer.promise;
    };

    self.copyTask = function(task) {
        $http.put('/rest/tasks/' + task.id + '/copy').error(function(result) {
            console.log("Error. Could not copy task. " + result);
            alert("Error: Could not copy task with mom id " + task.mom_id);
        })
    };

    self.getTaskDiskUsageByOTDBId = function(otdb_id) {
        var defer = $q.defer();
        $http.get('/rest/tasks/otdb/' + otdb_id + '/diskusage').success(function(result) {
            defer.resolve(result);
        }).error(function(result) {
            defer.resolve({found:false});
        });

        return defer.promise;
    };

    self.getTaskDiskUsage = function(task) {
        var defer = $q.defer();
        $http.get('/rest/tasks/otdb/' + task.otdb_id + '/diskusage').success(function(result) {
            result.task = task;
            defer.resolve(result);
        }).error(function(result) {
            defer.resolve({found:false});
        });

        return defer.promise;
    };

    self.computeMinMaxTaskTimes = function() {
        var starttimes = self.filteredTasks.map(function(t) { return t.starttime;});
        var endtimes = self.filteredTasks.map(function(t) { return t.endtime;});

        var minStarttime = new Date(Math.min.apply(null, starttimes));
        var maxEndtime = new Date(Math.max.apply(null, endtimes));

        self.taskTimes = {
            min: minStarttime,
            max: maxEndtime
        };
    };

    self.getResources = function() {
        var defer = $q.defer();
        $http.get('/rest/resources').success(function(result) {
            self.resources = result.resources;
            self.resourceDict = self.toIdBasedDict(self.resources);
            //try to select first storage resource as default selected_resource_id
            var storageResources = self.resources.filter(function(r) { return r.type_name == 'storage'; });
            if(storageResources.length > 0) {
                self.selected_resource_id = storageResources[0].id;
            } else {
                //else, just the first resource
                self.selected_resource_id = self.resources[0].id;
            }

            defer.resolve();
        });

        return defer.promise;
    };

    self.getResourceUsages = function() {
        var defer = $q.defer();
        $http.get('/rest/resourceusages').success(function(result) {
            //convert datetime strings to Date objects
            for(var i in result.resourceusages) {
                var resource_usages = result.resourceusages[i].usages;

                for(var status in resource_usages) {
                    var usages = resource_usages[status];
                    for(var usage of usages) {
                        usage.timestamp = self.convertDatestringToLocalUTCDate(usage.timestamp);
                    }
                }
                self.resourceUsagesDict[result.resourceusages[i].resource_id] = result.resourceusages[i];
            }

            self.resourceUsagesChangeCntr++;

            defer.resolve();
        });

        return defer.promise;
    };

    self.getResourceClaims = function(from, until) {
        var defer = $q.defer();
        var url = '/rest/resourceclaims';
        if(from) {
            url += '/' + self.convertLocalUTCDateToISOString(from);

            if(until) {
                url += '/' + self.convertLocalUTCDateToISOString(until);
            }
        }

        $http.get(url).success(function(result) {
            //convert datetime strings to Date objects
            for(var i in result.resourceclaims) {
                var resourceclaim = result.resourceclaims[i];
                resourceclaim.starttime = self.convertDatestringToLocalUTCDate(resourceclaim.starttime);
                resourceclaim.endtime = self.convertDatestringToLocalUTCDate(resourceclaim.endtime);
            }

            var newClaimDict = self.toIdBasedDict(result.resourceclaims);
            var newClaimIds = Object.keys(newClaimDict);

            for(var i = newClaimIds.length-1; i >= 0; i--) {
                var claim_id = newClaimIds[i];
                var claim = newClaimDict[claim_id];

                if(!self.resourceClaimDict.hasOwnProperty(claim_id)) {
                    self.resourceClaims.push(claim);
                    self.resourceClaimDict[claim_id] = claim;
                }
            }

            self.computeMinMaxResourceClaimTimes();

            defer.resolve();
        });

        return defer.promise;
    };

    self.computeMinMaxResourceClaimTimes = function() {
        var starttimes = self.resourceClaims.map(function(rc) { return rc.starttime;});
        var endtimes = self.resourceClaims.map(function(rc) { return rc.endtime;});

        var minStarttime = new Date(Math.min.apply(null, starttimes));
        var maxEndtime = new Date(Math.max.apply(null, endtimes));

        self.resourceClaimTimes = {
            min: minStarttime,
            max: maxEndtime
        };
    };

    self.getResourceGroups = function() {
        var defer = $q.defer();
        $http.get('/rest/resourcegroups').success(function(result) {
            self.resourceGroups = result.resourcegroups;
            self.resourceGroupsDict = self.toIdBasedDict(self.resourceGroups);

            defer.resolve();
        });

        return defer.promise;
    };

    self.getResourceGroupMemberships = function() {
        var defer = $q.defer();
        $http.get('/rest/resourcegroupmemberships').success(function(result) {
            self.resourceGroupMemberships = result.resourcegroupmemberships;

            defer.resolve();
        });

        return defer.promise;
    };

    self.getTaskTypes = function() {
        var defer = $q.defer();
        $http.get('/rest/tasktypes').success(function(result) {
            self.tasktypes = result.tasktypes;
            self.tasktypesDict = self.toIdBasedDict(self.tasktypes);

            defer.resolve();
        });

        return defer.promise;
    };

    self.getTaskStatusTypes = function() {
        var defer = $q.defer();
        $http.get('/rest/taskstatustypes').success(function(result) {
            self.taskstatustypes = result.taskstatustypes;

            self.editableTaskStatusIds = [];
            for(var taskstatustype of self.taskstatustypes) {
                if(taskstatustype.name == 'approved' || taskstatustype.name == 'conflict' || taskstatustype.name == 'prescheduled') {
                    self.editableTaskStatusIds.push(taskstatustype.id);
                }
            }

            defer.resolve();
        });

        return defer.promise;
    };

    self.getMoMProjects = function() {
        var defer = $q.defer();
        $http.get('/rest/momprojects').success(function(result) {
            //convert datetime strings to Date objects
            var dict = {};
            var list = [];
            for(var i in result.momprojects) {
                var momproject = result.momprojects[i];
                momproject.statustime = new Date(momproject.statustime);
                dict[momproject.mom_id] = momproject;
                list.push(momproject);
            }

            list.sort(function(a, b) { return ((a.name < b.name) ? -1 : ((a.name > b.name) ? 1 : 0)); });

            self.momProjects = list;
            self.momProjectsDict = dict;

            defer.resolve();
        });

        return defer.promise;
    };

    self.getMoMObjectDetailsForTask = function(task) {
        $http.get('/rest/momobjectdetails/'+task.mom_id).success(function(result) {
            if(result.momobjectdetails) {
                task.name = result.momobjectdetails.object_name;
                task.project_name = result.momobjectdetails.project_name;
                task.project_id = result.momobjectdetails.project_mom_id;
            }
        });
    };

    self.getProjectDiskUsage = function(project_name) {
        var defer = $q.defer();
        $http.get('/rest/momprojects/' + project_name + '/diskusage').success(function(result) {
            defer.resolve(result);
        }).error(function(result) {
            defer.resolve({found:false});
        });

        return defer.promise;
    };

    self.getProjectsDiskUsage = function() {
        var defer = $q.defer();
        $http.get('/rest/momprojects/diskusage').success(function(result) {
            defer.resolve(result);
        }).error(function(result) {
            defer.resolve({found:false});
        });

        return defer.promise;
    };

    self.getConfig = function() {
        var defer = $q.defer();
        $http.get('/rest/config').success(function(result) {
            self.config = result.config;
            defer.resolve();
        });

        return defer.promise;
    };


    //start with local client time
    //lofarTime will be synced with server,
    //because local machine might have incorrect clock
    //take utcOffset into account, see explanation above.
    self.lofarTime = new Date(Date.now() - self.utcOffset);

    self._syncLofarTimeWithServer = function() {
        $http.get('/rest/lofarTime', {timeout:1000}).success(function(result) {
            self.lofarTime = self.convertDatestringToLocalUTCDate(result.lofarTime);

            //check if local to utc offset has changed
            self.utcOffset = moment().utcOffset()*60000;
        });

        setTimeout(self._syncLofarTimeWithServer, 60000);
    };
    self._syncLofarTimeWithServer();

    self.lastUpdateChangeNumber = undefined;

    self.initialLoad = function() {
        $http.get('/rest/mostRecentChangeNumber').success(function(result) {
            if(result.mostRecentChangeNumber >= 0) {
                self.lastUpdateChangeNumber = result.mostRecentChangeNumber;
            }

            var nrOfItemsToLoad = 7;
            var nrOfItemsLoaded = 0;
            var checkInitialLoadCompleteness = function() {
                nrOfItemsLoaded += 1;
                if(nrOfItemsLoaded >= nrOfItemsToLoad) {
                    self.initialLoadComplete = true;
                }
            };

            self.getConfig().then(checkInitialLoadCompleteness);
            self.getMoMProjects().then(checkInitialLoadCompleteness);
            self.getTaskTypes().then(checkInitialLoadCompleteness);
            self.getTaskStatusTypes().then(checkInitialLoadCompleteness);
            self.getResourceGroups().then(checkInitialLoadCompleteness);
            self.getResources().then(checkInitialLoadCompleteness);
            self.getResourceGroupMemberships().then(checkInitialLoadCompleteness);

            self.getTasksAndClaimsForViewSpan();

            self.getResourceUsages();

            self.subscribeToUpdates();
        });
    };

    self.subscribeToUpdates = function() {
        var url = '/rest/updates';
        if(self.lastUpdateChangeNumber) {
            url += '/' + self.lastUpdateChangeNumber;
        }
        $http.get(url, {timeout:300000}).success(function(result) {

            try {
                var changeNumbers = result.changes.map(function(item) { return item.changeNumber; });
                self.lastUpdateChangeNumber = changeNumbers.reduce(function(a, b, idx, arr) { return a > b ? a : b; }, undefined);

                var anyResourceClaims = false;
                for(var i in result.changes) {
                    try {
                        var change = result.changes[i];

                        if(change.objectType == 'task') {
                            var changedTask = change.value;
                            if(change.changeType == 'update') {
                                var task = self.taskDict[changedTask.id];
                                if(task) {
                                    self.applyChanges(task, changedTask);
                                }
                            } else if(change.changeType == 'insert') {
                                var task = self.taskDict[changedTask.id];
                                if(!task) {
                                    changedTask.starttime = self.convertDatestringToLocalUTCDate(changedTask.starttime);
                                    changedTask.endtime = self.convertDatestringToLocalUTCDate(changedTask.endtime);
                                    self.tasks.push(changedTask);
                                    self.taskDict[changedTask.id] = changedTask;
                                }
                            } else if(change.changeType == 'delete') {
                                delete self.taskDict[changedTask.id]
                                for(var k = self.tasks.length-1; k >= 0; k--) {
                                    if(self.tasks[k].id == changedTask.id) {
                                        self.tasks.splice(k, 1);
                                        break;
                                    }
                                }
                            }

                            self.taskChangeCntr++;

                            self.computeMinMaxTaskTimes();
                        } else if(change.objectType == 'resourceClaim') {
                            anyResourceClaims = true;
                            var changedClaim = change.value;
                            if(change.changeType == 'update') {
                                var claim = self.resourceClaimDict[changedClaim.id];
                                if(claim) {
                                    self.applyChanges(claim, changedClaim);
                                }
                            } else if(change.changeType == 'insert') {
                                var claim = self.resourceClaimDict[changedClaim.id];
                                if(!claim) {
                                    changedClaim.starttime = self.convertDatestringToLocalUTCDate(changedClaim.starttime);
                                    changedClaim.endtime = self.convertDatestringToLocalUTCDate(changedClaim.endtime);
                                    self.resourceClaims.push(changedClaim);
                                    self.resourceClaimDict[changedClaim.id] = changedClaim;
                                }
                            } else if(change.changeType == 'delete') {
                                delete self.resourceClaimDict[changedClaim.id]
                                for(var k = self.resourceClaims.length-1; k >= 0; k--) {
                                    if(self.resourceClaims[k].id == changedClaim.id) {
                                        self.resourceClaims.splice(k, 1);
                                        break;
                                    }
                                }
                            }

                            self.claimChangeCntr++;
                            self.computeMinMaxResourceClaimTimes();
                        } else if(change.objectType == 'resourceCapacity') {
                            if(change.changeType == 'update') {
                                var changedCapacity = change.value;
                                var resource = self.resourceDict[changedCapacity.resource_id];
                                if(resource) {
                                    resource.available_capacity = changedCapacity.available;
                                    resource.total_capacity = changedCapacity.total;
                                }
                            }
                        } else if(change.objectType == 'resourceAvailability') {
                            if(change.changeType == 'update') {
                                var changedAvailability = change.value;
                                var resource = self.resourceDict[changedAvailability.resource_id];
                                if(resource) {
                                    resource.active = changedAvailability.total;
                                }
                            }
                        }
                    } catch(err) {
                        console.log(err)
                    }
                }
                if(anyResourceClaims) {
                    self.getResourceUsages();
                }
            } catch(err) {
                console.log(err)
            }

            //and update again
            self.subscribeToUpdates();
        }).error(function() {
            setTimeout(self.subscribeToUpdates, 1000);
        });
    };

    return self;
}]);

var dataControllerMod = angular.module('DataControllerMod', ['ngResource']);

dataControllerMod.controller('DataController',
                            ['$scope', '$q', 'dataService',
                            function($scope, $q, dataService) {
    var self = this;
    $scope.dataService = dataService;
    dataService.dataCtrl = this;

    $scope.dateOptions = {
        formatYear: 'yyyy',
        startingDay: 1
    };

    $scope.viewFromDatePopupOpened = false;
    $scope.viewToDatePopupOpened = false;

    $scope.openViewFromDatePopup = function() { $scope.viewFromDatePopupOpened = true; };
    $scope.openViewToDatePopup = function() { $scope.viewToDatePopupOpened = true; };
    $scope.zoomTimespans = [{value:30, name:'30 Minutes'}, {value:60, name:'1 Hour'}, {value:3*60, name:'3 Hours'}, {value:6*60, name:'6 Hours'}, {value:12*60, name:'12 Hours'}, {value:24*60, name:'1 Day'}, {value:2*24*60, name:'2 Days'}, {value:3*24*60, name:'3 Days'}, {value:5*24*60, name:'5 Days'}, {value:7*24*60, name:'1 Week'}, {value:14*24*60, name:'2 Weeks'}, {value:28*24*60, name:'4 Weeks'}, {value:1, name:'Custom (1 min)'}];
    $scope.zoomTimespan = $scope.zoomTimespans[5];
    $scope.jumpToNow = function() {
        var floorLofarTime = dataService.floorDate(dataService.lofarTime, 1, 5);
        dataService.viewTimeSpan = {
            from: dataService.floorDate(new Date(floorLofarTime.getTime() - 0.25*$scope.zoomTimespan.value*60*1000), 1, 5),
            to: dataService.floorDate(new Date(floorLofarTime.getTime() + 0.75*$scope.zoomTimespan.value*60*1000), 1, 5)
        };
    };

    $scope.jumpToNow();
    
    $scope.loadTasksSelectAndJumpIntoView = function(task_ids) {
        var list_of_promises = task_ids.map(function(t_id) { return $scope.dataService.getTask(t_id); });
        var defer = $q.defer();
        $q.all(list_of_promises).then(function(in_tasks) {
            var loaded_tasks = in_tasks.filter(function(t) { return t != undefined; });         
            var loaded_tasks_ids = loaded_tasks.map(function(t) { return t.id; });
            $scope.dataService.setSelectedTaskIds(loaded_tasks_ids);
            $scope.jumpToSelectedTasks();
            defer.resolve(loaded_tasks);
        });
        return defer.promise;
    };    

    $scope.loadTaskByOTDBIdSelectAndJumpIntoView = function(otdb_id) {
        var defer = $q.defer();
        $scope.dataService.getTaskByOTDBId(otdb_id).then(function(task) {
            if(task) {
                $scope.dataService.setSelectedTaskId(task.id);
                $scope.jumpToSelectedTasks();
                defer.resolve(task);
            } else {
                defer.resolve(undefined);
            }
        });
        return defer.promise;
    };

    $scope.loadTaskByMoMIdSelectAndJumpIntoView = function(mom_id) {
        var defer = $q.defer();
        $scope.dataService.getTaskByMoMId(mom_id).then(function(task) {
            if(task) {
                $scope.dataService.setSelectedTaskId(task.id);
                $scope.jumpToSelectedTasks();
                defer.resolve(task);
            } else {
                defer.resolve(undefined);
            }
        });
        return defer.promise;
    };

    $scope.loadTasksByMoMGroupIdSelectAndJumpIntoView = function(mom_group_id) {
        var defer = $q.defer();
        $scope.dataService.getTasksByMoMGroupId(mom_group_id).then(function(tasks) {
            if(tasks) {
                var task_ids = tasks.map(function(t) { return t.id; });

                $scope.dataService.setSelectedTaskIds(task_ids);
                $scope.jumpToSelectedTasks();
                defer.resolve(tasks);
            } else {
                defer.resolve(undefined);
            }
        });
        return defer.promise;
    };

    $scope.selectCurrentTask = function() {
        var currentTasks = dataService.tasks.filter(function(t) { return t.starttime <= dataService.viewTimeSpan.to && t.endime >= dataService.viewTimeSpan.from; });
        if(currentTasks.lenght > 0) {
            dataService.setSelectedTaskId(currentTasks[0].id);
        }
    };

    $scope.jumpToSelectedTasks = function() {
        if(dataService.selected_task_ids == undefined)
            return;

        var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; });

        if(tasks.lenght == 0)
            return;

        var minStarttime = new Date(Math.min.apply(null, tasks.map(function(t) { return t.starttime; })));
        var maxEndtime = new Date(Math.max.apply(null, tasks.map(function(t) { return t.endtime; })));

        var selectedTasksDurationInmsec = maxEndtime.getTime() - minStarttime.getTime();
        var selectedTasksDurationInMinutes = selectedTasksDurationInmsec/60000;
        var viewSpanInMinutes = selectedTasksDurationInMinutes;

        var fittingSpans = $scope.zoomTimespans.filter(function(w) { return w.value >= selectedTasksDurationInMinutes; });
        if(fittingSpans.length > 0) {
            $scope.zoomTimespan = fittingSpans[0];
            //select one span larger if possible
            if(fittingSpans.length > 1)
                $scope.zoomTimespan = fittingSpans[1];
            viewSpanInMinutes = $scope.zoomTimespan.value;
        }

        var focusTime = new Date(minStarttime.getTime() + 0.5*selectedTasksDurationInmsec);

        dataService.viewTimeSpan = {
            from: dataService.floorDate(new Date(focusTime.getTime() - 0.25*viewSpanInMinutes*60*1000), 1, 5),
            to: dataService.floorDate(new Date(focusTime.getTime() + 0.75*viewSpanInMinutes*60*1000), 1, 5)
        };
        dataService.autoFollowNow = false;
    };

    $scope.scrollBack = function() {
        dataService.autoFollowNow = false;
        var viewTimeSpanInmsec = dataService.viewTimeSpan.to.getTime() - dataService.viewTimeSpan.from.getTime();
        dataService.viewTimeSpan = {
            from: dataService.floorDate(new Date(dataService.viewTimeSpan.from.getTime() - 0.25*viewTimeSpanInmsec), 1, 5),
            to: dataService.floorDate(new Date(dataService.viewTimeSpan.to.getTime() - 0.25*viewTimeSpanInmsec), 1, 5)
        };
    };

    $scope.scrollForward = function() {
        dataService.autoFollowNow = false;
        var viewTimeSpanInmsec = dataService.viewTimeSpan.to.getTime() - dataService.viewTimeSpan.from.getTime();
        dataService.viewTimeSpan = {
            from: dataService.floorDate(new Date(dataService.viewTimeSpan.from.getTime() + 0.25*viewTimeSpanInmsec), 1, 5),
            to: dataService.floorDate(new Date(dataService.viewTimeSpan.to.getTime() + 0.25*viewTimeSpanInmsec), 1, 5)
        };
    };

    $scope.onZoomTimespanChanged = function() {
        var viewTimeSpanInmsec = dataService.viewTimeSpan.to.getTime() - dataService.viewTimeSpan.from.getTime();
        var focusTime = new Date(dataService.viewTimeSpan.from + 0.5*viewTimeSpanInmsec);

        if(dataService.autoFollowNow) {
            focusTime = dataService.floorDate(dataService.lofarTime, 1, 5);
        } else {
            var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; });

            if(tasks.lenght > 0) {
                var minStarttime = new Date(Math.min.apply(null, tasks.map(function(t) { return t.starttime; })));
                var maxEndtime = new Date(Math.max.apply(null, tasks.map(function(t) { return t.endtime; })));

                focusTime = dataService.floorDate(new Date(0.5*(minStarttime.getTime() + maxEndtime.getTime())), 1, 5);
            }
        }

        dataService.viewTimeSpan = {
            from: dataService.floorDate(new Date(focusTime.getTime() - 0.25*$scope.zoomTimespan.value*60*1000)),
            to: dataService.floorDate(new Date(focusTime.getTime() + 0.75*$scope.zoomTimespan.value*60*1000))
        };
    };

    $scope.selectZoomTimespan = function() {
        var viewTimeSpanInmsec = dataService.viewTimeSpan.to.getTime() - dataService.viewTimeSpan.from.getTime();
        var viewTimeSpanInMinutes = Math.round(viewTimeSpanInmsec/60000);

        var foundZoomTimespan = $scope.zoomTimespans.find(function(zts) { return zts.value == viewTimeSpanInMinutes; });

        if(foundZoomTimespan) {
            $scope.zoomTimespan = foundZoomTimespan;
        } else {
            var customZoomTimespan = $scope.zoomTimespans.find(function(zts) { return zts.name.startsWith('Custom'); });
            customZoomTimespan.value = viewTimeSpanInMinutes;
            customZoomTimespan.name = 'Custom (' + viewTimeSpanInMinutes + ' min)';
            $scope.zoomTimespan = customZoomTimespan;
        }
    };
    $scope.onViewTimeSpanFromChanged = function() {
        if (!isNaN(dataService.viewTimeSpan.from)) {
            dataService.autoFollowNow = false;
            if(dataService.viewTimeSpan.from >= dataService.viewTimeSpan.to) {
                dataService.viewTimeSpan.to = dataService.floorDate(new Date(dataService.viewTimeSpan.from.getTime() + $scope.zoomTimespan.value*60*1000), 1, 5);
            }
        }
    };

    $scope.onViewTimeSpanToChanged = function() {
        if (!isNaN(dataService.viewTimeSpan.to)) {
            dataService.autoFollowNow = false;
            if(dataService.viewTimeSpan.to <= dataService.viewTimeSpan.from) {
                dataService.viewTimeSpan.from = dataService.floorDate(new Date(dataService.viewTimeSpan.to.getTime() - $scope.zoomTimespan.value*60*1000), 1, 5);
            }
        }
    };

    $scope.$watch('dataService.viewTimeSpan', function() {
        $scope.selectZoomTimespan();

        $scope.$evalAsync(function() { dataService.clearTasksAndClaimsOutsideViewSpan(); });
        $scope.$evalAsync(function() { dataService.getTasksAndClaimsForViewSpan(); });
    }, true);

    $scope.$watch('dataService.filteredTaskChangeCntr', dataService.computeMinMaxTaskTimes);

    $scope.$watch('dataService.lofarTime', function() {
        if(dataService.autoFollowNow && (Math.round(dataService.lofarTime.getTime()/1000))%5==0) {
            $scope.jumpToNow();
        }
    });

    $scope.$watch('dataService.autoFollowNow', function() {
        if(dataService.autoFollowNow) {
            $scope.jumpToNow();
        }
    });

    dataService.initialLoad();

    //clock ticking every second
    //updating current lofarTime by the elapsed time since previous tick
    //lofarTime is synced every minute with server utc time.
    self._prevTick = Date.now();
    self._doTimeTick = function() {
        var tick = Date.now();
        var elapsed = tick - self._prevTick;
        self._prevTick = tick;
        //evalAsync, so lofarTime will be seen by watches
        $scope.$evalAsync(function() {
            dataService.lofarTime = new Date(dataService.lofarTime.getTime() + elapsed);
        });

        setTimeout(self._doTimeTick, 1000);
    };
    self._doTimeTick();
}
]);

//extend the default dateFilter so that it always displays dates as 'yyyy-MM-dd HH:mm:ss'
//without any extra timezone string.
//see also comments above why we do tricks with local and utc dates
angular.module('raeApp').config(['$provide', function($provide) {
    $provide.decorator('dateFilter', ['$delegate', function($delegate) {
        var srcFilter = $delegate;

        function zeroPaddedString(num) {
            var numstr = num.toString();
            if(numstr.length < 2) {
                return '0' + numstr;
            }
            return numstr;
        };

        var extendsFilter = function() {
            if(arguments[0] instanceof Date && arguments.length == 1) {
                var date = arguments[0];
                var dateString =  date.getFullYear() + '-' + zeroPaddedString(date.getMonth()+1) + '-' + zeroPaddedString(date.getDate()) + ' ' +
                                  zeroPaddedString(date.getHours()) + ':' + zeroPaddedString(date.getMinutes()) + ':' + zeroPaddedString(date.getSeconds());

                return dateString;
            }
            return srcFilter.apply(this, arguments);
        }

        return extendsFilter;
    }])
}])