<commit_before>//TODO revert to ROOT.draw for popup
//TODO investigate the use of a single call to the backend (should be faster but less flexible)

// callback function called to populate the page from the ROOT file
// called when ROOT is ready
function createmyGUI() {
    window.filename = QueryString.result;
    if (QueryString.render==="true") $("#renderLocal").bootstrapToggle('on');
    $.getJSON( "../data/ResultsAnalysisReport.json?salt="+makeid(), createGUI);
}

function makeid()
{
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for( var i=0; i < 5; i++ )
        text += possible.charAt(Math.floor(Math.random() * possible.length));

    return text;
}

// actually build the GUI after reading json
function createGUI(data) {
    // fill in the combo box
    loadAvailableResults(data);
    // set the filename: 1. from URL (above); 2. from the combo
    if(window.filename === undefined) {
        window.filename = $('#inputFile option:selected').text();
    }
    if(window.filename === "") return;
    // set data-id of the button to the result id
    $('#SAMADhi').attr("data-id", data.SelectedResults[$('#inputFile')[0].selectedIndex][0]["result_id"]);
    // open the ROOT file
    new JSROOT.TFile(window.filename, onFileOpen);
}

// called when the file is open
function onFileOpen(file) {
    window.fileStructure = parseDirectory(file,file,"",buildFileStructure);
    var activeDir = QueryString.dir;
    var histoGrid = $("#histoGrid");
    var renderLocally = [ "TH1F"  ];
    if($("#renderLocal").is(':checked')) {
	    renderLocally.push("TCanvas");
    }
    window.activeDownloads = 1000; // keep track of the callbacks - start with big number, subtract it at the end of the loop
    if(activeDir != undefined) {
        file.ReadDirectory(activeDir,function(dir) {
	    $("#pleasewait").modal();
            dirkeys = dir.fKeys;
            for(var i=0;i<dirkeys.length;i++) {
	        if($.inArray( dirkeys[i].fClassName, renderLocally)!== -1) {
		    window.activeDownloads++;
                    // prepare the html container
		    $('<div/>', {class:"col-lg-3 col-sm-4 col-xs-12"})
			.append($('<div/>',{class:"thumbnail well drawing", id:"drawing_"+dirkeys[i].fName}))
				.appendTo(histoGrid);
		    var element = $("#drawing_"+dirkeys[i].fName);
		    element.height(element.width()*0.75);
                    // draw in it
                    file.ReadObject(activeDir+"/"+dirkeys[i].fName, function(obj) {
			try {
                        	JSROOT.draw("drawing_"+obj.fName, obj, "");
			} catch(err) { } // ignore
			var svgElement = $("#drawing_"+obj.fName+" svg");
			var ratio = svgElement.height()/svgElement.width();
			var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
			svgElement.width(svgElement.parent().width()-padding_tot);
			svgElement.height(svgElement.width()*ratio);
			window.activeDownloads--;
	    		if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if last callback, remove the modal
                    });
		} else if($.inArray( dirkeys[i].fClassName, [ "TCanvas" ]  )!== -1) { // this can be used to all objects not handled by JSROOT
		        window.activeDownloads++;
			encodedfile = encodeURIComponent("/home/delaer/public_html/pages/"+window.filename)
			encodedcanvas = encodeURIComponent(activeDir+"/"+dirkeys[i].fName)
			$.ajax({
				url:       "http://localhost/cgi-bin/getCanvas.py?file="+encodedfile+"&canvas="+encodedcanvas,
				cache:     false,
				dataType:  "text",
				data:      { mode: 'info' },
				success:   function(result) {
					var element = $('<div/>', {class:"col-lg-3 col-sm-4 col-xs-12"})
						.append($('<div/>',{class:"thumbnail well drawing"})
							.append(result));
					var name = element.find(".drawing").comments().html()
					element.find(".drawing").attr("id","drawing_"+name);
					element.appendTo(histoGrid);
					var svgElement = element.find("svg");
					var ratio = svgElement.height()/svgElement.width();
					var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
					svgElement.width(svgElement.parent().width()-padding_tot);
					svgElement.height(svgElement.width()*ratio);
				window.activeDownloads--;
	    			if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if last callback, remove the modal
				}
			});
	       	}
            }
	    window.activeDownloads -= 1000; // done with the loop. From now on, this contains the #active callbacks.
	    if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if no more callback, remove the modal
        });
    }
}

// recursively parse the file to build the directory structure
var parseDirectoryCount = 0;
function parseDirectory(dir,file,path, callback) {
	var keys = dir.fKeys;
	var structure = [];
	var lastItem = false;
	for (i = 0; i < keys.length; i++) {
		lastItem = (i===keys.length-1);
		if(keys[i].fClassName=="TDirectory") {
	                parseDirectoryCount++;
			file.ReadObject(path+"/"+keys[i].fName, function(obj) {
				structure.push({ name:obj.dir_name, title:obj.fTitle, content:parseDirectory(obj,file,path+"/"+obj.dir_name,callback) });
				parseDirectoryCount--;
				if(parseDirectoryCount===0 && lastItem && callback) callback();
			});
		}
	}
	return structure;
}

// once the file has been parsed, build the HTML
function buildFileStructure() {
	window.fileStructure.sort(function(a,b){ return a.name>b.name; });
	$("#fileTree").html($("<li/>").html($("<a/>").attr('href','#').html("ROOT file")));
	generateHtmlTree($("#fileTree").find("li"),window.fileStructure);
	$('#fileTree').treed();
}

// recursive function to convert the structure into HTML
function generateHtmlTree(rootElement,tree){
	// create a <ul> and move in there
	rootElement.append($("<ul/>"));
	rootElement = rootElement.find("ul");
	// for each element, create a <li>.
	for (var i = 0; i < tree.length; i++) {
		// if there are subdirs, call recursively
		if(tree[i].content.length>0) {
			rootElement.append($("<li/>").html(tree[i].title));
			generateHtmlTree(rootElement.find("li").last(),tree[i].content);
		} else {
			rootElement.append($("<li/>").html($("<a/>").attr('href','?result='+encodeURIComponent(window.filename)+"&render="+$("#renderLocal").is(':checked')+'&dir='+tree[i].name).html(tree[i].title)));
		}
	}
}

// load the json with available results
function loadAvailableResults(data) {
	var results = data.SelectedResults;
	var inputs = $("#inputFile");
	for(var i=0;i<results.length;i++){
		inputs.append($("<option/>").html(results[i][1]));
		if(results[i][1]===window.filename) {
			$('#inputFile')[0].selectedIndex = i;
		}
	}
}

$(window).resize(resizeAll);
function resizeAll() {
	if($(".drawing").is(":visible")) {
		var refwidth = $(".drawing").width();
		var refheight = $(".drawing").height();
		var elements = $(".drawing svg");
		for (var i=0;i<elements.size();i++) {
			var ratio = elements.eq(i).width()/elements.eq(i).height();
			elements.eq(i).width(elements.eq(i).parent().width()-10);
			elements.eq(i).height(elements.eq(i).width()/ratio);
			elements.eq(i).parent().height(elements.eq(i).parent().width()/ratio);
		}
	}
	var modal = $("#modal_plot");
	if(modal.is(":visible")){
		var svgElement = $("#modal_plot >svg");
		var ratio = svgElement.height()/svgElement.width();
		var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
		svgElement.width(svgElement.parent().width()-padding_tot);
		svgElement.height(svgElement.width()*ratio);
	}
}

// register event handlers
$(function(){
     $('#histoGrid').on('dblclick',function(e) {
	 // check that we actually clicked on a plot and get its name
	 var hit = e.target.closest(".drawing");
	 if(hit==null) return;
	 var id = e.target.closest(".drawing").id;
	 var name = id.substring(8,id.length);
	 // prepare the modal window
	 // title
	 $('#myModalLabel').html(name);
	 $('#myModalLabel').attr("drawing",id);
	 // display
	 $("#histoZoom").toggle(400);
	 $("#histoGrid").toggle(400);
	 // wait for the modal to appear before drawing
	 window.setTimeout(drawModal,500);
     });
     $('#histoZoom').on('dblclick',function(e) {
	 // display
	 $("#histoZoom").toggle(400);
	 $("#histoGrid").toggle(400);
	 window.setTimeout(resizeAll,500);
     });
     $('#inputFile').on('change',function(e){
	 var newfilename = $("#inputFile option:selected").text();
	 window.location=window.location.origin+window.location.pathname+"?result="+encodeURIComponent(newfilename)+"&render="+$("#renderLocal").is(':checked');
     });
     $('#SAMADhi').click(function(){
        var id = $('#SAMADhi').attr("data-id");
	window.open("http://cp3.irmp.ucl.ac.be/~delaere/level2/SAMADhi/index.php?-table=result&-action=browse&-cursor=0&-skip=0&-limit=30&-mode=list&-recordid=result%3Fresult_id%3D"+id);
     });
     $("#SAMADhi").mouseup(function(){
	$(this).blur();
     });
     $("#renderLocal").change(function(){
       var value = $("#renderLocal").is(':checked');
       $("#fileTree a").each(function(){
        if(value) {
	  this.setAttribute("href",this.getAttribute("href").replace("render=false","render=true"));
	} else {
	  this.setAttribute("href",this.getAttribute("href").replace("render=true","render=false"));
        }
       });
     });
});

// draw the modal window
function drawModal() {
	// drawback of this method: we cannot interact with the zoomed plot
	var id = $('#myModalLabel').attr("drawing");
	$("#modal_plot").html("");
	$("#"+id+" >svg").clone().appendTo($("#modal_plot"));
	var svgElement = $("#modal_plot >svg");
	var ratio = svgElement.height()/svgElement.width();
	var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
	svgElement.width(svgElement.parent().width()-padding_tot);
	svgElement.height(svgElement.width()*ratio);
}

// decode the URL
var QueryString = function () {
  // This function is anonymous, is executed immediately and
  // the return value is assigned to QueryString!
  var query_string = {};
  var query = window.location.search.substring(1);
  var vars = query.split("&");
  for (var i=0;i<vars.length;i++) {
    var pair = vars[i].split("=");
        // If first entry with this name
    if (typeof query_string[pair[0]] === "undefined") {
      query_string[pair[0]] = decodeURIComponent(pair[1]);
        // If second entry with this name
    } else if (typeof query_string[pair[0]] === "string") {
      var arr = [ query_string[pair[0]],decodeURIComponent(pair[1]) ];
      query_string[pair[0]] = arr;
        // If third or later entry with this name
    } else {
      query_string[pair[0]].push(decodeURIComponent(pair[1]));
    }
  }
    return query_string;
}();
<commit_msg>removed useless data in ajax query<commit_after>//TODO revert to ROOT.draw for popup
//TODO investigate the use of a single call to the backend (should be faster but less flexible)

// callback function called to populate the page from the ROOT file
// called when ROOT is ready
function createmyGUI() {
    window.filename = QueryString.result;
    if (QueryString.render==="true") $("#renderLocal").bootstrapToggle('on');
    $.getJSON( "../data/ResultsAnalysisReport.json?salt="+makeid(), createGUI);
}

function makeid()
{
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for( var i=0; i < 5; i++ )
        text += possible.charAt(Math.floor(Math.random() * possible.length));

    return text;
}

// actually build the GUI after reading json
function createGUI(data) {
    // fill in the combo box
    loadAvailableResults(data);
    // set the filename: 1. from URL (above); 2. from the combo
    if(window.filename === undefined) {
        window.filename = $('#inputFile option:selected').text();
    }
    if(window.filename === "") return;
    // set data-id of the button to the result id
    $('#SAMADhi').attr("data-id", data.SelectedResults[$('#inputFile')[0].selectedIndex][0]["result_id"]);
    // open the ROOT file
    new JSROOT.TFile(window.filename, onFileOpen);
}

// called when the file is open
function onFileOpen(file) {
    window.fileStructure = parseDirectory(file,file,"",buildFileStructure);
    var activeDir = QueryString.dir;
    var histoGrid = $("#histoGrid");
    var renderLocally = [ "TH1F"  ];
    if($("#renderLocal").is(':checked')) {
	    renderLocally.push("TCanvas");
    }
    window.activeDownloads = 1000; // keep track of the callbacks - start with big number, subtract it at the end of the loop
    if(activeDir != undefined) {
        file.ReadDirectory(activeDir,function(dir) {
	    $("#pleasewait").modal();
            dirkeys = dir.fKeys;
            for(var i=0;i<dirkeys.length;i++) {
	        if($.inArray( dirkeys[i].fClassName, renderLocally)!== -1) {
		    window.activeDownloads++;
                    // prepare the html container
		    $('<div/>', {class:"col-lg-3 col-sm-4 col-xs-12"})
			.append($('<div/>',{class:"thumbnail well drawing", id:"drawing_"+dirkeys[i].fName}))
				.appendTo(histoGrid);
		    var element = $("#drawing_"+dirkeys[i].fName);
		    element.height(element.width()*0.75);
                    // draw in it
                    file.ReadObject(activeDir+"/"+dirkeys[i].fName, function(obj) {
			try {
                        	JSROOT.draw("drawing_"+obj.fName, obj, "");
			} catch(err) { } // ignore
			var svgElement = $("#drawing_"+obj.fName+" svg");
			var ratio = svgElement.height()/svgElement.width();
			var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
			svgElement.width(svgElement.parent().width()-padding_tot);
			svgElement.height(svgElement.width()*ratio);
			window.activeDownloads--;
	    		if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if last callback, remove the modal
                    });
		} else if($.inArray( dirkeys[i].fClassName, [ "TCanvas" ]  )!== -1) { // this can be used to all objects not handled by JSROOT
		        window.activeDownloads++;
			encodedfile = encodeURIComponent("/home/delaer/public_html/pages/"+window.filename)
			encodedcanvas = encodeURIComponent(activeDir+"/"+dirkeys[i].fName)
			$.ajax({
				url:       "http://localhost/cgi-bin/getCanvas.py?file="+encodedfile+"&canvas="+encodedcanvas,
				cache:     false,
				dataType:  "text",
				success:   function(result) {
					var element = $('<div/>', {class:"col-lg-3 col-sm-4 col-xs-12"})
						.append($('<div/>',{class:"thumbnail well drawing"})
							.append(result));
					var name = element.find(".drawing").comments().html()
					element.find(".drawing").attr("id","drawing_"+name);
					element.appendTo(histoGrid);
					var svgElement = element.find("svg");
					var ratio = svgElement.height()/svgElement.width();
					var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
					svgElement.width(svgElement.parent().width()-padding_tot);
					svgElement.height(svgElement.width()*ratio);
				window.activeDownloads--;
	    			if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if last callback, remove the modal
				}
			});
	       	}
            }
	    window.activeDownloads -= 1000; // done with the loop. From now on, this contains the #active callbacks.
	    if(window.activeDownloads==0) $("#pleasewait").modal("hide"); // if no more callback, remove the modal
        });
    }
}

// recursively parse the file to build the directory structure
var parseDirectoryCount = 0;
function parseDirectory(dir,file,path, callback) {
	var keys = dir.fKeys;
	var structure = [];
	var lastItem = false;
	for (i = 0; i < keys.length; i++) {
		lastItem = (i===keys.length-1);
		if(keys[i].fClassName=="TDirectory") {
	                parseDirectoryCount++;
			file.ReadObject(path+"/"+keys[i].fName, function(obj) {
				structure.push({ name:obj.dir_name, title:obj.fTitle, content:parseDirectory(obj,file,path+"/"+obj.dir_name,callback) });
				parseDirectoryCount--;
				if(parseDirectoryCount===0 && lastItem && callback) callback();
			});
		}
	}
	return structure;
}

// once the file has been parsed, build the HTML
function buildFileStructure() {
	window.fileStructure.sort(function(a,b){ return a.name>b.name; });
	$("#fileTree").html($("<li/>").html($("<a/>").attr('href','#').html("ROOT file")));
	generateHtmlTree($("#fileTree").find("li"),window.fileStructure);
	$('#fileTree').treed();
}

// recursive function to convert the structure into HTML
function generateHtmlTree(rootElement,tree){
	// create a <ul> and move in there
	rootElement.append($("<ul/>"));
	rootElement = rootElement.find("ul");
	// for each element, create a <li>.
	for (var i = 0; i < tree.length; i++) {
		// if there are subdirs, call recursively
		if(tree[i].content.length>0) {
			rootElement.append($("<li/>").html(tree[i].title));
			generateHtmlTree(rootElement.find("li").last(),tree[i].content);
		} else {
			rootElement.append($("<li/>").html($("<a/>").attr('href','?result='+encodeURIComponent(window.filename)+"&render="+$("#renderLocal").is(':checked')+'&dir='+tree[i].name).html(tree[i].title)));
		}
	}
}

// load the json with available results
function loadAvailableResults(data) {
	var results = data.SelectedResults;
	var inputs = $("#inputFile");
	for(var i=0;i<results.length;i++){
		inputs.append($("<option/>").html(results[i][1]));
		if(results[i][1]===window.filename) {
			$('#inputFile')[0].selectedIndex = i;
		}
	}
}

$(window).resize(resizeAll);
function resizeAll() {
	if($(".drawing").is(":visible")) {
		var refwidth = $(".drawing").width();
		var refheight = $(".drawing").height();
		var elements = $(".drawing svg");
		for (var i=0;i<elements.size();i++) {
			var ratio = elements.eq(i).width()/elements.eq(i).height();
			elements.eq(i).width(elements.eq(i).parent().width()-10);
			elements.eq(i).height(elements.eq(i).width()/ratio);
			elements.eq(i).parent().height(elements.eq(i).parent().width()/ratio);
		}
	}
	var modal = $("#modal_plot");
	if(modal.is(":visible")){
		var svgElement = $("#modal_plot >svg");
		var ratio = svgElement.height()/svgElement.width();
		var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
		svgElement.width(svgElement.parent().width()-padding_tot);
		svgElement.height(svgElement.width()*ratio);
	}
}

// register event handlers
$(function(){
     $('#histoGrid').on('dblclick',function(e) {
	 // check that we actually clicked on a plot and get its name
	 var hit = e.target.closest(".drawing");
	 if(hit==null) return;
	 var id = e.target.closest(".drawing").id;
	 var name = id.substring(8,id.length);
	 // prepare the modal window
	 // title
	 $('#myModalLabel').html(name);
	 $('#myModalLabel').attr("drawing",id);
	 // display
	 $("#histoZoom").toggle(400);
	 $("#histoGrid").toggle(400);
	 // wait for the modal to appear before drawing
	 window.setTimeout(drawModal,500);
     });
     $('#histoZoom').on('dblclick',function(e) {
	 // display
	 $("#histoZoom").toggle(400);
	 $("#histoGrid").toggle(400);
	 window.setTimeout(resizeAll,500);
     });
     $('#inputFile').on('change',function(e){
	 var newfilename = $("#inputFile option:selected").text();
	 window.location=window.location.origin+window.location.pathname+"?result="+encodeURIComponent(newfilename)+"&render="+$("#renderLocal").is(':checked');
     });
     $('#SAMADhi').click(function(){
        var id = $('#SAMADhi').attr("data-id");
	window.open("http://cp3.irmp.ucl.ac.be/~delaere/level2/SAMADhi/index.php?-table=result&-action=browse&-cursor=0&-skip=0&-limit=30&-mode=list&-recordid=result%3Fresult_id%3D"+id);
     });
     $("#SAMADhi").mouseup(function(){
	$(this).blur();
     });
     $("#renderLocal").change(function(){
       var value = $("#renderLocal").is(':checked');
       $("#fileTree a").each(function(){
        if(value) {
	  this.setAttribute("href",this.getAttribute("href").replace("render=false","render=true"));
	} else {
	  this.setAttribute("href",this.getAttribute("href").replace("render=true","render=false"));
        }
       });
     });
});

// draw the modal window
function drawModal() {
	// drawback of this method: we cannot interact with the zoomed plot
	var id = $('#myModalLabel').attr("drawing");
	$("#modal_plot").html("");
	$("#"+id+" >svg").clone().appendTo($("#modal_plot"));
	var svgElement = $("#modal_plot >svg");
	var ratio = svgElement.height()/svgElement.width();
	var padding_tot = Number(svgElement.parent().css("padding-left").replace("px",""))+Number(svgElement.parent().css("padding-right").replace("px",""));
	svgElement.width(svgElement.parent().width()-padding_tot);
	svgElement.height(svgElement.width()*ratio);
}

// decode the URL
var QueryString = function () {
  // This function is anonymous, is executed immediately and
  // the return value is assigned to QueryString!
  var query_string = {};
  var query = window.location.search.substring(1);
  var vars = query.split("&");
  for (var i=0;i<vars.length;i++) {
    var pair = vars[i].split("=");
        // If first entry with this name
    if (typeof query_string[pair[0]] === "undefined") {
      query_string[pair[0]] = decodeURIComponent(pair[1]);
        // If second entry with this name
    } else if (typeof query_string[pair[0]] === "string") {
      var arr = [ query_string[pair[0]],decodeURIComponent(pair[1]) ];
      query_string[pair[0]] = arr;
        // If third or later entry with this name
    } else {
      query_string[pair[0]].push(decodeURIComponent(pair[1]));
    }
  }
    return query_string;
}();
<|endoftext|>


<commit_before>var assert = require('assert');
var should = require('should');
var fs     = require('fs');
var mosto  = require('../mosto');
var melted = require('melted-node');
var _      = require('underscore');
var seed   = require('seed-random');
var mbc    = require('mbc-common');
var Media  = require('mbc-common/models/Media');
var Q      = require('q');
var moment = require('moment');
var helper = require('./media_helpers.js');
var uuid   = require('node-uuid');

describe.only("Mosto functional test", function() {
    /*
     * arrancar sin playlists
     ** ver negro
     */
    this.timeout(5000);
    var self = this;
    // use a random seed
    self.start_seed = seed()();
    self.rand = seed(self.start_seed);

    self.create_playlist = function(medias) {
        medias = medias || [];
        var mediamodels = [];
        medias.forEach(function(media) {
            mediamodels.push( new Media.Piece(media.toJSON()) );
        });
        return new Media.List({
            models: mediamodels,
        });
    };

    self.setup_playlists = function(start_time) {
        /*
         * sets up playlists to be consecutive starting from start_time
         * and annotates the medias to keep track of start and end times
         *
         * returns a promise
         */
        var timewalk = moment(start_time);
        self.occurrences = [];
        var defer = Q.defer();
        var done = _.after(self.playlists.length, function(){
            defer.resolve()
        });
        var occcol = self.db.collection('scheds');
        var listcol = self.db.collection('lists');
        for( var i = 0 ; i < self.playlists.length ; i++ ) {
            var playlist = self.playlists[i];
            var occurrence = {
                start: timewalk.unix(),
            };
            var medias = playlist.get('collection');
            for( var j = 0 ; j < medias.length ; j++ ) {
                var media = medias.at(j);
                media.start_time = timewalk.valueOf();
                timewalk.add(helper.framesToMilliseconds(
                    media.get('durationraw'), media.get('fps')));
                media.end_time = timewalk.valueOf();
            }
            occurrence.end = timewalk.unix();
            var playlist_json = _.omit(playlist.toJSON(), 'collection');
            playlist_json.models = _.invoke(playlist_json.models, 'toJSON');
            playlist_json._id = uuid.v4();
            // I need to wrap this in order to keep the `playlist` variable
            (function(pl, oc) {
                listcol.insert(playlist_json, function(err, obj) {
                    obj = obj[0];
                    // update self.playlists
                    _.extend(pl, obj);
                    occurrence = new Media.Occurrence({
                        list: obj._id,
                        start: oc.start,
                        end: oc.end,
                        _id: uuid.v4(),
                    });
                    occcol.insert(occurrence.toJSON(), function(err, obj) {
                        var obj = obj[0];
                        self.occurrences.push(obj);
                        done();
                    });
                });
            })(playlist, occurrence);
        }
        return defer.promise;
    };
    self.clean_playlists = function(done) {
        // Cleans the playlists and occurrences from the database
        var ready = _.after(2, function(){ done(); });
        self.db.collection('scheds').drop(ready);
        self.db.collection('lists').drop(ready);
    }

    self.get_occurrence = function(time) {
        time = time || moment();
        return _.find(self.occurrences, function(pl) {
            return pl.start <= time.unix() && pl.end >= time.unix();
        });
    };

    self.get_playlist = function(time) {
        time = time || moment();
        var occurrence = self.get_occurrence(time);
        return _.find(self.playlists, function(pl) {
            return pl._id == occurrence.list;
        });
    };

    self.get_media = function(time) {
        time = time || moment();
        var playlist = self.get_playlist(time);

        return _.find(playlist.get('models'), function(me) {
            return me.start_time <= time && me.end_time >= time;
        });
    };

    self.publisher = mbc.pubsub();
    self.listener = mbc.pubsub();
    self.db = mbc.db();

    before(function() {
        self.melted = melted();
    });
    after(function() {
        delete self.melted;
    });
    describe('start without playlists', function() {
        before(function(done) {
            self.mosto = new mosto();
            self.mosto.once('playing', function() {
                done()});
            self.mosto.init();
        });
        after(function(done) {
            self.mosto.finish(function() {
                delete self.mosto;
                done();
            });
        });
        it('should show black', function(done) {
            var promise = self.melted.sendPromisedCommand('USTA U0', '202 OK');
            promise.then(function(result) {
                result = result.split('\r\n')[1].split(' ');
                var file = result[2];
                file.should.include('black_id');
            }).then(done).done();
        });
    });
    /*
     * arrancar con playlists
     ** ver que haya el play correspondiente
     ** ver que este en el frame correcto
     */
    describe('start with playlists', function() {
        self.playlist_count = _.randint(5, 10, self.rand());
        before(function() {
            self.medias = helper.getMBCMedia();
            self.playlists = [];
            for(var i = 0 ; i < self.playlist_count ; i++) {
                var playlist_length = _.randint(1, 10, self.rand());
                self.playlists.push(self.create_playlist(
                    _.draw(self.medias, playlist_length)));
            }
        });
        describe('starting now', function() {
            before(function(done) {
                self.db.dropDatabase(function(err, success) {
                    // let playlists start somewhere between now and 30
                    //  seconds ago
                    var setup = self.setup_playlists(moment(
                        moment() + _.randint(0, -30000)));
                    setup.then(function() {
                        self.mosto = new mosto();
                        self.mosto.once('playing', function() {
                            // send pubsub messages with new playlists
                            done();
                        });
                        self.mosto.init();
                    });
                });
            });
            after(function(done) {
                self.mosto.finish(function() {
                    delete self.mosto;
                    self.clean_playlists(done);
                });
            });
            it('should start the right clip', function(done) {
                var time = moment();
                var expected_occurrence = self.get_occurrence(time);
                var expected_media = self.get_media(time);

                var result = self.melted.sendPromisedCommand('USTA U0', '202 OK');
                result.then(function(val) {
                    var lines = val.split("\r\n");
                    lines[0].should.eql('202 OK');
                    var filename = lines[1].split(' ')[2];
                    filename.match(expected_occurrence._id + '-' + expected_media.get('_id')).should.be.ok;
                }).then(done).done();
            });
            it('should start on the right frame (within the second)', function(done){
                var time = moment();
                var expected_media = self.get_media(time);

                var result = self.melted.sendPromisedCommand('USTA U0', '202 OK');
                result.then(function(val) {
                    var lines = val.split("\r\n");
                    lines[0].should.eql('202 OK');
                    var splitted = lines[1].split(' ');
                    var frame = parseInt(splitted[3]);
                    var fps = parseInt(splitted[5]);
                    var elapsed = helper.framesToMilliseconds(frame, fps);
                    elapsed.should.be.approximately((time - expected_media.start_time).valueOf(), 1000);
                }).then(done).done();
            });
            /*
            ** borrar la playlist
            *** ver que no se rompa nada
            *** ver que se este pasando negro
            */
            describe('delete currently playing playlist', function() {
                it('should not break');
                it('should be playing blank clip');
            });
        });
        /*
         * arrancar un playlist empezado hace 5m
         */
        describe('starting 5m ago', function() {
            /*
            ** ver que el frame se mantenga sincronizado durante 10 segundos
            */
            it("should keep sync'ed for at least 10 secs");
            /*
            ** mover la playlist hacia atras 1m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist back', function() {
                it("should stay sync'ed");
            });
            /*
            ** mover la playlist hacia adelante 2m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist forward', function() {
                it("should stay sync'ed");
            });
        });
    });
    /*
     * pruebas de timespan
     */
    describe('timespan tests', function() {
        /*
        ** levantar mosto con una ventana de 1m de ancho
        ** que haya una playlist de 2m empezando now
        ** que haya otra playlist de 30s empezando t+2m
        ** que haya otra playlist de 30s empezando en t+2m30s
        ** al start chequear que solo este la 1er playlist
        ** al 1m05 chequear que la 2da playlist este cargada
        */
        describe("start mosto with a 2m timeSpan", function() {
            it('should have a 2m timeSpan');
            describe('playlists longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[1m, 1m, 1m], 1m:[30s, 30s], 1m:[1m]]', function() {
                    it('should start with a 3m playlist starting now');
                    it('should start with a playlist 1m long starting in t+3m');
                    it('should start with a playlist 1m long starting in t+4m');
                    describe('at startup', function() {
                        it('should load only the first playlist at start');
                    });
                    describe('after 1m05s', function() {
                        it('should have loaded the 2nd playlist');
                    });
                });
            });
            describe('clips longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[3m], 1m[30s, 30s], 1m:[1m]]', function() {
                    it('should preload at least one clip');
                });
            });
        });
    });
    /*
     * idem anterior con una playlist de un solo clip
     ** si tengo un clip (playlist?) mas largo que la ventana, deberia haber n+1 cargados
     */
    describe("modify melted test", function() {
        it("should rectify things");
    });
});
<commit_msg>method to delete an occurrence<commit_after>var assert = require('assert');
var should = require('should');
var fs     = require('fs');
var mosto  = require('../mosto');
var melted = require('melted-node');
var _      = require('underscore');
var seed   = require('seed-random');
var mbc    = require('mbc-common');
var Media  = require('mbc-common/models/Media');
var Q      = require('q');
var moment = require('moment');
var helper = require('./media_helpers.js');
var uuid   = require('node-uuid');

describe.only("Mosto functional test", function() {
    /*
     * arrancar sin playlists
     ** ver negro
     */
    this.timeout(5000);
    var self = this;
    // use a random seed
    self.start_seed = seed()();
    self.rand = seed(self.start_seed);

    self.create_playlist = function(medias) {
        medias = medias || [];
        var mediamodels = [];
        medias.forEach(function(media) {
            mediamodels.push( new Media.Piece(media.toJSON()) );
        });
        return new Media.List({
            models: mediamodels,
        });
    };

    self.setup_playlists = function(start_time) {
        /*
         * sets up playlists to be consecutive starting from start_time
         * and annotates the medias to keep track of start and end times
         *
         * returns a promise
         */
        var timewalk = moment(start_time);
        self.occurrences = [];
        var defer = Q.defer();
        var done = _.after(self.playlists.length, function(){
            defer.resolve()
        });
        var occcol = self.db.collection('scheds');
        var listcol = self.db.collection('lists');
        for( var i = 0 ; i < self.playlists.length ; i++ ) {
            var playlist = self.playlists[i];
            var occurrence = {
                start: timewalk.unix(),
            };
            var medias = playlist.get('collection');
            for( var j = 0 ; j < medias.length ; j++ ) {
                var media = medias.at(j);
                media.start_time = timewalk.valueOf();
                timewalk.add(helper.framesToMilliseconds(
                    media.get('durationraw'), media.get('fps')));
                media.end_time = timewalk.valueOf();
            }
            occurrence.end = timewalk.unix();
            var playlist_json = _.omit(playlist.toJSON(), 'collection');
            playlist_json.models = _.invoke(playlist_json.models, 'toJSON');
            playlist_json._id = uuid.v4();
            // I need to wrap this in order to keep the `playlist` variable
            (function(pl, oc) {
                listcol.insert(playlist_json, function(err, obj) {
                    obj = obj[0];
                    // update self.playlists
                    _.extend(pl, obj);
                    occurrence = new Media.Occurrence({
                        list: obj._id,
                        start: oc.start,
                        end: oc.end,
                        _id: uuid.v4(),
                    });
                    occcol.insert(occurrence.toJSON(), function(err, obj) {
                        var obj = obj[0];
                        self.occurrences.push(obj);
                        done();
                    });
                });
            })(playlist, occurrence);
        }
        return defer.promise;
    };

    self.clean_playlists = function(done) {
        // Cleans the playlists and occurrences from the database
        var ready = _.after(2, function(){ done(); });
        self.db.collection('scheds').drop(ready);
        self.db.collection('lists').drop(ready);
    }

    self.delete_occurrence = function(occurrence, done) {
        var occurrences = self.db.collection('scheds');
        occurrences.remove({ _id: occurrence._id }, function(err, obj) {
            self.listener.once('message', function(chan, msg) {
                if( chan == 'schedbackend.delete' )
                    done();
            });
            self.listener.subscribe('schedbackend.delete');
            self.publisher.publishJSON('schedbackend.delete', { model: occurrence });
        });
    };

    self.get_occurrence = function(time) {
        time = time || moment();
        return _.find(self.occurrences, function(pl) {
            return pl.start <= time.unix() && pl.end >= time.unix();
        });
    };

    self.get_playlist = function(time) {
        time = time || moment();
        var occurrence = self.get_occurrence(time);
        return _.find(self.playlists, function(pl) {
            return pl._id == occurrence.list;
        });
    };

    self.get_media = function(time) {
        time = time || moment();
        var playlist = self.get_playlist(time);

        return _.find(playlist.get('models'), function(me) {
            return me.start_time <= time && me.end_time >= time;
        });
    };

    self.publisher = mbc.pubsub();
    self.listener = mbc.pubsub();
    self.db = mbc.db();

    before(function() {
        self.melted = melted();
    });
    after(function() {
        delete self.melted;
    });
    describe('start without playlists', function() {
        before(function(done) {
            self.mosto = new mosto();
            self.mosto.once('playing', function() {
                done()});
            self.mosto.init();
        });
        after(function(done) {
            self.mosto.finish(function() {
                delete self.mosto;
                done();
            });
        });
        it('should show black', function(done) {
            var promise = self.melted.sendPromisedCommand('USTA U0', '202 OK');
            promise.then(function(result) {
                result = result.split('\r\n')[1].split(' ');
                var file = result[2];
                file.should.include('black_id');
            }).then(done).done();
        });
    });
    /*
     * arrancar con playlists
     ** ver que haya el play correspondiente
     ** ver que este en el frame correcto
     */
    describe('start with playlists', function() {
        self.playlist_count = _.randint(5, 10, self.rand());
        before(function() {
            self.medias = helper.getMBCMedia();
            self.playlists = [];
            for(var i = 0 ; i < self.playlist_count ; i++) {
                var playlist_length = _.randint(1, 10, self.rand());
                self.playlists.push(self.create_playlist(
                    _.draw(self.medias, playlist_length)));
            }
        });
        describe('starting now', function() {
            before(function(done) {
                self.db.dropDatabase(function(err, success) {
                    // let playlists start somewhere between now and 30
                    //  seconds ago
                    var setup = self.setup_playlists(moment(
                        moment() + _.randint(0, -30000)));
                    setup.then(function() {
                        self.mosto = new mosto();
                        self.mosto.once('playing', function() {
                            // send pubsub messages with new playlists
                            done();
                        });
                        self.mosto.init();
                    });
                });
            });
            after(function(done) {
                self.mosto.finish(function() {
                    delete self.mosto;
                    self.clean_playlists(done);
                });
            });
            it('should start the right clip', function(done) {
                var time = moment();
                var expected_occurrence = self.get_occurrence(time);
                var expected_media = self.get_media(time);

                var result = self.melted.sendPromisedCommand('USTA U0', '202 OK');
                result.then(function(val) {
                    var lines = val.split("\r\n");
                    lines[0].should.eql('202 OK');
                    var filename = lines[1].split(' ')[2];
                    filename.match(expected_occurrence._id + '-' + expected_media.get('_id')).should.be.ok;
                }).then(done).done();
            });
            it('should start on the right frame (within the second)', function(done){
                var time = moment();
                var expected_media = self.get_media(time);

                var result = self.melted.sendPromisedCommand('USTA U0', '202 OK');
                result.then(function(val) {
                    var lines = val.split("\r\n");
                    lines[0].should.eql('202 OK');
                    var splitted = lines[1].split(' ');
                    var frame = parseInt(splitted[3]);
                    var fps = parseInt(splitted[5]);
                    var elapsed = helper.framesToMilliseconds(frame, fps);
                    elapsed.should.be.approximately((time - expected_media.start_time).valueOf(), 1000);
                }).then(done).done();
            });
            /*
            ** borrar la playlist
            *** ver que no se rompa nada
            *** ver que se este pasando negro
            */
            describe('delete currently playing playlist', function() {
                it('should not break');
                it('should be playing blank clip');
            });
        });
        /*
         * arrancar un playlist empezado hace 5m
         */
        describe('starting 5m ago', function() {
            /*
            ** ver que el frame se mantenga sincronizado durante 10 segundos
            */
            it("should keep sync'ed for at least 10 secs");
            /*
            ** mover la playlist hacia atras 1m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist back', function() {
                it("should stay sync'ed");
            });
            /*
            ** mover la playlist hacia adelante 2m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist forward', function() {
                it("should stay sync'ed");
            });
        });
    });
    /*
     * pruebas de timespan
     */
    describe('timespan tests', function() {
        /*
        ** levantar mosto con una ventana de 1m de ancho
        ** que haya una playlist de 2m empezando now
        ** que haya otra playlist de 30s empezando t+2m
        ** que haya otra playlist de 30s empezando en t+2m30s
        ** al start chequear que solo este la 1er playlist
        ** al 1m05 chequear que la 2da playlist este cargada
        */
        describe("start mosto with a 2m timeSpan", function() {
            it('should have a 2m timeSpan');
            describe('playlists longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[1m, 1m, 1m], 1m:[30s, 30s], 1m:[1m]]', function() {
                    it('should start with a 3m playlist starting now');
                    it('should start with a playlist 1m long starting in t+3m');
                    it('should start with a playlist 1m long starting in t+4m');
                    describe('at startup', function() {
                        it('should load only the first playlist at start');
                    });
                    describe('after 1m05s', function() {
                        it('should have loaded the 2nd playlist');
                    });
                });
            });
            describe('clips longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[3m], 1m[30s, 30s], 1m:[1m]]', function() {
                    it('should preload at least one clip');
                });
            });
        });
    });
    /*
     * idem anterior con una playlist de un solo clip
     ** si tengo un clip (playlist?) mas largo que la ventana, deberia haber n+1 cargados
     */
    describe("modify melted test", function() {
        it("should rectify things");
    });
});
<|endoftext|>


<commit_before>/*global setImmediate:true*/
// node 0.8 compat
if (typeof setImmediate === 'undefined') {
    setImmediate = process.nextTick;
}

/**
 * @class Relation
 *
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
var _ = require('underscore'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    passError = require('passerror'),
    urlTools = require('url-tools'),
    query = require('../query');

/**
 * new Relation(options)
 * =====================
 *
 * Create a new Relation instance. For existing assets the
 * instantiation of relations happens automatically if you use the
 * `populate` transform. You only need to create relations manually
 * when you need to introduce new ones.
 *
 * Note that the base Relation class should be considered
 * abstract. Please instantiate the appropriate subclass.
 *
 * Options:
 *
 *  - `from` The source asset of the relation.
 *  - `to`   The target asset of the relation, or an asset configuration
 *           object if the target asset hasn't yet been resolved and created.
 */
function Relation(config) {
    if (config.hrefType) {
        this._hrefType = config.hrefType;
        delete config.hrefType;
    }
    _.extend(this, config);
    this.id = '' + _.uniqueId();
}

Relation.prototype = {
    /**
     * relation.from (Asset)
     * =====================
     *
     * The source asset of the relation.
     */

    /**
     * relation.to (Asset or asset config object)
     * ==========================================
     *
     * The target asset of the relation. If the relation hasn't yet
     * been resolved, it can also be a relative url string or an asset
     * configuration object.
     */

    /**
     * relation.href (getter/setter)
     * =============================
     *
     * Get or set the href of the relation. The relation must be
     * attached to an asset.
     *
     * What is actually retrieved or updated depends on the relation
     * type. For `HtmlImage` the `src` attribute of the HTML element
     * is changed, for `CssImport` the parsed representation of
     * the @import rule is updated, etc.
     *
     * Most of the time you don't need to think about this property,
     * as the href is automatically updated when the url of the source
     * or target asset is changed, or an intermediate asset is
     * inlined.
     *
     * @api public
     */

    /**
     * relation.refreshHref
     * ====================
     *
     * Update `href` of a relation to make sure it points at the
     * current url of its target asset.
     *
     * It's not necessary to call this function manually as long as
     * the source and target assets of the relation have only been
     * moved by having their `url` property changed (the recommended
     * way), but some transforms will need this after some low-level
     * surgery, such as attaching an existing relation to a different
     * asset.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    refreshHref: function () {
        // if (this.to.isInline) won't work because relation.to might be unresolved and thus not an Asset instance:
        if (this.to && this.to.url) {
            var hrefType = this.hrefType,
                href;
            if (hrefType === 'rootRelative') {
                href = urlTools.buildRootRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url, this.assetGraph && this.assetGraph.root);
            } else if (hrefType === 'relative') {
                href = urlTools.buildRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else if (hrefType === 'protocolRelative') {
                href = urlTools.buildProtocolRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else {
                // Absolute
                href = this.to.url;
            }
            if (this.href !== href) {
                this.href = href;
                this.from.markDirty();
            }
        }
        return this;
    },

    /**
     * relation.hrefType (getter/setter)
     * =================================
     *
     * Either 'absolute', 'rootRelative', 'protocolRelative', or 'relative'. Decides what "degree" of
     * relative url refreshHref tries to issue.
     */

    get hrefType() {
        if (!this._hrefType) {
            var href = this.href;
            if (/^\/\//.test(href)) {
                this._hrefType = 'protocolRelative';
            } else if (/^\//.test(href)) {
                this._hrefType = 'rootRelative';
            } else if (/^[a-z\+]+:/i.test(href)) {
                this._hrefType = 'absolute';
            } else {
                this._hrefType = 'relative';
            }
        }
        return this._hrefType;
    },

    set hrefType(hrefType) {
        if (hrefType !== this._hrefType) {
            this._hrefType = hrefType;
            this.refreshHref();
        }
    },

    /**
     * relation.inline()
     * =================
     *
     * Inline the relation. This is only supported by certain relation
     * types and will produce different results depending on the type
     * (`data:` url, inline script, inline stylesheet...).
     *
     * Will make a clone of the target asset if it has more incoming
     * relations than this one.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    inline: function () {
        if (this.to.incomingRelations.length !== 1) {
            // This isn't the only incoming relation to the asset, clone before inlining.
            this.to.clone(this);
        }
        if (!this.to.isInline) {
            if (this.assetGraph) {
                var affectedRelations = [].concat(this.assetGraph._objInBaseAssetPaths[this.to.id]);
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._unregisterBaseAssetPath();
                }, this);
                this.to.url = null;
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._registerBaseAssetPath();
                    affectedRelation.refreshHref();
                }, this);
            } else {
                this.to.url = null;
            }
        }
        return this;
    },

    /**
     * relation.isRelation (boolean)
     * =============================
     *
     * Property that's true for all relation instances. Avoids
     * reliance on the `instanceof` operator.
     */
    isRelation: true,

    /**
     * relation.attach(asset, position[, adjacentRelation])
     * ====================================================
     *
     * Attaches the relation to an asset.
     *
     * The ordering of certain relation types is significant
     * (`HtmlScript`, for instance), so it's important that the order
     * isn't scrambled in the indices. Therefore the caller must
     * explicitly specify a position at which to insert the object.
     *
     * @param {Asset} asset The asset to attach the relation to.
     * @param {String} position "first", "last", "before", or "after".
     * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is "before" or "after".
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    attach: function (asset, position, adjacentRelation) {
        this.from = asset;
        this.from.markDirty();
        if (this.from.assetGraph && !this.assetGraph) {
            this.from.assetGraph.addRelation(this, position, adjacentRelation);
        }
        if (this.to && this.to.url) {
            this.refreshHref();
        }
        return this;
    },

    /**
     * relation.detach()
     * =================
     *
     * Detaches the relation from the asset it is currently attached
     * to. If the relation is currently part of a graph, it will
     * removed from it.
     *
     * Detaching implies that the tag/statement/declaration
     * representing the relation is physically removed from the
     * referring asset. Not all relation types support this.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    detach: function () {
        this.from.markDirty();
        if (this.assetGraph) {
            this.assetGraph.removeRelation(this);
        }
        return this;
    },

    /**
     * relation.remove()
     * =================
     *
     * Removes the relation from the graph it's currently part
     * of. Doesn't detach the relation (compare with
     * `relation.detach()`).
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    remove: function () {
        if (!this.assetGraph) {
            throw new Error("relation.remove(): Not in a graph");
        }
        this.assetGraph.removeRelation(this);
        return this;
    },

    /**
     * relation.baseAssetQuery (Object)
     * ================================
     *
     * Subclass-specific query object used for finding the base asset
     * for the relation (the asset whose url should be the basis for
     * resolving the href of the relation). This is usually the first
     * non-inline asset, but for some relation types it's the first
     * Html document.
     *
     * You shouldn't need to worry about this.
     *
     * @api public
     */
    baseAssetQuery: {isInline: false},

    /**
     * relation.baseAsset (getter)
     * ===========================
     *
     * Find the asset from which the url of the relation is to be
     * resolved. This is usually the first non-inline containing
     * asset, but for some relation types it's the first Html ancestor
     * -- infamously `CssAlphaImageLoader` and `CssBehavior`, but also
     * `JavaScriptGetStaticUrl`.
     *
     * The relation doesn't have to be in the graph as long as the
     * source asset is, so this can be used during population of the
     * graph.
     *
     * @return {Asset} The base asset for the relation.
     * @api public
     */
    get baseAsset() {
        if (!this.from || ! this.from.assetGraph || !(this.from.id in this.from.assetGraph.idIndex)) {
            throw new Error("Relation.baseAsset getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        var baseAssetPath = this.from.baseAssetSubstitute ? this._computeBaseAssetPath()[0] : this.baseAssetPath;
        if (baseAssetPath) {
            return this.baseAssetPath[0];
        }
    },

    _computeBaseAssetPath: function () {
        var baseAssetMatcher = this.from.assetGraph.constructor.query.queryObjToMatcherFunction(this.baseAssetQuery),
            queue = [[this.from.baseAssetSubstitute || this.from]],
            seenAssets = {};
        // Reverse breadth-first search
        while (queue.length) {
            var assetPath = queue.shift();
            if (baseAssetMatcher(assetPath[0])) {
                return assetPath;
            }
            seenAssets[assetPath[0].id] = true;

            // Add all yet-unseen assets reachable via incoming relations to the end of the queue:
            assetPath[0].incomingRelations.forEach(function (incomingRelation) {
                var sourceAsset = incomingRelation.from.baseAssetSubstitute || incomingRelation.from;
                if (!(sourceAsset.id in seenAssets)) {
                    queue.push([sourceAsset, incomingRelation].concat(assetPath));
                }
            });
        }
    },

    /**
     * relation.baseAssetPath (getter)
     * ===============================
     *
     * Helper for the relation.baseAsset getter.
     *
     * @return {Array} The complete path to the base asset: [baseAsset, intermediateRelation, asset, ...]. For normal non-inline assets that will just be [baseAsset].
     * @api public
     */
    get baseAssetPath() {
        if (!this.from || !this.from.assetGraph) {
            throw new Error("Relation.baseAssetPath getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        if (!('_baseAssetPath' in this)) {
            this._baseAssetPath = this._computeBaseAssetPath() || null;
        }
        return this._baseAssetPath;
    },

    /**
     * relation._registerBaseAssetPath()
     * =================================
     *
     * Register the base asset path of the relation (save it in the
     * internal index).
     *
     * @return {Boolean} Whether the base asset path could be computed.
     * @api private
     */
    _registerBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._registerBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.push(this);
            return false;
        } else {
            this.baseAssetPath.forEach(function (obj) {
                this.assetGraph._objInBaseAssetPaths[obj.id].push(this);
            }, this);
            return true;
        }
    },

    resolve: function (cb) {
        if (!this.from.assetGraph) {
            throw new Error("relation.resolve(): Source asset not in a graph");
        }
        var baseAsset = this.baseAsset;
        if (baseAsset) {
            this.from.assetGraph.resolveAssetConfig(this.to, baseAsset.nonInlineAncestor.url, passError(cb, function (resolvedAssetConfigs) {
                if (!_.isArray(resolvedAssetConfigs)) {
                    resolvedAssetConfigs = [resolvedAssetConfigs];
                }
                if (resolvedAssetConfigs.length === 0) {
                    this.from.assetGraph.emit('warn', new Error('The relation ' + this.toString() + ' multiplied to nothing, containing asset: ' + this.from.urlOrDescription));
                    this.remove();
                    cb(null, []);
                } else if (resolvedAssetConfigs.length === 1) {
                    this.to = resolvedAssetConfigs[0];
                    this.refreshHref();
                    cb(null, [this]);
                } else {
                    var multipliedRelations = [];
                    resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                        var multipliedRelation = new this.constructor({to: resolvedAssetConfig});
                        multipliedRelation.attach(this.from, 'before', this);
                        multipliedRelations.push(multipliedRelation);
                    }, this);
                    this.detach();
                    cb(null, multipliedRelations);
                }
            }.bind(this)));
        } else {
            setImmediate(function () {
                cb(new Error('No base asset found'));
            });
        }
    },

    /**
     * relation._unregisterBaseAssetPath()
     * ===================================
     *
     * Remove the the base asset path of the relation from the
     * internal indices.
     *
     * @api private
     */
    _unregisterBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._unregisterBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.splice(this.assetGraph._relationsWithNoBaseAsset.indexOf(this), 1);
        } else {
            this.baseAssetPath.forEach(function (obj) {
                var indexInBaseAssetPathsForOtherObj = this.assetGraph._objInBaseAssetPaths[obj.id].indexOf(this);
                if (indexInBaseAssetPathsForOtherObj === -1) {
                    throw new Error('relation._unregisterBaseAssetPath: Assertion failure: Relation ' + this + ' is no longer in _objInBaseAssetPaths[' + obj + ']');
                }
                this.assetGraph._objInBaseAssetPaths[obj.id].splice(indexInBaseAssetPathsForOtherObj, 1);
            }, this);
        }
        delete this._baseAssetPath;
    },

    /**
     * relation.toString()
     * ===================
     *
     * Get a brief text containing the type, id of the relation. Will
     * also contain the `.toString()` of the relation's source and
     * target assets if available.
     *
     * @return {String} The string, eg. "[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"
     * @api public
     */
    toString: function () {
        return "[" + this.type + "/" + this.id + ": " + ((this.from && this.to) ? this.from.toString() + " => " + (this.to.isAsset ? this.to.toString() : this.to.url || this.to.type || '?') : "unattached") + "]";
    }
};

module.exports = Relation;
<commit_msg>Lint Relation<commit_after>/*global setImmediate:true*/
// node 0.8 compat
if (typeof setImmediate === 'undefined') {
    setImmediate = process.nextTick;
}

/**
 * @class Relation
 *
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
var _ = require('underscore'),
    passError = require('passerror'),
    urlTools = require('url-tools');

/**
 * new Relation(options)
 * =====================
 *
 * Create a new Relation instance. For existing assets the
 * instantiation of relations happens automatically if you use the
 * `populate` transform. You only need to create relations manually
 * when you need to introduce new ones.
 *
 * Note that the base Relation class should be considered
 * abstract. Please instantiate the appropriate subclass.
 *
 * Options:
 *
 *  - `from` The source asset of the relation.
 *  - `to`   The target asset of the relation, or an asset configuration
 *           object if the target asset hasn't yet been resolved and created.
 */
function Relation(config) {
    if (config.hrefType) {
        this._hrefType = config.hrefType;
        delete config.hrefType;
    }
    _.extend(this, config);
    this.id = '' + _.uniqueId();
}

Relation.prototype = {
    /**
     * relation.from (Asset)
     * =====================
     *
     * The source asset of the relation.
     */

    /**
     * relation.to (Asset or asset config object)
     * ==========================================
     *
     * The target asset of the relation. If the relation hasn't yet
     * been resolved, it can also be a relative url string or an asset
     * configuration object.
     */

    /**
     * relation.href (getter/setter)
     * =============================
     *
     * Get or set the href of the relation. The relation must be
     * attached to an asset.
     *
     * What is actually retrieved or updated depends on the relation
     * type. For `HtmlImage` the `src` attribute of the HTML element
     * is changed, for `CssImport` the parsed representation of
     * the @import rule is updated, etc.
     *
     * Most of the time you don't need to think about this property,
     * as the href is automatically updated when the url of the source
     * or target asset is changed, or an intermediate asset is
     * inlined.
     *
     * @api public
     */

    /**
     * relation.refreshHref
     * ====================
     *
     * Update `href` of a relation to make sure it points at the
     * current url of its target asset.
     *
     * It's not necessary to call this function manually as long as
     * the source and target assets of the relation have only been
     * moved by having their `url` property changed (the recommended
     * way), but some transforms will need this after some low-level
     * surgery, such as attaching an existing relation to a different
     * asset.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    refreshHref: function () {
        // if (this.to.isInline) won't work because relation.to might be unresolved and thus not an Asset instance:
        if (this.to && this.to.url) {
            var hrefType = this.hrefType,
                href;
            if (hrefType === 'rootRelative') {
                href = urlTools.buildRootRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url, this.assetGraph && this.assetGraph.root);
            } else if (hrefType === 'relative') {
                href = urlTools.buildRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else if (hrefType === 'protocolRelative') {
                href = urlTools.buildProtocolRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else {
                // Absolute
                href = this.to.url;
            }
            if (this.href !== href) {
                this.href = href;
                this.from.markDirty();
            }
        }
        return this;
    },

    /**
     * relation.hrefType (getter/setter)
     * =================================
     *
     * Either 'absolute', 'rootRelative', 'protocolRelative', or 'relative'. Decides what "degree" of
     * relative url refreshHref tries to issue.
     */

    get hrefType() {
        if (!this._hrefType) {
            var href = this.href;
            if (/^\/\//.test(href)) {
                this._hrefType = 'protocolRelative';
            } else if (/^\//.test(href)) {
                this._hrefType = 'rootRelative';
            } else if (/^[a-z\+]+:/i.test(href)) {
                this._hrefType = 'absolute';
            } else {
                this._hrefType = 'relative';
            }
        }
        return this._hrefType;
    },

    set hrefType(hrefType) {
        if (hrefType !== this._hrefType) {
            this._hrefType = hrefType;
            this.refreshHref();
        }
    },

    /**
     * relation.inline()
     * =================
     *
     * Inline the relation. This is only supported by certain relation
     * types and will produce different results depending on the type
     * (`data:` url, inline script, inline stylesheet...).
     *
     * Will make a clone of the target asset if it has more incoming
     * relations than this one.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    inline: function () {
        if (this.to.incomingRelations.length !== 1) {
            // This isn't the only incoming relation to the asset, clone before inlining.
            this.to.clone(this);
        }
        if (!this.to.isInline) {
            if (this.assetGraph) {
                var affectedRelations = [].concat(this.assetGraph._objInBaseAssetPaths[this.to.id]);
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._unregisterBaseAssetPath();
                }, this);
                this.to.url = null;
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._registerBaseAssetPath();
                    affectedRelation.refreshHref();
                }, this);
            } else {
                this.to.url = null;
            }
        }
        return this;
    },

    /**
     * relation.isRelation (boolean)
     * =============================
     *
     * Property that's true for all relation instances. Avoids
     * reliance on the `instanceof` operator.
     */
    isRelation: true,

    /**
     * relation.attach(asset, position[, adjacentRelation])
     * ====================================================
     *
     * Attaches the relation to an asset.
     *
     * The ordering of certain relation types is significant
     * (`HtmlScript`, for instance), so it's important that the order
     * isn't scrambled in the indices. Therefore the caller must
     * explicitly specify a position at which to insert the object.
     *
     * @param {Asset} asset The asset to attach the relation to.
     * @param {String} position "first", "last", "before", or "after".
     * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is "before" or "after".
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    attach: function (asset, position, adjacentRelation) {
        this.from = asset;
        this.from.markDirty();
        if (this.from.assetGraph && !this.assetGraph) {
            this.from.assetGraph.addRelation(this, position, adjacentRelation);
        }
        if (this.to && this.to.url) {
            this.refreshHref();
        }
        return this;
    },

    /**
     * relation.detach()
     * =================
     *
     * Detaches the relation from the asset it is currently attached
     * to. If the relation is currently part of a graph, it will
     * removed from it.
     *
     * Detaching implies that the tag/statement/declaration
     * representing the relation is physically removed from the
     * referring asset. Not all relation types support this.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    detach: function () {
        this.from.markDirty();
        if (this.assetGraph) {
            this.assetGraph.removeRelation(this);
        }
        return this;
    },

    /**
     * relation.remove()
     * =================
     *
     * Removes the relation from the graph it's currently part
     * of. Doesn't detach the relation (compare with
     * `relation.detach()`).
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    remove: function () {
        if (!this.assetGraph) {
            throw new Error('relation.remove(): Not in a graph');
        }
        this.assetGraph.removeRelation(this);
        return this;
    },

    /**
     * relation.baseAssetQuery (Object)
     * ================================
     *
     * Subclass-specific query object used for finding the base asset
     * for the relation (the asset whose url should be the basis for
     * resolving the href of the relation). This is usually the first
     * non-inline asset, but for some relation types it's the first
     * Html document.
     *
     * You shouldn't need to worry about this.
     *
     * @api public
     */
    baseAssetQuery: {isInline: false},

    /**
     * relation.baseAsset (getter)
     * ===========================
     *
     * Find the asset from which the url of the relation is to be
     * resolved. This is usually the first non-inline containing
     * asset, but for some relation types it's the first Html ancestor
     * -- infamously `CssAlphaImageLoader` and `CssBehavior`, but also
     * `JavaScriptGetStaticUrl`.
     *
     * The relation doesn't have to be in the graph as long as the
     * source asset is, so this can be used during population of the
     * graph.
     *
     * @return {Asset} The base asset for the relation.
     * @api public
     */
    get baseAsset() {
        if (!this.from || ! this.from.assetGraph || !(this.from.id in this.from.assetGraph.idIndex)) {
            throw new Error('Relation.baseAsset getter: The "from" asset of the relation is not in the graph: ' + this.from);
        }
        var baseAssetPath = this.from.baseAssetSubstitute ? this._computeBaseAssetPath()[0] : this.baseAssetPath;
        if (baseAssetPath) {
            return this.baseAssetPath[0];
        }
    },

    _computeBaseAssetPath: function () {
        var baseAssetMatcher = this.from.assetGraph.constructor.query.queryObjToMatcherFunction(this.baseAssetQuery),
            queue = [[this.from.baseAssetSubstitute || this.from]],
            seenAssets = {};
        // Reverse breadth-first search
        while (queue.length) {
            var assetPath = queue.shift();
            if (baseAssetMatcher(assetPath[0])) {
                return assetPath;
            }
            seenAssets[assetPath[0].id] = true;

            // Add all yet-unseen assets reachable via incoming relations to the end of the queue:
            assetPath[0].incomingRelations.forEach(function (incomingRelation) {
                var sourceAsset = incomingRelation.from.baseAssetSubstitute || incomingRelation.from;
                if (!(sourceAsset.id in seenAssets)) {
                    queue.push([sourceAsset, incomingRelation].concat(assetPath));
                }
            });
        }
    },

    /**
     * relation.baseAssetPath (getter)
     * ===============================
     *
     * Helper for the relation.baseAsset getter.
     *
     * @return {Array} The complete path to the base asset: [baseAsset, intermediateRelation, asset, ...]. For normal non-inline assets that will just be [baseAsset].
     * @api public
     */
    get baseAssetPath() {
        if (!this.from || !this.from.assetGraph) {
            throw new Error('Relation.baseAssetPath getter: The 'from' asset of the relation is not in the graph: ' + this.from);
        }
        if (!('_baseAssetPath' in this)) {
            this._baseAssetPath = this._computeBaseAssetPath() || null;
        }
        return this._baseAssetPath;
    },

    /**
     * relation._registerBaseAssetPath()
     * =================================
     *
     * Register the base asset path of the relation (save it in the
     * internal index).
     *
     * @return {Boolean} Whether the base asset path could be computed.
     * @api private
     */
    _registerBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error('relation._registerBaseAssetPath(): Not in a graph');
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.push(this);
            return false;
        } else {
            this.baseAssetPath.forEach(function (obj) {
                this.assetGraph._objInBaseAssetPaths[obj.id].push(this);
            }, this);
            return true;
        }
    },

    resolve: function (cb) {
        if (!this.from.assetGraph) {
            throw new Error('relation.resolve(): Source asset not in a graph');
        }
        var baseAsset = this.baseAsset;
        if (baseAsset) {
            this.from.assetGraph.resolveAssetConfig(this.to, baseAsset.nonInlineAncestor.url, passError(cb, function (resolvedAssetConfigs) {
                if (!_.isArray(resolvedAssetConfigs)) {
                    resolvedAssetConfigs = [resolvedAssetConfigs];
                }
                if (resolvedAssetConfigs.length === 0) {
                    this.from.assetGraph.emit('warn', new Error('The relation ' + this.toString() + ' multiplied to nothing, containing asset: ' + this.from.urlOrDescription));
                    this.remove();
                    cb(null, []);
                } else if (resolvedAssetConfigs.length === 1) {
                    this.to = resolvedAssetConfigs[0];
                    this.refreshHref();
                    cb(null, [this]);
                } else {
                    var multipliedRelations = [];
                    resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                        var multipliedRelation = new this.constructor({to: resolvedAssetConfig});
                        multipliedRelation.attach(this.from, 'before', this);
                        multipliedRelations.push(multipliedRelation);
                    }, this);
                    this.detach();
                    cb(null, multipliedRelations);
                }
            }.bind(this)));
        } else {
            setImmediate(function () {
                cb(new Error('No base asset found'));
            });
        }
    },

    /**
     * relation._unregisterBaseAssetPath()
     * ===================================
     *
     * Remove the the base asset path of the relation from the
     * internal indices.
     *
     * @api private
     */
    _unregisterBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error('relation._unregisterBaseAssetPath(): Not in a graph');
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.splice(this.assetGraph._relationsWithNoBaseAsset.indexOf(this), 1);
        } else {
            this.baseAssetPath.forEach(function (obj) {
                var indexInBaseAssetPathsForOtherObj = this.assetGraph._objInBaseAssetPaths[obj.id].indexOf(this);
                if (indexInBaseAssetPathsForOtherObj === -1) {
                    throw new Error('relation._unregisterBaseAssetPath: Assertion failure: Relation ' + this + ' is no longer in _objInBaseAssetPaths[' + obj + ']');
                }
                this.assetGraph._objInBaseAssetPaths[obj.id].splice(indexInBaseAssetPathsForOtherObj, 1);
            }, this);
        }
        delete this._baseAssetPath;
    },

    /**
     * relation.toString()
     * ===================
     *
     * Get a brief text containing the type, id of the relation. Will
     * also contain the `.toString()` of the relation's source and
     * target assets if available.
     *
     * @return {String} The string, eg. "[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"
     * @api public
     */
    toString: function () {
        return '[' + this.type + '/' + this.id + ': ' + ((this.from && this.to) ? this.from.toString() + ' => ' + (this.to.isAsset ? this.to.toString() : this.to.url || this.to.type || '?') : 'unattached') + ']';
    }
};

module.exports = Relation;
<|endoftext|>


<commit_before>var express = require('express')
var path = require('path')

var app = express()

app.use('/', (req, res) => {
  var urlParts = req.url.split('/')
  console.log(urlParts)
  if(urlParts[1] === '') res.sendFile(path.join(__dirname + '/public/index.html'))
  if(urlParts[1] === 'assets') res.sendFile(path.join(__dirname + '/assets/'+urlParts[2]))
  else res.sendFile(path.join(__dirname + '/public/'+urlParts[1]))
})

app.listen(2000)
<commit_msg>server change<commit_after>var express = require('express')
var path = require('path')

var app = express()

app.use('/', (req, res) => {
  var urlParts = req.url.split('/')
  if(urlParts[1] === '' || urlParts[1] === ':1') res.sendFile(path.join(__dirname + '/public/index.html'))
  if(urlParts[1] === 'assets') res.sendFile(path.join(__dirname + '/assets/'+urlParts[2]))
  else res.sendFile(path.join(__dirname + '/public/'+urlParts[1]))
})

app.listen(2000)
<|endoftext|>


<commit_before>/* ==========================================================
 * sco.tab.js
 * http://github.com/terebentina/sco.js
 * ==========================================================
 * Copyright 2012 Dan Caragea.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */

/*jshint laxcomma:true, sub:true, browser:true, jquery:true, devel:true */
/*global define:true */

(function(factory) {
	"use strict";

    if (typeof define === 'function' && define.amd) {
        // Register as an anonymous AMD module:
        define([
            'jquery'
        ], factory);
    } else {
        // Browser globals:
        factory(window.jQuery);
    }
}(function($) {
	"use strict";

	function Tab($tabs, options) {
		var self = this;

		$.extend(self, {
			options: {},
			$tabs: null,
			$content: null,

			get: function(key) {
				if (typeof self.options[key] !== 'undefined') {
					return self.options[key];
				}

				return undefined;
			},

			set: function(map, save) {
				if (typeof save === 'undefined') {
					save = true;
				}
				$.each(map, function(k, v) {
					if (k === 'active') {
						if (v !== self.get('active')) {
							if (typeof self.options.onBeforeSelect == 'function') {
								self.options.onBeforeSelect.call(self, v);
							}
							var $header_children = self.$tabs.children();
							// remove the .active class from all tab headers and add .active to selected tab header
							$header_children.removeClass('active').eq(v).addClass('active');
							// hide all tab content and show only the selected one
							self.$content.hide().removeClass('active').eq(v).addClass('active').fadeIn(function() {
								if (typeof self.options.onAfterSelect == 'function') {
									self.options.onAfterSelect.call(self, v);
								}
							});
						}
					//} else if (k === 'href' && v !== '') {
					//	// @todo not working
					//	self.$content.find('.active').load(v);
					}

					if (save) {
						self.options[k] = v;
					}
				});
				return self;
			},

			_init: function() {
				self.$tabs = $tabs;
				var data = self.$tabs.data();
				if (typeof data.content === 'undefined') {
					return;
				}
				self.$content = $(data.content).children();

				data = $.extend({}, $.fn.scotab.defaults, data, options);

				self.$tabs.on('click.scotab', 'a', function(e) {
					var $this = $(this)
						,$my_li = $this.parents('li')
						,my_index = $my_li.index()
						,map = {active: my_index};

					// @todo not working
					//if ($this.attr('href').indexOf('#') !== 0) {
					//	e.preventDefault();
					//	map.href = $this.attr('href');
					//}

					self.set(map);
				});

				// allow plugins to add their own custom init code. Note that self.$tabs and self.$content is available in onInit()
				if (typeof data.onInit === 'function') {
					data.onInit.call(self, data);
					delete data.onInit;
				}
				if (typeof data.onBeforeSelect === 'function') {
					self.options.onBeforeSelect = data.onBeforeSelect;
					delete data.onBeforeSelect;
				}
				if (typeof data.onAfterSelect === 'function') {
					self.options.onAfterSelect = data.onAfterSelect;
					delete data.onAfterSelect;
				}

				// finally, set the initial values
				self.set(data);

				if ($.address) {
					$.address.externalChange(function(e) {
						var hash = '#' + e.value.slice(1);
						self.$tabs.find('a').each(function(i) {
							if ($(this).attr('href') === hash) {
								self.set({active: i});
								return false;
							}
						});
					}).history(true);
				}

				return self;
			}
		});

		self._init();
	}

	$.fn.scotab = function(options) {
		if (typeof options === 'undefined') {
			options = {};
		}
		this.each(function() {
			$(this).data("scotab", new Tab($(this), options));
		});

		return this;
	};

	$.fn.scotab.defaults = {
		active: 0
		,onBeforeSelect: null
		,onAfterSelect: null
		//,auto_advance: false	// false or milliseconds to wait till advancing
		//,easing: null
		//,href: ''
	};
}));
<commit_msg>tabs without $.address<commit_after>/* ==========================================================
 * sco.tab.js
 * http://github.com/terebentina/sco.js
 * ==========================================================
 * Copyright 2012 Dan Caragea.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */

/*jshint laxcomma:true, sub:true, browser:true, jquery:true, devel:true */
/*global define:true */

(function(factory) {
	"use strict";

    if (typeof define === 'function' && define.amd) {
        // Register as an anonymous AMD module:
        define([
            'jquery'
        ], factory);
    } else {
        // Browser globals:
        factory(window.jQuery);
    }
}(function($) {
	"use strict";

	function Tab($tabs, options) {
		var self = this;

		$.extend(self, {
			options: {},
			$tabs: null,
			$content: null,

			get: function(key) {
				if (typeof self.options[key] !== 'undefined') {
					return self.options[key];
				}

				return undefined;
			},

			set: function(map, save) {
				if (typeof save === 'undefined') {
					save = true;
				}
				$.each(map, function(k, v) {
					if (k === 'active') {
						if (v !== self.get('active')) {
							if (typeof self.options.onBeforeSelect == 'function') {
								self.options.onBeforeSelect.call(self, v);
							}
							var $header_children = self.$tabs.children();
							// remove the .active class from all tab headers and add .active to selected tab header
							$header_children.removeClass('active').eq(v).addClass('active');
							// hide all tab content and show only the selected one
							self.$content.hide().removeClass('active').eq(v).addClass('active').fadeIn(function() {
								if (typeof self.options.onAfterSelect == 'function') {
									self.options.onAfterSelect.call(self, v);
								}
							});
						}
					//} else if (k === 'href' && v !== '') {
					//	// @todo not working
					//	self.$content.find('.active').load(v);
					}

					if (save) {
						self.options[k] = v;
					}
				});
				return self;
			},

			_init: function() {
				self.$tabs = $tabs;
				var data = self.$tabs.data();
				if (typeof data.content === 'undefined') {
					return;
				}
				self.$content = $(data.content).children();

				data = $.extend({}, $.fn.scotab.defaults, data, options);

				self.$tabs.on('click.scotab', 'a', function(e) {
					var $this = $(this)
						,$my_li = $this.parents('li')
						,my_index = $my_li.index()
						,map = {active: my_index};

					if (!$.address) {
						e.preventDefault();
					}
					// @todo not working
					//if ($this.attr('href').indexOf('#') !== 0) {
					//	e.preventDefault();
					//	map.href = $this.attr('href');
					//}

					self.set(map);
				});

				// allow plugins to add their own custom init code. Note that self.$tabs and self.$content is available in onInit()
				if (typeof data.onInit === 'function') {
					data.onInit.call(self, data);
					delete data.onInit;
				}
				if (typeof data.onBeforeSelect === 'function') {
					self.options.onBeforeSelect = data.onBeforeSelect;
					delete data.onBeforeSelect;
				}
				if (typeof data.onAfterSelect === 'function') {
					self.options.onAfterSelect = data.onAfterSelect;
					delete data.onAfterSelect;
				}

				// finally, set the initial values
				self.set(data);

				if ($.address) {
					$.address.externalChange(function(e) {
						var hash = '#' + e.value.slice(1);
						self.$tabs.find('a').each(function(i) {
							if ($(this).attr('href') === hash) {
								self.set({active: i});
								return false;
							}
						});
					}).history(true);
				}

				return self;
			}
		});

		self._init();
	}

	$.fn.scotab = function(options) {
		if (typeof options === 'undefined') {
			options = {};
		}
		this.each(function() {
			$(this).data("scotab", new Tab($(this), options));
		});

		return this;
	};

	$.fn.scotab.defaults = {
		active: 0
		,onBeforeSelect: null
		,onAfterSelect: null
		//,auto_advance: false	// false or milliseconds to wait till advancing
		//,easing: null
		//,href: ''
	};
}));
<|endoftext|>