<commit_before>var app = app || {};

(function ($) {
    'use strict';

    var RssFeed = Backbone.Model.extend({
        url: '/rssfeeds'
    });

    var RssFeeds = Backbone.Collection.extend({
        model: RssFeed,
        url: '/rssfeeds'
    });

    var MyRefsRouter = Backbone.Router.extend({
        routes: {
            'rss': function () {
                $(".content").hide();
                $("#rss").show();
            },
            'marks': function () {
                $(".content").hide();
                $("#marks").show();
            }
        }
    });

    var appRouter = new MyRefsRouter();
    Backbone.history.start();
    appRouter.navigate('rss', {trigger: true});

    app.RssView = Backbone.View.extend({
        tagName: 'div',

        template: _.template('\

 \

 \
                        <a data-toggle="collapse" data-parent="#feeds" href="#<%=id%>"><%=title%> \

 \

\
                <div id="<%=id%>" class="panel-collapse collapse"> \

 \

 \

 \

'),

        render: function () {
            this.$el.addClass('panel panel-default');
            this.$el.html(this.template(this.model.attributes));
            return this.$el;
        }
    });

    app.RssListView = Backbone.View.extend({
        id: 'feeds',
        events: {
            "click .new-rss-btn": "addRssFeed"
        },
        initialize: function () {
            _.bindAll(this, 'render');
            this.$new_feed_input = $('#new_feed_input');
            this.$el.addClass('panel-group');

            this.model = new RssFeeds();
            this.model.bind('reset', this.render);
            this.model.fetch({reset: true});
        },

        render: function () {
            var self = this;
            $('#rssfeeds_row').find('div.col-lg-12').html(this.$el);
            _(this.model.models).each(function (rssFeed) {
                self.$el.append(new app.RssView({model: rssFeed}).render());
            }, this);
            self.updateFeeds();
            return this;
        },

        addRssFeed: function (e) {
            var url = this.$new_feed_input.val().trim();
            if (url !== '') {
                var newRssFeed = new RssFeed();
                newRssFeed.set({'url': url});
                this.model.create(newRssFeed);
                this.$new_feed_input.val('');
            }
        },

        updateFeeds: function () {
            var source = new EventSource('/updatefeeds');
            source.onmessage = function (msg) {
                var feedupdates = JSON.parse(msg.data);
                var entries = JSON.parse(feedupdates.entries);

                if (entries.length > 0) {
                    var $feed_title = $('#feeds').find('a[href="#' + feedupdates.id + '"]');
                    $feed_title.append("" + entries.length + "");
                    var $feedBody = $('#feeds').find('#' + feedupdates.id).find('.panel-body');
                    _(entries).each(function (entry) {
                        entry.feed_id = feedupdates.id;
                        localStorage[entry.link] = JSON.stringify(entry);
                        $feedBody.find('ul').append('
' + entry.title + '
');
                    });
                }
            };
            source.addEventListener('close', function () {
                $('#feeds').find('a.article').on('click', function (evt) {
                    evt.preventDefault();
                    $('#article_modal').find('.modal-title').html($(this).html());
                    var article = JSON.parse(localStorage[this.href]);
                    if (article.content === undefined) {
                        $('#article_modal').find('.modal-body').html('
' + article.summary + '
' + '
'  + article.link + '
');
                    } else {
                        $('#article_modal').find('.modal-body').html(article.content[0].value);
                    }

                    $('#article_modal').modal();

                    $.ajax({
                        url: '/article',
                        type: 'PUT',
                        data: JSON.stringify({"url": article.link, "feed_id": article.feed_id})
                    });
                });
                console.log("closing feeds update");
                this.close();
            }, false);
        }
    });
    new app.RssListView();
})(jQuery);<commit_msg>added Article and ArticleView<commit_after>var app = app || {};

(function ($) {
    'use strict';

    var RssFeed = Backbone.Model.extend({
        url: '/rssfeeds'
    });

    var RssFeeds = Backbone.Collection.extend({
        model: RssFeed,
        url: '/rssfeeds'
    });

    var Article = Backbone.Model.extend({});

    var MyRefsRouter = Backbone.Router.extend({
        routes: {
            'rss': function () {
                $(".content").hide();
                $("#rss").show();
            },
            'marks': function () {
                $(".content").hide();
                $("#marks").show();
            }
        }
    });

    app.ArticleView = Backbone.View.extend({
        tagName: 'li',
        template: _.template('<a class="article" href="<%=link%>"><%=title%>'),
        render: function () {
            this.$el.html(this.template(this.model.attributes));
            return this.$el;
        }
    });

    var appRouter = new MyRefsRouter();
    Backbone.history.start();
    appRouter.navigate('rss', {trigger: true});

    app.RssView = Backbone.View.extend({
        tagName: 'div',

        template: _.template('\

 \

 \
                        <a data-toggle="collapse" data-parent="#feeds" href="#<%=id%>"><%=title%> \

 \

\
                <div id="<%=id%>" class="panel-collapse collapse"> \

 \

 \

 \

'),

        render: function () {
            this.$el.addClass('panel panel-default');
            this.$el.html(this.template(this.model.attributes));
            return this.$el;
        }
    });

    app.RssListView = Backbone.View.extend({
        id: 'feeds',
        events: {
            "click .new-rss-btn": "addRssFeed"
        },
        initialize: function () {
            _.bindAll(this, 'render');
            this.$new_feed_input = $('#new_feed_input');
            this.$el.addClass('panel-group');

            this.model = new RssFeeds();
            this.model.bind('reset', this.render);
            this.model.fetch({reset: true});
        },

        render: function () {
            var self = this;
            $('#rssfeeds_row').find('div.col-lg-12').html(this.$el);
            _(this.model.models).each(function (rssFeed) {
                self.$el.append(new app.RssView({model: rssFeed}).render());
            }, this);
            self.updateFeeds();
            return this;
        },

        addRssFeed: function (e) {
            var url = this.$new_feed_input.val().trim();
            if (url !== '') {
                var newRssFeed = new RssFeed();
                newRssFeed.set({'url': url});
                this.model.create(newRssFeed);
                this.$new_feed_input.val('');
            }
        },

        updateFeeds: function () {
            var source = new EventSource('/updatefeeds');
            source.onmessage = function (msg) {
                var feedupdates = JSON.parse(msg.data);
                var entries = JSON.parse(feedupdates.entries);

                if (entries.length > 0) {
                    var $feed_title = $('#feeds').find('a[href="#' + feedupdates.id + '"]');
                    $feed_title.append("" + entries.length + "");
                    var $feedBody = $('#feeds').find('#' + feedupdates.id).find('.panel-body');
                    _(entries).each(function (entry) {
                        entry.feed_id = feedupdates.id;
                        localStorage[entry.link] = JSON.stringify(entry);
                        $feedBody.find('ul').append(new app.ArticleView({model: new Article(entry)}).render());
                    });
                }
            };
            source.addEventListener('close', function () {
                $('#feeds').find('a.article').on('click', function (evt) {
                    evt.preventDefault();
                    $('#article_modal').find('.modal-title').html($(this).html());
                    var article = JSON.parse(localStorage[this.href]);
                    if (article.content === undefined) {
                        $('#article_modal').find('.modal-body').html('
' + article.summary + '
' + '
'  + article.link + '
');
                    } else {
                        $('#article_modal').find('.modal-body').html(article.content[0].value);
                    }

                    $('#article_modal').modal();

                    $.ajax({
                        url: '/article',
                        type: 'PUT',
                        data: JSON.stringify({"url": article.link, "feed_id": article.feed_id})
                    });
                });
                console.log("closing feeds update");
                this.close();
            }, false);
        }
    });
    new app.RssListView();
})(jQuery);<|endoftext|>


<commit_before>var chai 		= require('chai'),
	chaiHttp	= require('chai-http'),
	server 		= require('../server/app'),
	expect		= chai.expect,
	should 		= chai.should();

chai.use(chaiHttp);

describe('Search', function(){
	it('should return a 200 status code', function(done){
		chai.request(server)
		.get('/search')
		.end(function(err, res){
			res.should.have.status(200);
			done();
		})
	});

	it('should return a list of N results', function(done){
		var n = 5;
		chai.request(server)
		.get('/search')
		.query({type: 'artist', limit: n})
		.end(function(err, res){
			expect(res).to.have.lengthOf(5)
			done();
		});

	});
	it('should return no more than 20 results if no limit is specified', function(done){
		chai.request(server)
		.get('/search')
		.query({limit:''})
		.query({type: 'artist'})
		.end(function(err, res){
			res.should.have.length.below(21);
			done();
		})

	});

})<commit_msg>add test for /browse endpoint<commit_after>var chai 		= require('chai'),
	chaiHttp	= require('chai-http'),
	server 		= require('../server/app'),
	expect		= chai.expect,
	should 		= chai.should();

chai.use(chaiHttp);

describe('Search', function(){
	it('should return a 200 status code', function(done){
		chai.request(server)
		.get('/search')
		.end(function(err, res){
			res.should.have.status(200);
			done();
		})
	});

	it('should return a list of N results', function(done){
		var n = 5;
		chai.request(server)
		.get('/search')
		.query({type: 'artist', limit: n})
		.end(function(err, res){
			expect(res).to.have.lengthOf(5)
			done();
		});

	});
	it('should return no more than 20 results if no limit is specified', function(done){
		chai.request(server)
		.get('/search')
		.query({limit:''})
		.query({type: 'artist'})
		.end(function(err, res){
			res.should.have.length.below(21);
			done();
		})

	});

});
describe('Browse', funciton(){
	it('should return a 200 status code', function(done){
		chai.request(server)
		.get('/browse')
		.end(function(err, res){
			res.should.have.status(200);
		})
	});
});<|endoftext|>


<commit_before>Curiosity.factory('context', function($rootScope, $location, elasticClient, elasticFunc, log){

	// initializing service's vars
	var contextObj = {};
	var contextDocumentType = "context-doc";
	var prevIdx = -2;
	var client = elasticClient.getClient(globalConf.confServer);

	var context = {}

	contextObj.info = {};
	contextObj.info.currentContext = {};
	contextObj.info.contextList = [];
	contextObj.info.contextIdx = -1;

	/**
	* @desc Load a context from its id
	* @param string contextId elasticsearch context's id
	*/
	contextObj.loadContext = function(contextId) {
		if (prevIdx != contextObj.info.contextIdx && typeof(contextId) !== "undefined" && contextId != "") {
			var request = ejs.Request();
			var query = ejs.QueryStringQuery("_id:\"" + contextId + "\"");
			var filter = ejs.TypeFilter(contextDocumentType);

			request.query(query).filter(filter);
			client.search({index:globalConf.confIndex, body:request})
			.then(function(data) {
				context = data.hits.hits[0];
				if (typeof (context) === "undefined")  { 				// Context not found => default context loaded
					$rootScope.$broadcast("NoContext");
					log.log("Context : Error : context " + contextId + " not found", "danger");
				}
				else {													// Context found
					contextObj.info.currentContext = context._source;
					contextObj.setContextIdx();
					$location.search("context", contextId);
					prevIdx = contextObj.info.contextIdx;
					$rootScope.$broadcast("ContextLoaded");
				}
			},
			function (err) {
				log.log("Context : Error during context loading", "danger");
			});
		}
		else {
			$rootScope.$broadcast("NoContext"); 						// Undefined context id => default context
		}
	}

	/**
	* @desc send selected context, if its elasticsearch id is undefined then create a new document
	*/
	contextObj.sendContext = function() {
		if (typeof (context._id) === "undefined") {
			elasticFunc.sendNewDocument(client, globalConf.confIndex, contextDocumentType, contextObj.info.currentContext, contextObj.getContextList);
		}
		else {
			elasticFunc.sendDocument(client, globalConf.confIndex, contextDocumentType, contextObj.info.currentContext, context._id);
		}
	}

	/**
	* @desc create a new context from a name, if its name is alredy attributed then do nothing and set error attribut to true
	* @param string name context's name
	*/
	contextObj.newContext = function(name) {
		if (name != "") {
			context = {};
			contextObj.info.currentContext = {};
			contextObj.info.currentContext.contextName = name;
			var i = 0;
			while (i < contextObj.info.contextList.length) { 			// Check if a context alredy exist with the same name
				if (contextObj.info.contextList[i].fields.contextName == name) {
					log.log("Context : Context " + name + " already exists !!","danger");
					contextObj.info.error = true;
					return;
				}
				i++;
			}
			contextObj.info.error = false;
			log.log("Context : Context created locally", "success");
		}
		else {
			contextObj.info.error = true;
			log.log("Context : You have to name your new context", "danger");
		}
	}

	/**
	* @desc get module information from current context
	* @param string moduleName the module to find
	* @return module information
	*/
	contextObj.getModuleInformation = function(moduleName) {
		return (contextObj.info.currentContext[moduleName]);
	}

	/**
	* @desc set module information in current context
	* @param string moduleName the module to update
	* @param object value module information to store
	*/
	contextObj.setContextInformation = function(moduleName, value) {
		contextObj.info.currentContext[moduleName] = value;
	}

	/*
	* @desc delete current context from elasticsearch server and reinitialise current context object
	*/
	contextObj.deleteContext = function () {
		elasticFunc.deleteDocument(client, globalConf.confIndex, contextDocumentType, context._id);
		contextObj.info.currentContext = {};
		context = {};
	}

	/*
	* @desc retreive context list from elasticsearch server
	*/
	contextObj.getContextList = function() {
		var request = ejs.Request();
		var query = ejs.MatchAllQuery();
		var filter = ejs.TypeFilter(contextDocumentType);
		request.fields("contextName");
		request.size(100);
		request.query(query).filter(filter);
		client.search({index:globalConf.confIndex, body:request})
		.then(function(data) {
			contextObj.info.contextList = data.hits.hits;
			var i = 0;
			while (i < contextObj.info.contextList.length) {
				if (contextObj.info.contextList[i].fields.contextName instanceof Array)
					contextObj.info.contextList[i].fields.contextName = contextObj.info.contextList[i].fields.contextName[0];
				else
					break;
				i++;
			}
		},
		function (error) {
			log.log("Context : Unable to load context list", "danger");
		})
	}

	/*
	* @desc set context idx from current context
	*/
	contextObj.setContextIdx = function () {
		if (typeof (contextObj.info.currentContext.contextName) !== "undefined") {
			var i = 0;
			while (i < contextObj.info.contextList.length) {
				if (typeof (contextObj.info.contextList[i].fields) !== "undefined" &&
					contextObj.info.contextList[i].fields.contextName == contextObj.info.currentContext.contextName) {
					contextObj.info.contextIdx = i;
					break ;
				}
				i++;
			}
		}
	}

	/*
	* @desc send UpdateContext event to notifie each services to update their information
	*/
	contextObj.updateContext = function () {
		$rootScope.$broadcast("UpdateContext");
	}

	/*
	* @desc Update every attr of a reference object from a module with information stored in a context
	* @param string moduleName module's name where information has to be retreive
	* @param object obj object where to update attr
	*/
	contextObj.setModuleInformation = function (moduleName, obj) {
		var module = contextObj.getModuleInformation(moduleName);
		if (typeof(module) !== "undefined") {
			for (key in module) {
				obj[key] = module[key];
			}
		}
	}

	/**
	* @desc function calls when services is created, getContext list from elasticSearch server and set context from url if there is one
	*/
	contextObj.init = function() {
		contextObj.getContextList();
		var params = $location.search();
		if (typeof(params) != "undefined" && params.context) { 	// Context in url => get context from es server
			contextObj.loadContext(params.context);
		}
		else { 													// no context =>  Default context
			$rootScope.$broadcast("NoContext");
		}
	}

	return (contextObj);
})<commit_msg>Fix context with same name creation bug<commit_after>Curiosity.factory('context', function($rootScope, $location, elasticClient, elasticFunc, log){

	// initializing service's vars
	var contextObj = {};
	var contextDocumentType = "context-doc";
	var prevIdx = -2;
	var client = elasticClient.getClient(globalConf.confServer);

	var context = {}

	contextObj.info = {};
	contextObj.info.currentContext = {};
	contextObj.info.contextList = [];
	contextObj.info.contextIdx = -1;

	/**
	* @desc Load a context from its id
	* @param string contextId elasticsearch context's id
	*/
	contextObj.loadContext = function(contextId) {
		if (prevIdx != contextObj.info.contextIdx && typeof(contextId) !== "undefined" && contextId != "") {
			var request = ejs.Request();
			var query = ejs.QueryStringQuery("_id:\"" + contextId + "\"");
			var filter = ejs.TypeFilter(contextDocumentType);

			request.query(query).filter(filter);
			client.search({index:globalConf.confIndex, body:request})
			.then(function(data) {
				context = data.hits.hits[0];
				if (typeof (context) === "undefined")  { 				// Context not found => default context loaded
					$rootScope.$broadcast("NoContext");
					log.log("Context : Error : context " + contextId + " not found", "danger");
				}
				else {													// Context found
					contextObj.info.currentContext = context._source;
					contextObj.setContextIdx();
					$location.search("context", contextId);
					prevIdx = contextObj.info.contextIdx;
					$rootScope.$broadcast("ContextLoaded");
				}
			},
			function (err) {
				log.log("Context : Error during context loading", "danger");
			});
		}
		else {
			$rootScope.$broadcast("NoContext"); 						// Undefined context id => default context
		}
	}

	/**
	* @desc send selected context, if its elasticsearch id is undefined then create a new document
	*/
	contextObj.sendContext = function() {
		if (typeof (context._id) === "undefined") {
			elasticFunc.sendNewDocument(client, globalConf.confIndex, contextDocumentType, contextObj.info.currentContext, contextObj.getContextList);
		}
		else {
			elasticFunc.sendDocument(client, globalConf.confIndex, contextDocumentType, contextObj.info.currentContext, context._id);
		}
	}

	/**
	* @desc create a new context from a name, if its name is alredy attributed then do nothing and set error attribut to true
	* @param string name context's name
	*/
	contextObj.newContext = function(name) {
		if (name != "") {
			var i = 0;
			while (i < contextObj.info.contextList.length) { // Check if a context alredy exist with the same name
				if (contextObj.info.contextList[i].fields.contextName == name) {
					log.log("Context : Context " + name + " already exists !!","danger");
					contextObj.info.error = true;
					return;
				}
				i++;
			}
			context = {};
			contextObj.info.currentContext = {};
			contextObj.info.currentContext.contextName = name;
			contextObj.info.error = false;
			log.log("Context : Context created locally", "success");
		}
		else {
			contextObj.info.error = true;
			log.log("Context : You have to name your new context", "danger");
		}
	}

	/**
	* @desc get module information from current context
	* @param string moduleName the module to find
	* @return module information
	*/
	contextObj.getModuleInformation = function(moduleName) {
		return (contextObj.info.currentContext[moduleName]);
	}

	/**
	* @desc set module information in current context
	* @param string moduleName the module to update
	* @param object value module information to store
	*/
	contextObj.setContextInformation = function(moduleName, value) {
		contextObj.info.currentContext[moduleName] = value;
	}

	/*
	* @desc delete current context from elasticsearch server and reinitialise current context object
	*/
	contextObj.deleteContext = function () {
		elasticFunc.deleteDocument(client, globalConf.confIndex, contextDocumentType, context._id);
		contextObj.info.currentContext = {};
		context = {};
	}

	/*
	* @desc retreive context list from elasticsearch server
	*/
	contextObj.getContextList = function() {
		var request = ejs.Request();
		var query = ejs.MatchAllQuery();
		var filter = ejs.TypeFilter(contextDocumentType);
		request.fields("contextName");
		request.size(100);
		request.query(query).filter(filter);
		client.search({index:globalConf.confIndex, body:request})
		.then(function(data) {
			contextObj.info.contextList = data.hits.hits;
			var i = 0;
			while (i < contextObj.info.contextList.length) {
				if (contextObj.info.contextList[i].fields.contextName instanceof Array)
					contextObj.info.contextList[i].fields.contextName = contextObj.info.contextList[i].fields.contextName[0];
				else
					break;
				i++;
			}
		},
		function (error) {
			log.log("Context : Unable to load context list", "danger");
		})
	}

	/*
	* @desc set context idx from current context
	*/
	contextObj.setContextIdx = function () {
		if (typeof (contextObj.info.currentContext.contextName) !== "undefined") {
			var i = 0;
			while (i < contextObj.info.contextList.length) {
				if (typeof (contextObj.info.contextList[i].fields) !== "undefined" &&
					contextObj.info.contextList[i].fields.contextName == contextObj.info.currentContext.contextName) {
					contextObj.info.contextIdx = i;
					break ;
				}
				i++;
			}
		}
	}

	/*
	* @desc send UpdateContext event to notifie each services to update their information
	*/
	contextObj.updateContext = function () {
		$rootScope.$broadcast("UpdateContext");
	}

	/*
	* @desc Update every attr of a reference object from a module with information stored in a context
	* @param string moduleName module's name where information has to be retreive
	* @param object obj object where to update attr
	*/
	contextObj.setModuleInformation = function (moduleName, obj) {
		var module = contextObj.getModuleInformation(moduleName);
		if (typeof(module) !== "undefined") {
			for (key in module) {
				obj[key] = module[key];
			}
		}
	}

	/**
	* @desc function calls when services is created, getContext list from elasticSearch server and set context from url if there is one
	*/
	contextObj.init = function() {
		contextObj.getContextList();
		var params = $location.search();
		if (typeof(params) != "undefined" && params.context) { 	// Context in url => get context from es server
			contextObj.loadContext(params.context);
		}
		else { 													// no context =>  Default context
			$rootScope.$broadcast("NoContext");
		}
	}

	return (contextObj);
})<|endoftext|>


<commit_before>const test = require('tape');
const server = require('../src/server');

test('Check the index route', t => {
  const options = {
    method: 'GET',
    url: '/'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h1 header was found in the welcome.hbs response');
    t.end();
  });
});

test('Check /index.js', t => {
  const options = {
    method: 'GET',
    url: '/index.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/jquery.min.js', t => {
  const options = {
    method: 'GET',
    url: '/public/jquery.min.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/materialize/css/materialize.min.css', t => {
  const options = {
    method: 'GET',
    url: '/public/materialize/css/materialize.min.css'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/materialize/js/materialize.min.js', t => {
  const options = {
    method: 'GET',
    url: '/public/materialize/js/materialize.min.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /introduction', t => {
  const options = {
    method: 'GET',
    url: '/introduction'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The
 was found in the introduction.hbs response');
    t.end();
  });
});

test('Check /avatar', t => {
  const options = {
    method: 'GET',
    url: '/avatar'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the choose-an-avatar.hbs response');
    t.end();
  });
});

test('Check /eating', t => {
  const options = {
    method: 'GET',
    url: '/eating'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the eating.hbs response');
    t.end();
  });
});

test('Check /personality', t => {
  const options = {
    method: 'GET',
    url: '/personality'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the personality.hbs response');
    t.end();
  });
});

test('Check /sleep', t => {
  const options = {
    method: 'GET',
    url: '/sleep'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the sleep.hbs response');
    t.end();
  });
});

test('Check /feelings', t => {
  const options = {
    method: 'GET',
    url: '/feelings'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the feelings.hbs response');
    t.end();
  });
});

test('Check /hobbies', t => {
  const options = {
    method: 'GET',
    url: '/hobbies'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the hobbies.hbs response');
    t.end();
  });
});

test('Check /friends', t => {
  const options = {
    method: 'GET',
    url: '/friends'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h2 title was found in the friends.hbs response');
    t.end();
  });
});

test('Check /school', t => {
  const options = {
    method: 'GET',
    url: '/school'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h2 title was found in the school.hbs response');
    t.end();
  });
});

test('Check /finished', t => {
  const options = {
    method: 'GET',
    url: '/finished'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h1 header was found in the finished.hbs response');
    t.end();
  });
});
<commit_msg>Add test to check if buttons display correctly<commit_after>const test = require('tape');
const server = require('../src/server');

test('Check the index route', t => {
  const options = {
    method: 'GET',
    url: '/'
  };
  const data = {
    next: '/avatar'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h1 header was found in the welcome.hbs response');
    t.equal(response.payload.indexOf(prevButton), -1, 'The prev button user was not found in the welcome.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the welcome.hbs response');
    t.end();
  });
});

test('Check /index.js', t => {
  const options = {
    method: 'GET',
    url: '/index.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/jquery.min.js', t => {
  const options = {
    method: 'GET',
    url: '/public/jquery.min.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/materialize/css/materialize.min.css', t => {
  const options = {
    method: 'GET',
    url: '/public/materialize/css/materialize.min.css'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /public/materialize/js/materialize.min.js', t => {
  const options = {
    method: 'GET',
    url: '/public/materialize/js/materialize.min.js'
  };
  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.end();
  });
});

test('Check /avatar', t => {
  const options = {
    method: 'GET',
    url: '/avatar'
  };
  const data = {
    prev: '/',
    next: '/introduction'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the choose-an-avatar.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the choose-an-avatar.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the choose-an-avatar.hbs response');
    t.end();
  });
});

test('Check /introduction', t => {
  const options = {
    method: 'GET',
    url: '/introduction'
  };
  const data = {
    prev: '/avatar',
    next: '/eating'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The
 was found in the introduction.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the introduction.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the introduction.hbs response');
    t.end();
  });
});

test('Check /eating', t => {
  const options = {
    method: 'GET',
    url: '/eating'
  };
  const data = {
    prev: '/introduction',
    next: '/feelings'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the eating.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the eating.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the eating.hbs response');
    t.end();
  });
});

test('Check /feelings', t => {
  const options = {
    method: 'GET',
    url: '/feelings'
  };
  const data = {
    prev: '/eating',
    next: '/personality'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the feelings.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the feelings.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the feelings.hbs response');
    t.end();
  });
});

test('Check /personality', t => {
  const options = {
    method: 'GET',
    url: '/personality'
  };
  const data = {
    prev: '/feelings',
    next: '/hobbies'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the personality.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the personality.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the personality.hbs response');
    t.end();
  });
});

test('Check /hobbies', t => {
  const options = {
    method: 'GET',
    url: '/hobbies'
  };
  const data = {
    prev: '/personality',
    next: '/sleep'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the hobbies.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the hobbies.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the hobbies.hbs response');
    t.end();
  });
});

test('Check /sleep', t => {
  const options = {
    method: 'GET',
    url: '/sleep'
  };
  const data = {
    prev: '/hobbies',
    next: '/friends'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.ok(response.payload.indexOf('
') > -1, 'The h2 title was found in the sleep.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the sleep.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the sleep.hbs response');
    t.end();
  });
});

test('Check /friends', t => {
  const options = {
    method: 'GET',
    url: '/friends'
  };
  const data = {
    prev: '/sleep',
    next: '/school'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h2 title was found in the friends.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the friends.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the friends.hbs response');
    t.end();
  });
});

test('Check /school', t => {
  const options = {
    method: 'GET',
    url: '/school'
  };
  const data = {
    prev: '/friends',
    next: '/finished'
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h2 title was found in the school.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the school.hbs response');
    t.ok(response.payload.indexOf(nextButton) > -1, 'The next button user was found in the school.hbs response');
    t.end();
  });
});

test('Check /finished', t => {
  const options = {
    method: 'GET',
    url: '/finished'
  };
  const data = {
    prev: '/school',
  };
  const prevButton = `<a class="button button--prev" href="${data.prev}">Prev</a>`;
  const nextButton = `<a class="button button--next" href="${data.next}">Next</a>`;

  server.inject(options, response => {
    t.equal(response.statusCode, 200, 'You received a 200 status code');
    t.notEqual(response.payload.indexOf('
'), -1, 'The h1 header was found in the finished.hbs response');
    t.ok(response.payload.indexOf(prevButton) > -1, 'The prev button user was found in the finished.hbs response');
    t.equal(response.payload.indexOf(nextButton), -1, 'The next button user was found not in the finished.hbs response');
    t.end();
  });
});
<|endoftext|>


<commit_before>import React from 'react'
import { latLngBounds } from 'leaflet'
import polyline from '@mapbox/polyline'
import turfDistance from '@turf/distance'
import turfAlong from 'turf-along'

import ModeIcon from '../components/icons/mode-icon'

/**
 * @param  {string}  mode
 * @return {boolean}
 */

export const transitModes = ['TRAM', 'BUS', 'SUBWAY', 'FERRY', 'RAIL', 'GONDOLA']

export function isTransit (mode) {
  return transitModes.includes(mode) || mode === 'TRANSIT'
}

/**
 * @param  {string}  modesStr a comma-separated list of OTP modes
 * @return {boolean} whether any of the modes are transit modes
 */
export function hasTransit (modesStr) {
  let result = false
  modesStr.split(',').forEach(mode => {
    if (isTransit(mode)) result = true
  })
  return result
}

export function isWalk (mode) {
  mode = mode || this.get('mode')
  return mode === 'WALK'
}

export function isBicycle (mode) {
  mode = mode || this.get('mode')
  return mode === 'BICYCLE'
}

export function isBicycleRent (mode) {
  mode = mode || this.get('mode')
  return mode === 'BICYCLE_RENT'
}

export function isCar (mode) {
  mode = mode || this.get('mode')
  return mode === 'CAR'
}

export function isAccessMode (mode) {
  return isWalk(mode) || isBicycle(mode) || isBicycleRent(mode) || isCar(mode)
}

export function getMapColor (mode) {
  mode = mode || this.get('mode')
  if (mode === 'WALK') return '#444'
  if (mode === 'BICYCLE') return '#0073e5'
  if (mode === 'SUBWAY') return '#f00'
  if (mode === 'RAIL') return '#b00'
  if (mode === 'BUS') return '#080'
  if (mode === 'TRAM') return '#800'
  if (mode === 'FERRY') return '#008'
  if (mode === 'CAR') return '#444'
  return '#aaa'
}

export function getStepInstructions (step) {
  return `${step.relativeDirection} on ${step.streetName}`
}

export function getModeIcon (mode, customIcons) {
  if (customIcons && mode in customIcons) {
    return customIcons[mode]
  }
  return <ModeIcon mode={mode} />
}

export function getItineraryBounds (itinerary) {
  let coords = []
  itinerary.legs.forEach(leg => {
    const legCoords = polyline
      .toGeoJSON(leg.legGeometry.points)
      .coordinates.map(c => [c[1], c[0]])
    coords = [...coords, ...legCoords]
  })
  return latLngBounds(coords)
}

export function routeComparator (a, b) {
  let aComp, bComp
  const aInt = parseInt(a.shortName)
  const bInt = parseInt(b.shortName)
  if (!isNaN(aInt) && !isNaN(bInt)) {
    aComp = aInt
    bComp = bInt
  } else {
    aComp = a.shortName || a.longName
    bComp = b.shortName || b.longName
  }
  if (aComp < bComp) return -1
  if (aComp > bComp) return 1
  return 0
}

/* Returns an interpolated lat-lon at a specified distance along a leg */

export function legLocationAtDistance (leg, distance) {
  if (!leg.legGeometry) return null

  try {
    const line = polyline.toGeoJSON(leg.legGeometry.points)
    const pt = turfAlong(line, distance, 'meters')
    if (pt && pt.geometry && pt.geometry.coordinates) {
      return [
        pt.geometry.coordinates[1],
        pt.geometry.coordinates[0]
      ]
    }
  } catch (e) { }

  return null
}

/* Returns an interpolated elevation at a specified distance along a leg */

export function legElevationAtDistance (leg, distance) {
  // Iterate through the leg steps, constructing a combined profile for this leg
  let traversed = 0
  const ptArray = []
  for (let si = 0; si < leg.steps.length; si++) {
    const step = leg.steps[si]
    if (step.elevation && step.elevation.length > 0) {
      for (let ei = 0; ei < step.elevation.length; ei++) {
        const elevItem = step.elevation[ei]
        if (elevItem.first > step.length) continue
        ptArray.push({
          first: traversed + elevItem.first,
          second: elevItem.second
        })
      }
    }
    traversed += step.distance
  }

  // Iterate through the combined elevation profile
  traversed = 0
  for (let i = 1; i < ptArray.length; i++) {
    const elevDistanceSpan = ptArray[i].first - ptArray[i - 1].first
    if (distance >= traversed && distance <= traversed + elevDistanceSpan) {
      // Distance falls within this point and the previous one;
      // compute & return iterpolated elevation value
      const pct = (distance - traversed) / elevDistanceSpan
      const elevSpan = ptArray[i].second - ptArray[i - 1].second
      return ptArray[i - 1].second + elevSpan * pct
    }
    traversed += elevDistanceSpan
  }

  return null
}
<commit_msg>fix(util): Remove obsolete package import<commit_after>import React from 'react'
import { latLngBounds } from 'leaflet'
import polyline from '@mapbox/polyline'
import turfAlong from 'turf-along'

import ModeIcon from '../components/icons/mode-icon'

/**
 * @param  {string}  mode
 * @return {boolean}
 */

export const transitModes = ['TRAM', 'BUS', 'SUBWAY', 'FERRY', 'RAIL', 'GONDOLA']

export function isTransit (mode) {
  return transitModes.includes(mode) || mode === 'TRANSIT'
}

/**
 * @param  {string}  modesStr a comma-separated list of OTP modes
 * @return {boolean} whether any of the modes are transit modes
 */
export function hasTransit (modesStr) {
  let result = false
  modesStr.split(',').forEach(mode => {
    if (isTransit(mode)) result = true
  })
  return result
}

export function isWalk (mode) {
  mode = mode || this.get('mode')
  return mode === 'WALK'
}

export function isBicycle (mode) {
  mode = mode || this.get('mode')
  return mode === 'BICYCLE'
}

export function isBicycleRent (mode) {
  mode = mode || this.get('mode')
  return mode === 'BICYCLE_RENT'
}

export function isCar (mode) {
  mode = mode || this.get('mode')
  return mode === 'CAR'
}

export function isAccessMode (mode) {
  return isWalk(mode) || isBicycle(mode) || isBicycleRent(mode) || isCar(mode)
}

export function getMapColor (mode) {
  mode = mode || this.get('mode')
  if (mode === 'WALK') return '#444'
  if (mode === 'BICYCLE') return '#0073e5'
  if (mode === 'SUBWAY') return '#f00'
  if (mode === 'RAIL') return '#b00'
  if (mode === 'BUS') return '#080'
  if (mode === 'TRAM') return '#800'
  if (mode === 'FERRY') return '#008'
  if (mode === 'CAR') return '#444'
  return '#aaa'
}

export function getStepInstructions (step) {
  return `${step.relativeDirection} on ${step.streetName}`
}

export function getModeIcon (mode, customIcons) {
  if (customIcons && mode in customIcons) {
    return customIcons[mode]
  }
  return <ModeIcon mode={mode} />
}

export function getItineraryBounds (itinerary) {
  let coords = []
  itinerary.legs.forEach(leg => {
    const legCoords = polyline
      .toGeoJSON(leg.legGeometry.points)
      .coordinates.map(c => [c[1], c[0]])
    coords = [...coords, ...legCoords]
  })
  return latLngBounds(coords)
}

export function routeComparator (a, b) {
  let aComp, bComp
  const aInt = parseInt(a.shortName)
  const bInt = parseInt(b.shortName)
  if (!isNaN(aInt) && !isNaN(bInt)) {
    aComp = aInt
    bComp = bInt
  } else {
    aComp = a.shortName || a.longName
    bComp = b.shortName || b.longName
  }
  if (aComp < bComp) return -1
  if (aComp > bComp) return 1
  return 0
}

/* Returns an interpolated lat-lon at a specified distance along a leg */

export function legLocationAtDistance (leg, distance) {
  if (!leg.legGeometry) return null

  try {
    const line = polyline.toGeoJSON(leg.legGeometry.points)
    const pt = turfAlong(line, distance, 'meters')
    if (pt && pt.geometry && pt.geometry.coordinates) {
      return [
        pt.geometry.coordinates[1],
        pt.geometry.coordinates[0]
      ]
    }
  } catch (e) { }

  return null
}

/* Returns an interpolated elevation at a specified distance along a leg */

export function legElevationAtDistance (leg, distance) {
  // Iterate through the leg steps, constructing a combined profile for this leg
  let traversed = 0
  const ptArray = []
  for (let si = 0; si < leg.steps.length; si++) {
    const step = leg.steps[si]
    if (step.elevation && step.elevation.length > 0) {
      for (let ei = 0; ei < step.elevation.length; ei++) {
        const elevItem = step.elevation[ei]
        if (elevItem.first > step.length) continue
        ptArray.push({
          first: traversed + elevItem.first,
          second: elevItem.second
        })
      }
    }
    traversed += step.distance
  }

  // Iterate through the combined elevation profile
  traversed = 0
  for (let i = 1; i < ptArray.length; i++) {
    const elevDistanceSpan = ptArray[i].first - ptArray[i - 1].first
    if (distance >= traversed && distance <= traversed + elevDistanceSpan) {
      // Distance falls within this point and the previous one;
      // compute & return iterpolated elevation value
      const pct = (distance - traversed) / elevDistanceSpan
      const elevSpan = ptArray[i].second - ptArray[i - 1].second
      return ptArray[i - 1].second + elevSpan * pct
    }
    traversed += elevDistanceSpan
  }

  return null
}
<|endoftext|>


<commit_before>Kpcc.ArticlesController = Ember.ArrayController.extend({
    currentPage     : 0,
    limit           : 12,
    types           : "news,blogs",

    sortAscending   : false,
    sortProperties  : ['published_at'],

    // FIXME: This doesn't remove articles which have been taken out of
    // the API for some reason (deleted or unpublished).
    checkForNewRecords: function() {
        console.log("checking for new records");
        Kpcc.Article.find({
            page    : 1,
            limit   : this.get('limit'),
            type    : this.get('types')
        });
    },

    loadMore: function() {
        if(this.get('isLoading')) return;

        this.incrementProperty('currentPage');
        this.set('isLoading', true);

        var self = this;

        Kpcc.Article.find({
            page    : this.get('currentPage'),
            limit   : this.get('limit'),
            types   : this.get('types')

        }).then(function(){
            self.set('isLoading', false)
        });
    }
});
<commit_msg>Add segments to API call<commit_after>Kpcc.ArticlesController = Ember.ArrayController.extend({
    currentPage     : 0,
    limit           : 12,
    types           : "news,blogs,segments",

    sortAscending   : false,
    sortProperties  : ['published_at'],

    // FIXME: This doesn't remove articles which have been taken out of
    // the API for some reason (deleted or unpublished).
    checkForNewRecords: function() {
        console.log("checking for new records");
        Kpcc.Article.find({
            page    : 1,
            limit   : this.get('limit'),
            type    : this.get('types')
        });
    },

    loadMore: function() {
        if(this.get('isLoading')) return;

        this.incrementProperty('currentPage');
        this.set('isLoading', true);

        var self = this;

        Kpcc.Article.find({
            page    : this.get('currentPage'),
            limit   : this.get('limit'),
            types   : this.get('types')

        }).then(function(){
            self.set('isLoading', false)
        });
    }
});
<|endoftext|>


<commit_before>var extend = require('gextend');

/*
 * Route Metadata
 */
function RouteMetadata(mountpath, method, uri){
    this.uri = uri;
    this.mountpath = mountpath;
    this.method = method.toUpperCase();
    this.toString = function(){
        console.log(this.method, '\t', this.mountpath + this.uri);
    };
}

/*
 * App Metadata
 */
function AppMetadata(anden){
    this.name = anden.name;
    this.path = anden.app.path();
    this.routes = this.getRouters(anden);
    this.routesMeta = [];
}

AppMetadata.prototype.getRouters = function (anden){
    return [].concat(anden.routers, [anden.app._router]);
};

/*
 * Anden Metadata
 */
function AndenMetadata(anden){
    this.collect(anden);
}

AndenMetadata.prototype.reset = function(anden){
    this.meta = {};
    this.apps = {};
    this.apps[anden.name] = anden;
    this.apps = extend(this.apps, anden.subapps);
};

AndenMetadata.prototype.walkAllMetas = function(cb){
    Object.keys(this.meta).forEach(function(id){
        cb(this.meta[id]);
    }, this);
};

AndenMetadata.prototype.walkAllRoutes = function(cb){
    this.walkAllMetas(function(appmeta){
        appmeta.routesMeta.forEach(function(routemeta){
            cb(routemeta, appmeta);
        });
    });
};

AndenMetadata.prototype.collect = function(anden){
    this.reset(anden);
    var path, meta, routeMeta;

    Object.keys(this.apps).forEach(function(id){
        meta = new AppMetadata(this.apps[id]);

        meta.routes.forEach(function(route){
            route.stack.forEach(function(layer){
                layer.route && layer.route.stack.forEach(function(r){
                    meta.routesMeta.push(new RouteMetadata(meta.path, r.method, layer.route.path));
                });
            });
        });
        this.meta[meta.name] = meta;
    }, this);
};

exports.collectRouteMetameta = function (anden){
   return new AndenMetadata(anden);
};<commit_msg>UTILS: metadata shuffle<commit_after>var extend = require('gextend');

/////////////////////////////////////
/// AndenMetadata
/////////////////////////////////////
/*
 * Anden Metadata
 */
function AndenMetadata(anden){
    this.collect(anden);
}

AndenMetadata.prototype.reset = function(anden){
    this.meta = {};
    this.apps = {};
    this.apps[anden.name] = anden;
    this.apps = extend(this.apps, anden.subapps);
};

AndenMetadata.prototype.walkAllMetas = function(cb){
    Object.keys(this.meta).forEach(function(id){
        cb(this.meta[id]);
    }, this);
};

AndenMetadata.prototype.walkAllRoutes = function(cb){
    this.walkAllMetas(function(appmeta){
        appmeta.routesMeta.forEach(function(routemeta){
            cb(routemeta, appmeta);
        });
    });
};

AndenMetadata.prototype.collect = function(anden){
    this.reset(anden);
    var path, meta, routeMeta;

    Object.keys(this.apps).forEach(function(id){
        meta = new AppMetadata(this.apps[id]);

        meta.routes.forEach(function(route){
            route.stack.forEach(function(layer){
                layer.route && layer.route.stack.forEach(function(r){
                    meta.routesMeta.push(new RouteMetadata(meta.path, r.method, layer.route.path));
                });
            });
        });
        this.meta[meta.name] = meta;
    }, this);
};

/////////////////////////////////////
/// AppMetadata
/////////////////////////////////////
/*
 * App Metadata
 */
function AppMetadata(anden){
    this.name = anden.name;
    this.path = anden.app.path();
    this.routes = this.getRouters(anden);
    this.routesMeta = [];
}

AppMetadata.prototype.getRouters = function (anden){
    return [].concat(anden.routers, [anden.app._router]);
};

/////////////////////////////////////
/// RouteMetadata
/////////////////////////////////////
/*
 * Route Metadata
 */
function RouteMetadata(mountpath, method, uri){
    this.uri = uri;
    this.mountpath = mountpath;
    this.method = method.toUpperCase();
    this.toString = function(){
        console.log(this.method, '\t', this.mountpath + this.uri);
    };
}

exports.collectRouteMetameta = function (anden){
   return new AndenMetadata(anden);
};<|endoftext|>


<commit_before>// Copyright 2014-2018, University of Colorado Boulder

/**
 * Demonstrates vegas UI components.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var HSlider = require( 'SUN/HSlider' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LevelCompletedNode = require( 'VEGAS/LevelCompletedNode' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var ScoreboardBar = require( 'VEGAS/ScoreboardBar' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var vegas = require( 'VEGAS/vegas' );

  // constants
  var PERFECT_SCORE = 1000;

  /**
   * @constructor
   */
  function FiniteChallengesScreenView() {

    var self = this;
    ScreenView.call( this );

    var scoreProperty = new Property( 0 );

    // bar across the top
    var scoreboardBar = new ScoreboardBar(
      this.layoutBounds.width,
      new Property( 0 ), // challenge index
      new Property( 6 ), // challenges per level
      new Property( 0 ), // level
      scoreProperty, // score
      new Property( 0 ), // elapse time
      new BooleanProperty( true ), // timer enabled
      function() { console.log( 'Start Over' ); }, // callback for 'Start Over' button
      {
        font: new PhetFont( 20 ),
        leftMargin: 40, // visually aligned with left edge of challenge boxes
        rightMargin: 50, // visually aligned with right edge of challenge graph
        centerX: this.layoutBounds.centerX,
        top: this.layoutBounds.top
      } );
    this.addChild( scoreboardBar );

    // slider for testing score changes
    var scoreSlider = new HBox( {
      left: this.layoutBounds.left + 20,
      top: scoreboardBar.bottom + 30,
      children: [
        new Text( 'Score: ', { font: new PhetFont( 20 ) } ),
        new HSlider( scoreProperty, new Range( 0, PERFECT_SCORE ), {

          //TODO #66 remove this when scoreDisplay option is added to ScoreboardBar
          constrainValue: function( value ) {
            return Util.roundSymmetric( value );
          }
        } )
      ]
    } );
    this.addChild( scoreSlider );

    // LevelCompletedNode that cycles through score values when you press 'Continue' button
    var score = 0;
    var addLevelCompletedNode = function() {
      var maxScore = 12;
      var levelCompletedNode = new LevelCompletedNode( 7, score, maxScore, 4, true, 77, 74, true, function() {
        console.log( 'continue' );
        score++;
        if ( score > maxScore ) {
          score = 0;
        }
        levelCompletedNode.detach();
        addLevelCompletedNode();
      }, {
        center: self.layoutBounds.center
      } );
      self.addChild( levelCompletedNode );
    };
    addLevelCompletedNode();
  }

  vegas.register( 'FiniteChallengesScreenView', FiniteChallengesScreenView );

  return inherit( ScreenView, FiniteChallengesScreenView );
} );<commit_msg>clean up demo of LevelCompletedNode, https://github.com/phetsims/vegas/issues/66<commit_after>// Copyright 2014-2018, University of Colorado Boulder

/**
 * Demonstrates vegas UI components.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var HSlider = require( 'SUN/HSlider' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LevelCompletedNode = require( 'VEGAS/LevelCompletedNode' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' );
  var ScoreboardBar = require( 'VEGAS/ScoreboardBar' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var vegas = require( 'VEGAS/vegas' );

  // constants
  var PERFECT_SCORE = 10;

  /**
   * @constructor
   */
  function FiniteChallengesScreenView() {

    var self = this;
    ScreenView.call( this );

    var scoreProperty = new Property( 0 );

    // bar across the top
    var scoreboardBar = new ScoreboardBar(
      this.layoutBounds.width,
      new Property( 0 ), // challenge index
      new Property( 6 ), // challenges per level
      new Property( 0 ), // level
      scoreProperty, // score
      new Property( 0 ), // elapse time
      new BooleanProperty( true ), // timer enabled
      function() { console.log( 'Start Over' ); }, // callback for 'Start Over' button
      {
        font: new PhetFont( 20 ),
        leftMargin: 40, // visually aligned with left edge of challenge boxes
        rightMargin: 50, // visually aligned with right edge of challenge graph
        centerX: this.layoutBounds.centerX,
        top: this.layoutBounds.top
      } );
    this.addChild( scoreboardBar );

    // slider for testing score changes
    var scoreSlider = new HBox( {
      left: this.layoutBounds.left + 20,
      top: scoreboardBar.bottom + 30,
      children: [
        new Text( 'Score: ', { font: new PhetFont( 20 ) } ),
        new HSlider( scoreProperty, new Range( 0, PERFECT_SCORE ), {

          //TODO #66 remove this when scoreDisplay option is added to ScoreboardBar
          constrainValue: function( value ) {
            return Util.roundSymmetric( value );
          }
        } )
      ]
    } );
    this.addChild( scoreSlider );

    // button to open LevelCompleteNode
    var levelCompletedButton = new RectangularPushButton( {
      content: new Text( 'show LevelCompletedNode', { font: new PhetFont( 20 ) } ),
      centerX: this.layoutBounds.centerX,
      bottom: this.layoutBounds.bottom - 20,
      listener: function() {
        var levelCompletedNode = new LevelCompletedNode(
          7, // level
          scoreProperty.value, // score
          PERFECT_SCORE, // maxScore
          4, // numberOfStars
          true, // timerEnabled
          77, // elapsedTime
          74, // bestTimeAtThisLevel
          true, // isNewBestTime
          function() { levelCompletedNode.detach(); }, // Continue button callback
          {
            center: self.layoutBounds.center
          } );
        self.addChild( levelCompletedNode );
      }
    } );
    this.addChild( levelCompletedButton );
  }

  vegas.register( 'FiniteChallengesScreenView', FiniteChallengesScreenView );

  return inherit( ScreenView, FiniteChallengesScreenView );
} );<|endoftext|>


<commit_before>// Copyright 2014-2015, University of Colorado Boulder

/**
 * Class that represents a solar panel in the view.
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // Modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EFACModelImage = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EFACModelImage' );
  var Energy = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/Energy' );
  var EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  var EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyChunkPathMover' );
  var EnergyConverter = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyConverter' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Random = require( 'DOT/Random' );
  var Shape = require( 'KITE/Shape' );
  var SunEnergySource = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/SunEnergySource' );
  var Vector2 = require( 'DOT/Vector2' );

  // Images
  var CONNECTOR = require( 'image!ENERGY_FORMS_AND_CHANGES/connector.png' );
  var SOLAR_PANEL = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel.png' );
  var SOLAR_PANEL_GEN = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_gen.png' );
  var SOLAR_PANEL_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_icon.png' );
  var SOLAR_PANEL_POST_2 = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_post_2.png' );
  // var SUN_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/sun_icon.png' );
  var WIRE_BLACK_LEFT = require( 'image!ENERGY_FORMS_AND_CHANGES/wire_black_left.png' );

  // Constants
  var RAND = new Random();
  var SOLAR_PANEL_OFFSET = new Vector2( 0, 0.044 );
  var CONVERTER_IMAGE_OFFSET = new Vector2( 0.015, -0.040 );
  var CONNECTOR_IMAGE_OFFSET = new Vector2( 0.057, -0.04 );

  var SOLAR_PANEL_IMAGE = new EFACModelImage( SOLAR_PANEL, SOLAR_PANEL_OFFSET, {
    width: 0.15
  } );
  var CONVERTER_IMAGE = new EFACModelImage( SOLAR_PANEL_GEN, CONVERTER_IMAGE_OFFSET );
  var CURVED_WIRE_IMAGE = new EFACModelImage( WIRE_BLACK_LEFT, CONVERTER_IMAGE_OFFSET.plus( 0.009, 0.024 ) );
  var POST_IMAGE = new EFACModelImage( SOLAR_PANEL_POST_2, CONVERTER_IMAGE_OFFSET.plus( new Vector2( 0, 0.04 ) ) );
  var CONNECTOR_IMAGE = new EFACModelImage( CONNECTOR, CONNECTOR_IMAGE_OFFSET );

  var halfWidth = SOLAR_PANEL_IMAGE.width / 2;
  var halfHeight = SOLAR_PANEL_IMAGE.getHeight() / 2;
  var PANEL_IMAGE_BOUNDS = new Bounds2( -halfWidth, -halfHeight, halfWidth, halfHeight );
  var ABSORPTION_SHAPE = new Shape();

  // Constants used for creating the path followed by the energy chunks.
  // Many of these numbers were empirically determined based on the images,
  // and will need updating if the images change.
  var OFFSET_TO_CONVERGENCE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x, 0.01 );
  var OFFSET_TO_FIRST_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x, -0.025 );
  var OFFSET_TO_SECOND_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x + 0.005, -0.033 );
  var OFFSET_TO_THIRD_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x + 0.015, CONNECTOR_IMAGE_OFFSET.y );
  var OFFSET_TO_CONNECTOR_CENTER = CONNECTOR_IMAGE_OFFSET;

  // Inter chunk spacing time for when the chunks reach the 'convergence
  // point' at the bottom of the solar panel.  It is intended to
  // approximately match the rate at which the sun emits energy chunks.
  var MIN_INTER_CHUNK_TIME = 1 / ( SunEnergySource.ENERGY_CHUNK_EMISSION_PERIOD * SunEnergySource.NUM_EMISSION_SECTORS ); // In seconds.

  /**
   * Solar panel is an energy converter
   *
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @constructor
   */
  function SolarPanel( energyChunksVisibleProperty ) {
    EnergyConverter.call( this, new Image( SOLAR_PANEL_ICON ) );
    this.energyChunkMovers = [];
    this.latestChunkArrivalTime = 0;
    this.energyOutputRate = 0;
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;

    // Counter to mimic function of IClock in original Java code
    this.simulationTime = 0;

    var zoneWidth = 0.7 * PANEL_IMAGE_BOUNDS.width;
    var minX = PANEL_IMAGE_BOUNDS.minX;
    var minY = PANEL_IMAGE_BOUNDS.minY;
    var maxX = PANEL_IMAGE_BOUNDS.maxX;
    var maxY = PANEL_IMAGE_BOUNDS.maxY;

    ABSORPTION_SHAPE
      .moveTo( maxX - zoneWidth, maxY )
      .lineTo( maxX, maxY )
      .lineTo( minX + zoneWidth, minY )
      .lineTo( minX, minY )
      .close();

  }

  energyFormsAndChanges.register( 'SolarPanel', SolarPanel );

  return inherit( EnergyConverter, SolarPanel, {

    /**
     * @param  {Number} dt - time step size
     * @param  {Energy} incomingEnergy - type, amount, direction of energy
     */
    step: function( dt, incomingEnergy ) {

      var self = this;

      if ( this.active ) {

        // Handle any incoming energy chunks.
        if ( this.incomingEnergyChunks.length > 0 ) {

          this.incomingEnergyChunks.forEach( function( incomingChunk ) {

            if ( incomingChunk.energyTypeProperty.get() === EnergyType.LIGHT ) {

              // Convert this chunk to electrical energy and add it to
              // the list of energy chunks being managed.
              incomingChunk.energyTypeProperty.set( EnergyType.ELECTRICAL );
              self.energyChunkList.push( incomingChunk );

              // And a "mover" that will move this energy chunk
              // to the bottom of the solar panel.
              self.energyChunkMovers.push( new EnergyChunkPathMover( incomingChunk,
                self.createPathToPanelBottom( self.positionProperty.get() ),
                self.chooseChunkSpeedOnPanel( incomingChunk ) ) );
            }

            // By design, this shouldn't happen, so raise an error if it does.
            else {
              assert && assert( false,
                'Encountered energy chunk with unexpected type: ' + incomingChunk.energyTypeProperty.get() );
            }

          } );

          this.incomingEnergyChunks.length = 0;
        }

        // Move the energy chunks that are currently under management.
        this.moveEnergyChunks( dt );
      }

      // Produce the appropriate amount of energy.
      var energyProduced = 0;
      if ( this.active && incomingEnergy.type === EnergyType.LIGHT ) {
        energyProduced = incomingEnergy.amount; // Perfectly efficient conversion. We should patent this.
      }
      this.energyOutputRate = energyProduced / dt;

      this.simulationTime += dt;

      return new Energy( EnergyType.ELECTRICAL, energyProduced, 0 );
    },

    /**
     * Utility method to remove EnergyChunkMover from array.
     *
     * @param  {EnergyChunkMover} mover
     * @private
     */
    removeEnergyChunkMover: function( mover ) {
      _.remove( this.energyChunkMovers, function( m ) {
        return m === mover;
      } );
    },

    /**
     * Update energy chunk positions at each step
     *
     * @param  {Number} dt timestep
     * @private
     */
    moveEnergyChunks: function( dt ) {

      // Iterate over a copy to mutate original without problems
      var movers = _.clone( this.energyChunkMovers );

      var self = this;

      movers.forEach( function( mover ) {

        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          self.removeEnergyChunkMover( mover );

          // Energy chunk has reached the bottom of the panel and now needs to move through the converter.
          if ( mover.energyChunk.position.equals( self.position.plus( OFFSET_TO_CONVERGENCE_POINT ) ) ) {
            self.energyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createPathThroughConverter( self.position ),
              EFACConstants.ENERGY_CHUNK_VELOCITY ) );
          }

          // The energy chunk has traveled across the panel and through
          // the converter, so pass it off to the next element in the system.
          else {
            self.outgoingEnergyChunks.push( mover.energyChunk );

            _.remove( self.energyChunkList, function( chunk ) {
              return chunk === mover.energyChunk;
            } );
          }
        }
      } );
    },

    /**
     * @param {Energy} incomingEnergy
     * @public
     * @override
     */
    preLoadEnergyChunks: function( incomingEnergy ) {

      this.clearEnergyChunks();

      if ( incomingEnergy.amount === 0 || incomingEnergy.type !== EnergyType.LIGHT ) {
        // No energy chunk pre-loading needed.
        return;
      }

      var absorptionBounds = this.getAbsorptionShape().bounds;
      var lowerLeftOfPanel = new Vector2( absorptionBounds.minX, absorptionBounds.minY );
      var upperRightOfPanel = new Vector2( absorptionBounds.maxX, absorptionBounds.maxY );
      var crossLineAngle = upperRightOfPanel.minus( lowerLeftOfPanel ).angle();
      var crossLineLength = lowerLeftOfPanel.distance( upperRightOfPanel );
      var dt = 1 / EFACConstants.FRAMES_PER_SECOND;
      var energySinceLastChunk = EFACConstants.ENERGY_PER_CHUNK * 0.99;
      var preLoadComplete = false;

      // Simulate energy chunks moving through the system.
      while ( !preLoadComplete ) {

        // Full energy rate generates too many chunks, so an adjustment factor is used.
        this.energySinceLastChunk += incomingEnergy.amount * dt * 0.4;

        // Determine if time to add a new chunk.
        if ( this.energySinceLastChunk >= EFACConstants.ENERGY_PER_CHUNK ) {
          var initialPosition;
          if ( this.energyChunkList.length === 0 ) {
            // For predictability of the algorithm, add the first chunk to the center of the panel.
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * 0.5, 0 ).rotated( crossLineAngle ) );
          } else {
            // Choose a random location along the center portion of the cross line.
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * ( 0.5 * RAND.nextDouble() + 0.25 ), 0 ).rotated( crossLineAngle ) );
          }

          var newEnergyChunk = new EnergyChunk(
            EnergyType.ELECTRICAL,
            initialPosition,
            this.energyChunksVisibleProperty );

          this.energyChunkList.push( newEnergyChunk );

          // And a "mover" that will move this energy chunk
          // to the bottom of the solar panel.
          this.energyChunkMovers.push( new EnergyChunkPathMover( newEnergyChunk,
            this.createPathToPanelBottom( this.positionProperty.get() ),
            this.chooseChunkVelocityOnPanel( newEnergyChunk ) ) );

          // Update energy since last chunk.
          energySinceLastChunk -= EFACConstants.ENERGY_PER_CHUNK;
        }

        this.moveEnergyChunks( dt );

        if ( this.outgoingEnergyChunks.length > 0 ) {
          // An energy chunk has made it all the way through the system, which completes the pre-load.
          preLoadComplete = true;
        }
      }
    },

    /**
     * @return {Energy} Type, amount, direction of emitted energy
     */
    getEnergyOutputRate: function() {
      return new Energy( EnergyType.ELECTRICAL, this.energyOutputRate, 0 );
    },

    /**
     * Choose speed of chunk on panel such that it won't clump up with other chunks.
     *
     * @param  {EnergyChunk} incomingEnergyChunk
     *
     * @return {number} speed
     * @private
     */
    chooseChunkSpeedOnPanel: function( incomingEnergyChunk ) {
      // Start with default speed.
      var chunkSpeed = EFACConstants.ENERGY_CHUNK_VELOCITY;

      // Count the number of chunks currently on the panel.
      var numChunksOnPanel = 0;

      var self = this;

      this.energyChunkMovers.forEach( function( mover ) {
        if ( mover.getFinalDestination().equals( self.position.plus( OFFSET_TO_CONVERGENCE_POINT ) ) ) {
          numChunksOnPanel++;
        }
      } );

      // Compute the projected time of arrival at the convergence point.
      var distanceToConvergencePoint =
        incomingEnergyChunk.positionProperty.get().distance( this.position.plus( OFFSET_TO_CONVERGENCE_POINT ) );
      var travelTime = distanceToConvergencePoint / chunkSpeed;
      var projectedArrivalTime = this.simulationTime + travelTime;

      // Calculate the minimum spacing based on the number of chunks on the
      // panel.
      var minArrivalTimeSpacing = numChunksOnPanel <= 3 ?
        MIN_INTER_CHUNK_TIME :
        MIN_INTER_CHUNK_TIME / ( numChunksOnPanel - 2 );

      // If the projected arrival time is too close to the current last
      // chunk, slow down so that the minimum spacing is maintained.
      if ( this.latestChunkArrivalTime + minArrivalTimeSpacing > projectedArrivalTime ) {
        projectedArrivalTime = this.latestChunkArrivalTime + minArrivalTimeSpacing;
      }

      this.latestChunkArrivalTime = projectedArrivalTime;

      return distanceToConvergencePoint / ( projectedArrivalTime - this.simulationTime );
    },

    /**
     * @public
     * @override
     */
    clearEnergyChunks: function() {
      EnergyConverter.prototype.clearEnergyChunks.call( this );
      this.energyChunkMovers.length = 0;
      this.latestChunkArrivalTime = 0;
    },

    /**
     * @param {Vector2} panelPosition
     * @private
     */
    createPathToPanelBottom: function( panelPosition ) {
      var path = [];

      path.push( panelPosition.plus( OFFSET_TO_CONVERGENCE_POINT ) );

      return path;
    },

    /**
     * @param {Vector2} panelPosition
     * @private
     */
    createPathThroughConverter: function( panelPosition ) {
      var path = [];

      path.push( panelPosition.plus( OFFSET_TO_FIRST_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_SECOND_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_THIRD_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_CONNECTOR_CENTER ) );

      return path;
    },

    getAbsorptionShape: function() {
      return ABSORPTION_SHAPE;
    }

  }, {
    CURVED_WIRE_IMAGE: CURVED_WIRE_IMAGE,
    POST_IMAGE: POST_IMAGE,
    SOLAR_PANEL_IMAGE: SOLAR_PANEL_IMAGE,
    CONVERTER_IMAGE: CONVERTER_IMAGE,
    CONNECTOR_IMAGE: CONNECTOR_IMAGE
  } );
} );

<commit_msg>Fix position bug for solar panel wire<commit_after>// Copyright 2014-2015, University of Colorado Boulder

/**
 * Class that represents a solar panel in the view.
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // Modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EFACModelImage = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EFACModelImage' );
  var Energy = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/Energy' );
  var EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  var EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyChunkPathMover' );
  var EnergyConverter = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyConverter' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Random = require( 'DOT/Random' );
  var Shape = require( 'KITE/Shape' );
  var SunEnergySource = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/SunEnergySource' );
  var Vector2 = require( 'DOT/Vector2' );

  // Images
  var CONNECTOR = require( 'image!ENERGY_FORMS_AND_CHANGES/connector.png' );
  var SOLAR_PANEL = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel.png' );
  var SOLAR_PANEL_GEN = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_gen.png' );
  var SOLAR_PANEL_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_icon.png' );
  var SOLAR_PANEL_POST_2 = require( 'image!ENERGY_FORMS_AND_CHANGES/solar_panel_post_2.png' );
  // var SUN_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/sun_icon.png' );
  var WIRE_BLACK_LEFT = require( 'image!ENERGY_FORMS_AND_CHANGES/wire_black_left.png' );

  // Constants
  var RAND = new Random();
  var SOLAR_PANEL_OFFSET = new Vector2( 0, 0.044 );
  var CONVERTER_IMAGE_OFFSET = new Vector2( 0.015, -0.040 );
  var CONNECTOR_IMAGE_OFFSET = new Vector2( 0.057, -0.04 );

  var SOLAR_PANEL_IMAGE = new EFACModelImage( SOLAR_PANEL, SOLAR_PANEL_OFFSET, {
    width: 0.15
  } );
  var CONVERTER_IMAGE = new EFACModelImage( SOLAR_PANEL_GEN, CONVERTER_IMAGE_OFFSET );
  var CURVED_WIRE_IMAGE = new EFACModelImage( WIRE_BLACK_LEFT, CONVERTER_IMAGE_OFFSET.plusXY( 0.009, 0.024 ) );
  var POST_IMAGE = new EFACModelImage( SOLAR_PANEL_POST_2, CONVERTER_IMAGE_OFFSET.plusXY( 0, 0.04 ) );
  var CONNECTOR_IMAGE = new EFACModelImage( CONNECTOR, CONNECTOR_IMAGE_OFFSET );

  var halfWidth = SOLAR_PANEL_IMAGE.width / 2;
  var halfHeight = SOLAR_PANEL_IMAGE.getHeight() / 2;
  var PANEL_IMAGE_BOUNDS = new Bounds2( -halfWidth, -halfHeight, halfWidth, halfHeight );
  var ABSORPTION_SHAPE = new Shape();

  // Constants used for creating the path followed by the energy chunks.
  // Many of these numbers were empirically determined based on the images,
  // and will need updating if the images change.
  var OFFSET_TO_CONVERGENCE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x, 0.01 );
  var OFFSET_TO_FIRST_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x, -0.025 );
  var OFFSET_TO_SECOND_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x + 0.005, -0.033 );
  var OFFSET_TO_THIRD_CURVE_POINT = new Vector2( CONVERTER_IMAGE_OFFSET.x + 0.015, CONNECTOR_IMAGE_OFFSET.y );
  var OFFSET_TO_CONNECTOR_CENTER = CONNECTOR_IMAGE_OFFSET;

  // Inter chunk spacing time for when the chunks reach the 'convergence
  // point' at the bottom of the solar panel.  It is intended to
  // approximately match the rate at which the sun emits energy chunks.
  var MIN_INTER_CHUNK_TIME = 1 / ( SunEnergySource.ENERGY_CHUNK_EMISSION_PERIOD * SunEnergySource.NUM_EMISSION_SECTORS ); // In seconds.

  /**
   * Solar panel is an energy converter
   *
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @constructor
   */
  function SolarPanel( energyChunksVisibleProperty ) {
    EnergyConverter.call( this, new Image( SOLAR_PANEL_ICON ) );
    this.energyChunkMovers = [];
    this.latestChunkArrivalTime = 0;
    this.energyOutputRate = 0;
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;

    // Counter to mimic function of IClock in original Java code
    this.simulationTime = 0;

    var zoneWidth = 0.7 * PANEL_IMAGE_BOUNDS.width;
    var minX = PANEL_IMAGE_BOUNDS.minX;
    var minY = PANEL_IMAGE_BOUNDS.minY;
    var maxX = PANEL_IMAGE_BOUNDS.maxX;
    var maxY = PANEL_IMAGE_BOUNDS.maxY;

    ABSORPTION_SHAPE
      .moveTo( maxX - zoneWidth, maxY )
      .lineTo( maxX, maxY )
      .lineTo( minX + zoneWidth, minY )
      .lineTo( minX, minY )
      .close();

  }

  energyFormsAndChanges.register( 'SolarPanel', SolarPanel );

  return inherit( EnergyConverter, SolarPanel, {

    /**
     * @param  {Number} dt - time step size
     * @param  {Energy} incomingEnergy - type, amount, direction of energy
     */
    step: function( dt, incomingEnergy ) {

      var self = this;

      if ( this.active ) {

        // Handle any incoming energy chunks.
        if ( this.incomingEnergyChunks.length > 0 ) {

          this.incomingEnergyChunks.forEach( function( incomingChunk ) {

            if ( incomingChunk.energyTypeProperty.get() === EnergyType.LIGHT ) {

              // Convert this chunk to electrical energy and add it to
              // the list of energy chunks being managed.
              incomingChunk.energyTypeProperty.set( EnergyType.ELECTRICAL );
              self.energyChunkList.push( incomingChunk );

              // And a "mover" that will move this energy chunk
              // to the bottom of the solar panel.
              self.energyChunkMovers.push( new EnergyChunkPathMover( incomingChunk,
                self.createPathToPanelBottom( self.positionProperty.get() ),
                self.chooseChunkSpeedOnPanel( incomingChunk ) ) );
            }

            // By design, this shouldn't happen, so raise an error if it does.
            else {
              assert && assert( false,
                'Encountered energy chunk with unexpected type: ' + incomingChunk.energyTypeProperty.get() );
            }

          } );

          this.incomingEnergyChunks.length = 0;
        }

        // Move the energy chunks that are currently under management.
        this.moveEnergyChunks( dt );
      }

      // Produce the appropriate amount of energy.
      var energyProduced = 0;
      if ( this.active && incomingEnergy.type === EnergyType.LIGHT ) {
        energyProduced = incomingEnergy.amount; // Perfectly efficient conversion. We should patent this.
      }
      this.energyOutputRate = energyProduced / dt;

      this.simulationTime += dt;

      return new Energy( EnergyType.ELECTRICAL, energyProduced, 0 );
    },

    /**
     * Utility method to remove EnergyChunkMover from array.
     *
     * @param  {EnergyChunkMover} mover
     * @private
     */
    removeEnergyChunkMover: function( mover ) {
      _.remove( this.energyChunkMovers, function( m ) {
        return m === mover;
      } );
    },

    /**
     * Update energy chunk positions at each step
     *
     * @param  {Number} dt timestep
     * @private
     */
    moveEnergyChunks: function( dt ) {

      // Iterate over a copy to mutate original without problems
      var movers = _.clone( this.energyChunkMovers );

      var self = this;

      movers.forEach( function( mover ) {

        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          self.removeEnergyChunkMover( mover );

          // Energy chunk has reached the bottom of the panel and now needs to move through the converter.
          if ( mover.energyChunk.position.equals( self.position.plus( OFFSET_TO_CONVERGENCE_POINT ) ) ) {
            self.energyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createPathThroughConverter( self.position ),
              EFACConstants.ENERGY_CHUNK_VELOCITY ) );
          }

          // The energy chunk has traveled across the panel and through
          // the converter, so pass it off to the next element in the system.
          else {
            self.outgoingEnergyChunks.push( mover.energyChunk );

            _.remove( self.energyChunkList, function( chunk ) {
              return chunk === mover.energyChunk;
            } );
          }
        }
      } );
    },

    /**
     * @param {Energy} incomingEnergy
     * @public
     * @override
     */
    preLoadEnergyChunks: function( incomingEnergy ) {

      this.clearEnergyChunks();

      if ( incomingEnergy.amount === 0 || incomingEnergy.type !== EnergyType.LIGHT ) {
        // No energy chunk pre-loading needed.
        return;
      }

      var absorptionBounds = this.getAbsorptionShape().bounds;
      var lowerLeftOfPanel = new Vector2( absorptionBounds.minX, absorptionBounds.minY );
      var upperRightOfPanel = new Vector2( absorptionBounds.maxX, absorptionBounds.maxY );
      var crossLineAngle = upperRightOfPanel.minus( lowerLeftOfPanel ).angle();
      var crossLineLength = lowerLeftOfPanel.distance( upperRightOfPanel );
      var dt = 1 / EFACConstants.FRAMES_PER_SECOND;
      var energySinceLastChunk = EFACConstants.ENERGY_PER_CHUNK * 0.99;
      var preLoadComplete = false;

      // Simulate energy chunks moving through the system.
      while ( !preLoadComplete ) {

        // Full energy rate generates too many chunks, so an adjustment factor is used.
        this.energySinceLastChunk += incomingEnergy.amount * dt * 0.4;

        // Determine if time to add a new chunk.
        if ( this.energySinceLastChunk >= EFACConstants.ENERGY_PER_CHUNK ) {
          var initialPosition;
          if ( this.energyChunkList.length === 0 ) {
            // For predictability of the algorithm, add the first chunk to the center of the panel.
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * 0.5, 0 ).rotated( crossLineAngle ) );
          } else {
            // Choose a random location along the center portion of the cross line.
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * ( 0.5 * RAND.nextDouble() + 0.25 ), 0 ).rotated( crossLineAngle ) );
          }

          var newEnergyChunk = new EnergyChunk(
            EnergyType.ELECTRICAL,
            initialPosition,
            this.energyChunksVisibleProperty );

          this.energyChunkList.push( newEnergyChunk );

          // And a "mover" that will move this energy chunk
          // to the bottom of the solar panel.
          this.energyChunkMovers.push( new EnergyChunkPathMover( newEnergyChunk,
            this.createPathToPanelBottom( this.positionProperty.get() ),
            this.chooseChunkVelocityOnPanel( newEnergyChunk ) ) );

          // Update energy since last chunk.
          energySinceLastChunk -= EFACConstants.ENERGY_PER_CHUNK;
        }

        this.moveEnergyChunks( dt );

        if ( this.outgoingEnergyChunks.length > 0 ) {
          // An energy chunk has made it all the way through the system, which completes the pre-load.
          preLoadComplete = true;
        }
      }
    },

    /**
     * @return {Energy} Type, amount, direction of emitted energy
     */
    getEnergyOutputRate: function() {
      return new Energy( EnergyType.ELECTRICAL, this.energyOutputRate, 0 );
    },

    /**
     * Choose speed of chunk on panel such that it won't clump up with other chunks.
     *
     * @param  {EnergyChunk} incomingEnergyChunk
     *
     * @return {number} speed
     * @private
     */
    chooseChunkSpeedOnPanel: function( incomingEnergyChunk ) {
      // Start with default speed.
      var chunkSpeed = EFACConstants.ENERGY_CHUNK_VELOCITY;

      // Count the number of chunks currently on the panel.
      var numChunksOnPanel = 0;

      var self = this;

      this.energyChunkMovers.forEach( function( mover ) {
        if ( mover.getFinalDestination().equals( self.position.plus( OFFSET_TO_CONVERGENCE_POINT ) ) ) {
          numChunksOnPanel++;
        }
      } );

      // Compute the projected time of arrival at the convergence point.
      var distanceToConvergencePoint =
        incomingEnergyChunk.positionProperty.get().distance( this.position.plus( OFFSET_TO_CONVERGENCE_POINT ) );
      var travelTime = distanceToConvergencePoint / chunkSpeed;
      var projectedArrivalTime = this.simulationTime + travelTime;

      // Calculate the minimum spacing based on the number of chunks on the
      // panel.
      var minArrivalTimeSpacing = numChunksOnPanel <= 3 ?
        MIN_INTER_CHUNK_TIME :
        MIN_INTER_CHUNK_TIME / ( numChunksOnPanel - 2 );

      // If the projected arrival time is too close to the current last
      // chunk, slow down so that the minimum spacing is maintained.
      if ( this.latestChunkArrivalTime + minArrivalTimeSpacing > projectedArrivalTime ) {
        projectedArrivalTime = this.latestChunkArrivalTime + minArrivalTimeSpacing;
      }

      this.latestChunkArrivalTime = projectedArrivalTime;

      return distanceToConvergencePoint / ( projectedArrivalTime - this.simulationTime );
    },

    /**
     * @public
     * @override
     */
    clearEnergyChunks: function() {
      EnergyConverter.prototype.clearEnergyChunks.call( this );
      this.energyChunkMovers.length = 0;
      this.latestChunkArrivalTime = 0;
    },

    /**
     * @param {Vector2} panelPosition
     * @private
     */
    createPathToPanelBottom: function( panelPosition ) {
      var path = [];

      path.push( panelPosition.plus( OFFSET_TO_CONVERGENCE_POINT ) );

      return path;
    },

    /**
     * @param {Vector2} panelPosition
     * @private
     */
    createPathThroughConverter: function( panelPosition ) {
      var path = [];

      path.push( panelPosition.plus( OFFSET_TO_FIRST_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_SECOND_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_THIRD_CURVE_POINT ) );
      path.push( panelPosition.plus( OFFSET_TO_CONNECTOR_CENTER ) );

      return path;
    },

    getAbsorptionShape: function() {
      return ABSORPTION_SHAPE;
    }

  }, {
    CURVED_WIRE_IMAGE: CURVED_WIRE_IMAGE,
    POST_IMAGE: POST_IMAGE,
    SOLAR_PANEL_IMAGE: SOLAR_PANEL_IMAGE,
    CONVERTER_IMAGE: CONVERTER_IMAGE,
    CONNECTOR_IMAGE: CONNECTOR_IMAGE
  } );
} );

<|endoftext|>