Free cookie consent management tool by TermsFeed Policy Generator

source: branches/OaaS/HeuristicLab.Services.Optimization.Web/Content/experiment.view.js @ 9311

Last change on this file since 9311 was 9305, checked in by fschoepp, 12 years ago

#1888:

  • Added an Update / GetExperiment... methods to the controller for updating and querying experiments.
  • The AlgorithmConverter class now properly converts from/to JSON format.
  • Integrated backbone js as MVC provider for JavaScript + jquery.
  • Added experiment.model.js + experiment.view.js + experiment.controller.js containing the MVC impl. for the Experiment pages.
  • Added new methods to the ExperimentController usable by the backbone js model implementation.
  • Added the experiment dialog from HL 3.3.7 (variate experiment parameters). It's capable of variating the algorithm parameters.
File size: 24.9 KB
Line 
1// ================ VIEWS =======================
2var ExperimentTreeView = Backbone.View.extend({
3    renderTree: function (model) {
4        var jsonModel = model.toJSON();
5        var self = this;
6        this.tree = $('<div />').dynatree({
7            children: [jsonModel],
8            autoExpandMS: 750,
9            onActivate: function (node) {
10                // if we are able to remove this node from the tree, enable the remove button.
11                if (node && node.parent.parent != null) {
12                    $('.remove-button', this.$el).removeAttr("disabled");
13                } else if (node) {
14                    $('.remove-button', this.$el).attr("disabled", "disabled");
15                }
16
17                var modelNode = ExperimentNode.lookup(node.data.key);
18                if (modelNode && !modelNode.get('isExperiment')) {
19                    $('.variate-button', this.$el).removeAttr("disabled");
20                } else {
21                    $('.variate-button', this.$el).attr("disabled", "disabled");
22                }
23            },
24            dnd: {
25                onDragStart: function (node) {
26                    return node.parent.parent != null;
27                },
28                onDragEnter: function (node, sourceNode) {
29                    return true;
30                },
31                onDragOver: function (node, sourceNode, hitMode) {
32                    return node.data.title !== "Experiment" || hitMode === "over";
33                },
34                onDrop: function (node, sourceNode, hitMode, ui, draggable) {
35                    if (hitMode !== 'over')
36                        return;
37                    var modelNode = ExperimentNode.lookup(node.data.key);
38                    if (sourceNode) {
39                        var sourceModelNode = ExperimentNode.lookup(sourceNode.data.key);
40                        sourceNode.move(node, hitMode);
41                        // move node into the modelNode
42                        sourceModelNode.moveNode(modelNode);
43                    }
44                    else {
45                        var elem = $(draggable.element[0]);
46                        var newNode = { nodeId: elem.attr('id'), isExperiment: elem.attr('data-isExperiment') === 'true', title: elem.attr('data-name'), key: ExperimentNode.nextGlobalKey() };
47                        if (hitMode == "over") {
48                            node.addChild(newNode);
49                            node.expand(true);
50                            // update the data model
51                            newNode = modelNode.addNode(newNode);
52                            self.trigger('node-added', newNode);
53                        }
54                    }
55                    self.trigger('structure-changed', modelNode);
56                }
57            }
58        });
59        this.tree.dynatree("getRoot").visit(function (node) {
60            node.expand(true);
61        });
62    },
63    initialize: function (spec) {
64        this.renderTree(spec.model);
65    },
66    events: {
67        'click .remove-button': 'doRemove',
68        'click .variate-button': 'doVariate'
69    },
70    render: function () {
71        this.$el.empty();
72        this.renderTree(this.model);
73        var content = $(_.template($('#stepwizard_template').html(), {}));
74        this.tree.appendTo(this.$el);
75        content.appendTo(this.$el);
76    },
77    doRemove: function () {
78        var node = this.tree.dynatree('getActiveNode');
79        if (node && node.parent.parent != null) {
80            var nxt = node.getNextSibling();
81            if (nxt == null)
82                nxt = node.getPrevSibling();
83            if (nxt == null)
84                nxt = node.getParent();
85            node.remove();
86            ExperimentNode.lookup(node.data.key).remove();
87            if (nxt) {
88                this.tree.dynatree("getTree").activateKey(nxt.data.key);
89            }
90        }
91        this.trigger('structure-changed', node);
92    },
93    doVariate: function () {
94        var node = this.tree.dynatree('getActiveNode');
95        var modelNode = ExperimentNode.lookup(node.data.key);
96        this.trigger('variation-request', {
97            parentNode: node.parent,
98            model: modelNode
99        });
100    }
101});
102
103var ExperimentDetailsTreeView = Backbone.View.extend({
104  render: function () {
105    var jsonModel = this.model.toJSON();
106    var self = this;
107    var tree = $('<div />').dynatree({
108      children: [jsonModel],
109      autoExpandMS: 750,
110      onActivate: function (node) {
111        // if we click a node, simply open a dialog to enter it's parameters
112        self.trigger('node-clicked', node);
113      }
114    });
115
116    this.$el.empty();
117    tree.appendTo(this.$el);
118    tree.dynatree("getRoot").visit(function (node) {
119        node.expand(true);
120    });
121  }
122});
123
124var DraggableGroup = Backbone.View.extend({
125    render: function () {
126        var self = this;
127        var div = $('<div />');
128
129        for (var i = 0; i < this.model.length; i++) {
130            new DraggableView({ model: this.model.models[i], el: div }).render();
131        }
132
133        this.$el.empty();
134        div.appendTo(this.$el);
135    }
136});
137
138
139var DraggableView = Backbone.View.extend({
140  render: function () {
141    var late = _.template($("#draggable_template").html(), this.model.attributes);   
142    $(late).draggable({
143      revert: true,
144      connectToDynatree: true,
145      cursorAt: { top: -5, left: -5 },
146      helper: "clone"
147    }).appendTo(this.$el);
148  }
149});
150
151var SelectableGroup = Backbone.View.extend({
152    render: function () {
153        var self = this;
154        var div = $('<div />');
155        for (var i = 0; i < this.model.length; i++) {
156            new SelectableView({ model: this.model.models[i], el: div }).render();
157        }
158        this.$el.empty();
159        div.selectable({
160            filter: 'div',
161            selected: function (event, ui) {
162                self.selected(event, ui);
163            }
164        }).appendTo(this.$el);
165    },
166    selected: function (event, ui) {
167        var nodeId = $(ui.selected).attr('id');
168        this.trigger('node-selected', nodeId);
169    }
170});
171
172var SelectableView = Backbone.View.extend({
173    render: function () {
174        var late = _.template($("#draggable_template").html(), this.model.attributes);
175        $(late).appendTo(this.$el);
176    }
177});
178
179var StepWizardView = Backbone.View.extend({
180    render: function () {
181        var self = this;
182        this.$el.smartWizard({
183            onLeaveStep: function (step) {
184                var stepNum = parseInt(step.attr("rel"));
185                return self.validateStep(stepNum);
186            },
187            onShowStep: function (step) {
188                var stepNum = parseInt(step.attr("rel"));
189                return self.showStep(stepNum);
190            },
191            onFinish: function () {
192                self.trigger('experiment-finished');
193            }
194        });
195    },
196    validateStep: function (stepNumber) {
197        var validationModel = {
198            stepNumber: stepNumber,
199            succeeded: true
200        };
201        this.trigger('step-validated', validationModel);
202        return validationModel.succeeded;
203    },
204    showStep: function (stepNumber) {
205        this.trigger('step-shown', stepNumber);
206        return true;
207    }
208});
209
210var ParameterWizard = Backbone.View.extend({
211  createWizard: function (steps) {
212    // use template instead of jquery?!
213    var newWizard = $('<div />').addClass('wizard swMain');
214    var stepMenu = $('<ul />');
215    var prefix = name + '_';
216    for (var i = 0; i < steps.length; i++) {
217      var step = steps[i];
218      var a = $('<a></a>', { href: '#' + prefix + (i + 1) });
219      $('<label />').addClass("stepNumber").text((i + 1)).appendTo(a);
220      $('<span />').addClass("stepDesc").html(step.heading + '<br /><small>' + step.description + '</small>').appendTo(a);
221      var li = $('<li />').append(a);
222      li.appendTo(stepMenu);
223    }
224    stepMenu.appendTo(newWizard);
225    for (var i = 0; i < steps.length; i++) {
226      var div = $('<div />', { id: prefix + (i + 1) });
227      if (steps[i].content && steps[i].content != null)
228        div.append(steps[i].content);
229      div.appendTo(newWizard);
230    }
231    return newWizard;
232  },
233  render: function () {
234    var self = this;
235    this.$el.empty();
236    var data = this.model.get('data');
237
238    var hasProblemParameters = typeof data.ProblemParameters !== 'undefined';
239    var dialog = $('<div />');
240    var algoDiv = $('<div />');
241    var problemDiv = $('<div />');
242
243    for (var i = 0; i < data.AlgorithmParameters.length; i++) {
244      var parameterView = new ParameterEditableView({ model: data.AlgorithmParameters[i] });
245      // render to dialog
246      parameterView.render();
247      parameterView.$el.appendTo(algoDiv);
248    }
249
250    if (hasProblemParameters) {
251      var problemParameters = data.ProblemParameters;
252      for (var i = 0; i < problemParameters.length; i++) {
253        var parameterView = new ParameterEditableView({ model: problemParameters[i] });
254        // render to dialog
255        parameterView.render();
256        parameterView.$el.appendTo(problemDiv);
257      }
258    }
259
260    var newWizard =
261        hasProblemParameters ?
262          this.createWizard([
263            { heading: "Algorithm Parameters", description: "Adjust algorithm parameters", content: algoDiv },
264            { heading: "Problem Parameters", description: "Adjust problem parameters", content: problemDiv }
265          ]) :
266          this.createWizard([
267            { heading: "Algorithm Parameters", description: "Adjust algorithm parameters", content: algoDiv }
268          ]);
269
270    newWizard.appendTo(this.$el);
271    newWizard.smartWizard({
272      onFinish: function () {
273        self.trigger('parameters-finished');
274      }
275    });
276  }
277});
278
279var ParameterEditableView = Backbone.View.extend({
280    events: {
281        'change': 'valueChanged'
282    },
283    render: function () {
284        var mapper = new DatatypeMapper();
285        var content = $(_.template($('#parameter_template').html(), this.model));
286        mapper.mapHtml(this.model).appendTo($('.rightEntry', content));
287        content.appendTo(this.$el);
288    },
289    valueChanged: function (param) {
290        var value = $(param.target).val();
291        if (!isNaN(value))
292            value = parseFloat(value);
293        var name = $(param.target).attr("name");
294        // normally a controller would do this, just for once, let the view update the model
295        var splitted = name.split("_");
296        if (splitted.length == 1) {
297            this.model.Value = value;
298        } else if (splitted.length == 2) {
299            this.model.Value[parseInt(splitted[1])] = value;
300        } else if (splitted.length == 3) {
301            this.model.Value[parseInt(splitted[1])][parseInt(splitted[2])] = value;
302        }
303    }
304});
305
306
307var LoadingDialog = Backbone.View.extend({
308    initialize: function () {
309        this.reset();
310    },
311    render: function () {
312        var self = this;
313        this.$el.empty();
314        var dialog = $('<div />');
315        this.content.appendTo(dialog);
316        dialog.appendTo(this.$el);
317        this.$el.dialog({
318            autoOpen: true
319        });
320    },
321    text: function (txt) {
322        $('p', this.$el).text(txt);
323    },
324    setLoading: function(isLoading) {
325      if (isLoading)
326        $('img', this.$el).show();
327      else
328        $('img', this.$el).hide();
329    },
330    reset: function () {
331        this.content = $(_.template($('#loading_template').html(), {}));
332    },
333    setContent: function (el) {
334        this.content = el;
335    },
336    close: function () {
337        this.$el.dialog("close");
338    }
339});
340
341
342var ParameterDialog = Backbone.View.extend({
343  render: function () {
344    var self = this;
345
346    this.$el.empty();
347    var data = this.model.get('data');
348
349    var hasProblemParameters = typeof data.ProblemParameters !== 'undefined';
350    var dialog = $('<div />');
351
352    var parameterWizard = new ParameterWizard({ model: this.model, el: dialog });
353    parameterWizard.render();
354    this.listenTo(parameterWizard, 'parameters-finished', function () {
355      self.trigger('parameters-finished');
356    });
357    dialog.appendTo(this.$el);
358    this.$el.dialog({
359      autoOpen: true,
360      width: 1024,
361      height: 768
362    });
363  },
364  close: function () {
365    this.$el.dialog("close");
366  }
367});
368
369var ExperimentDetailsView = Backbone.View.extend({
370    events: {
371        'change': 'valueChanged'
372    },
373    valueChanged: function (param) {
374        var value = $(param.target).val();
375        if (!isNaN(value))
376            value = parseFloat(value);
377        var name = $(param.target).attr("name");
378        if (name == 'Name')
379            this.model.set({ title: value });
380        else if (name == 'RunImmediately')
381            this.model.set({ run: $(param.target).is(':checked') });
382        else if (name == 'Repititions')
383            this.model.set({ repititions: value });
384        else if (name == 'Group')
385            this.model.set({ group: value });
386
387        if (this.model.get('run')) {
388            $('input[name="Repititions"]', this.$el).removeAttr('disabled');
389            $('input[name="Group"]', this.$el).removeAttr('disabled');
390        } else {
391            $('input[name="Repititions"]', this.$el).attr('disabled', 'disabled');
392            $('input[name="Group"]', this.$el).attr('disabled', 'disabled');
393        }
394    },
395    render: function () {
396        $('input[name="Name"]', this.$el).val(this.model.get('title'));
397        $('input[name="RunImmediately"]', this.$el).attr('checked', this.model.get('run'));
398        $('input[name="Repititions"]', this.$el).val(this.model.get('repititions'));
399        $('input[name="Group"]', this.$el).val(this.model.get('group'));
400    }
401});
402
403var ValidationHintsView = Backbone.View.extend({
404    render: function () {
405        var template = _.template($('#validationhints_template').html(), this.model);
406        $(template).dialog({
407            resizable: false,
408            modal: true,
409            buttons: {
410                "OK": function () {
411                    $(this).dialog("close");
412                }
413            }
414        });
415    }
416});
417
418// ========== Variation Dialog Views ============
419
420var VariationDialog = Backbone.View.extend({
421    initialize: function (spec) {
422
423    },
424    events: {
425        'change': 'entrySelected'
426    },
427    render: function () {
428        var self = this;
429        var dialog = $(_.template($('#variationdialog_template').html(), {}));
430
431        var content = $('select[class="variationDialogContent"]', dialog);
432        var data = this.model.get('data');
433        for (var i = 0; i < data.AlgorithmParameters.length; i++) {
434            data.AlgorithmParameters[i].Index = i;
435            var entry = new VariationEntryView({ model: data.AlgorithmParameters[i], el: content });
436            entry.render();
437        }
438
439        this.details = $('div[class="variationDetails"]', dialog);
440        this.activateButton = $('input[name="active"]', dialog);
441        this.activateButton.change(function (evt) {
442            var checked = $(evt.target).is(':checked');
443            if (self.selectedModel && self.selectedModel != null) {
444                self.selectedModel.Active = checked;
445            }
446        });
447        dialog.dialog({
448            height: 300,
449            buttons: {
450                "Abort": function () {
451                    $(this).dialog("close");
452                },
453                "OK": function () {
454                    self.generateVariations();
455                    $(this).dialog("close");
456                }
457            }
458        });
459
460        content.change(function (evt) {
461            var optionSelected = $("option:selected", this);
462            var model = self.model.get('data').AlgorithmParameters[parseInt($(optionSelected).attr('data-index'))]
463            self.entrySelected(model);
464        });
465
466        this.variationDetails = new VariationContentView({ el: this.details });
467        var model = this.model.get('data').AlgorithmParameters[0];
468        this.entrySelected(model);
469    },
470    entrySelected: function (model) {
471        this.selectedModel = model;
472        if (model.Active)
473            this.activateButton.attr('checked', 'checked');
474        else
475            this.activateButton.removeAttr('checked');
476        this.variationDetails.model = model;
477        this.details.empty();
478        this.variationDetails.render();
479    },
480    generateVariations: function () {
481        this.solutions = [];
482        this.exhaust(0, []);
483        this.trigger('variations-generated', this.solutions);
484    },
485    exhaust: function (index, element) {
486        if (index == this.model.get('data').AlgorithmParameters.length) {
487            // we found a solution! store it by creating a deep copy
488            this.solutions.push($.extend(true, [], element));
489            return;
490        }
491
492        var currentParameter = this.model.get('data').AlgorithmParameters[index];
493        if (currentParameter.Active && currentParameter.Generated) {
494            for (var i = 0; i < currentParameter.Generated.length; i++) {
495                element[index] = {
496                    Name: currentParameter.Name,
497                    Value: currentParameter.Generated[i]
498                };
499                if (currentParameter.Options) {
500                    element[index].Options = currentParameter.Options;
501                }
502                this.exhaust(index + 1, element);
503            }
504        } else {
505            element[index] = {
506                Name: currentParameter.Name,
507                Value: currentParameter.Value
508            };
509            if (currentParameter.Options) {
510                element[index].Options = currentParameter.Options;
511            }
512            this.exhaust(index + 1, element);
513        }
514    }
515});
516
517var TableEditView = Backbone.View.extend({
518    events: {
519        'change': 'valueChanged',
520        'click button[data-operation="Add"]': 'addClicked',
521        'click button[data-operation="Remove"]': 'removeClicked'
522    },
523    render: function () {
524        this.$el.empty();
525        // http: //stackoverflow.com/questions/8749236/create-table-with-jquery-append
526        var table = $('<table></table>').addClass('editableTable');
527
528        // determine dimensions
529        var rows = this.model.length;
530        var columns = 1;
531        if ($.isArray(this.model[0])) {
532            columns = this.model[0].length;
533        }
534
535        // create head elements
536        var head = $('<thead></thead>');
537        var headerRow = $('<tr></tr>');
538        headerRow.appendTo(head);
539        for (var i = 0; i < columns; i++) {
540            $('<td />').text((i + 1) + '. Column').appendTo(headerRow);
541        }
542        head.appendTo(table);
543
544        // create body
545        var body = $('<tbody></tbody>');
546
547        for (var i = 0; i < rows; i++) {
548            var row = $('<tr></tr>');
549            for (var j = 0; j < columns; j++) {
550                var rowContent = this.model[i];
551                var columnEntry = $.isArray(rowContent) ? rowContent[j] : rowContent;
552                var col = $('<td></td>');
553                $('<input type="text" name="' + i + '_' + j + '" />').val(columnEntry).appendTo(col);
554                col.appendTo(row);
555            }
556            if (this.options.editable) {
557                $('<button data-operation="Add" name="' + i + '" />').val(columnEntry).text('+').appendTo(col);
558                $('<button data-operation="Remove" name="' + i + '" />').val(columnEntry).text('-').appendTo(col);
559            }
560            row.appendTo(body);
561        }
562        body.appendTo(table);
563        table.appendTo(this.$el);
564    },
565    valueChanged: function (evt) {
566        var target = $(evt.target);
567        var value = target.val();
568        var index = target.getattr('name');
569        var splittedName = index.split('_');
570        var i = parseInt(splittedName[0]);
571        var j = parseInt(splittedName[1]);
572        if ($.isArray(this.model[i])) {
573            this.model[i][j] = parseFloat(value);
574        } else {
575            this.model[i] = parseFloat(value);
576        }
577    },
578    addClicked: function (evt) {
579        var target = $(evt.target);
580        var index = target.attr('name');
581        var i = parseInt(index);
582        if ($.isArray(this.model[i])) {
583            var cpy = [];
584            for (var j = 0; j < this.model[i].length; j++) {
585                cpy.push(this.model[i][j]);
586            }
587            this.model.splice(i, 0, cpy);
588        } else {
589            this.model.splice(i, 0, this.model[i]);
590        }
591        // render after model changes
592        this.render();
593    },
594    removeClicked: function (evt) {
595        var target = $(evt.target);
596        var index = target.attr('name');
597        var i = parseInt(index);
598        this.model.splice(i, 1);
599        // render after model changes
600        this.render();
601    }
602});
603
604var OptionSelectionView = Backbone.View.extend({
605    events: {
606        'change': 'checked'
607    },
608    render: function () {
609        this.$el.empty();
610
611        for (var i = 0; i < this.model.length; i++) {
612            $(_.template($('#checkbox_template').html(), { name: this.model[i], text: this.model[i] })).appendTo(this.$el);
613        }
614    },
615    checked: function (evt) {
616        var checked = $(evt.target).is(':checked');
617        var name = $(evt.target).attr('name')
618        this.trigger('changed', { name: name, checked: checked });
619    }
620});
621
622var VariationContentView = Backbone.View.extend({
623    render: function () {
624        var self = this;
625        if (this.model.Value === 'false' || this.model.Value === 'true' ||
626            this.model.Value === true || this.model.Value === false) {
627            $(_.template($('#variation_boolean_template').html(), {})).appendTo(this.$el);
628            this.model.Generated = [false, true];
629        }
630        else if (!isNaN(this.model.Value)) {
631            var elem = $(_.template($('#variation_number_template').html(), {}));
632            elem.appendTo(this.$el)
633            $('input[name="generate"]', elem).click(function (evt) { self.openGenerator(evt); });
634            if (!this.model.Generated) {
635                this.model.Generated = [0, 1, 2];
636            }
637            var tev = new TableEditView({ model: this.model.Generated, el: $('div div', this.$el), editable: true });
638            tev.render();
639        }
640        else if (this.model.Options) {
641            var osv = new OptionSelectionView({ model: this.model.Options, el: this.$el });
642            this.listenTo(osv, 'changed', function (evt) {
643                self.selectionChanged(evt);
644            });
645            if (!this.model.Generated)
646                this.model.Generated = [];
647            osv.render();
648            for (var i = 0; i < this.model.Generated.length; i++) {
649                $('input[name="' + this.model.Generated[i] + '"]', this.$el).attr('checked', 'checked');
650            }
651        }
652    },
653    openGenerator: function (evt) {
654        var self = this;
655        var generator = new GeneratorDialog();
656        this.listenTo(generator, 'success', function (data) {
657            self.doGenerate(data);
658        });
659        generator.render();
660    },
661    doGenerate: function (data) {
662        if (data.minimum > data.maximum) {
663            var tmp = data.minimum;
664            data.minimum = data.maximum;
665            data.maximum = tmp;
666        }
667        if (data.step < 0)
668            data.step *= -1;
669        if (data.step == 0)
670            data.step = 1;
671        this.model.Generated = _.range(data.minimum, data.maximum + 1, data.step);
672        var tev = new TableEditView({ model: this.model.Generated, el: $('div div', this.$el), editable: true });
673        tev.render();
674    },
675    selectionChanged: function (evt) {
676        if (!evt.checked) {
677            this.model.Generated = _.without(this.model.Generated, evt.name);
678        } else if (_.indexOf(this.model.Generated, evt.name) == -1) {
679            this.model.Generated.push(evt.name);
680        }
681    }
682});
683
684var GeneratorDialog = Backbone.View.extend({
685    render: function() {
686        var self = this;
687        var generator = $(_.template($('#variation_generator_template').html(),{}));
688        generator.dialog({
689            buttons: {
690                "Abort": function () {
691                    $(this).dialog("close");
692                },
693                "OK": function () {
694                    $(this).dialog("close");
695                    self.trigger("success", {
696                        minimum: parseInt($('input[name="minimum"]', generator).val()),
697                        maximum: parseInt($('input[name="maximum"]', generator).val()),
698                        step: parseFloat($('input[name="step"]', generator).val())
699                    });
700                }
701            }
702        });
703    }
704});
705
706var VariationEntryView = Backbone.View.extend({
707    render: function () {
708        var entryTemplate = _.template($('#variationentry_template').html(), this.model);
709        $(entryTemplate).appendTo(this.$el);       
710    }
711});
Note: See TracBrowser for help on using the repository browser.