Initial commit

This commit is contained in:
Juho Teperi
2013-05-13 11:43:34 +03:00
commit fbf8fb087a
44 changed files with 2425 additions and 0 deletions

40
app/scripts/app.js Normal file
View File

@@ -0,0 +1,40 @@
'use strict';
define([
'collections/route',
'collections/results',
'views/route',
'views/leginput',
'views/roundtrip',
'views/passengers',
'views/map',
'views/total',
'views/operation'
], function (Route, Results, RouteView, LegInput, RoundtripInput, PassengersInput, MapView, TotalView, OperationView) {
var App = function () {
this.initialize = function () {
this.route = new Route();
this.legInput = new LegInput({collection: this.route});
this.roundtripInput = new RoundtripInput();
this.passengersInput = new PassengersInput();
this.routeView = new RouteView({collection: this.route});
this.mapView = new MapView({collection: this.route});
// Order is important. ResultsView requires that Route legs are
// on dom as ResultView references from and to legs to calculate
// position.
this.results = new Results([], {route: this.route});
this.totalView = new TotalView({model: this.results.total});
new OperationView();
};
this.initialize();
};
return App;
});

View File

@@ -0,0 +1,53 @@
'use strict';
define([
'backbone',
'models/result',
'models/total'
], function (Backbone, Result, Total) {
var Results = Backbone.Collection.extend({
model: Result,
initialize: function (model, opts) {
this.route = opts.route;
this.route.on('add', this.newLeg, this);
this.route.on('remove', this.calculate, this);
this.route.on('sort', this.calculate, this);
this.total = new Total();
},
newLeg: function (model, collection, options) {
if (this.route.length < 2) {
return;
}
var from;
if (this.length > 0) {
from = this.route.get(this.last().get('to'));
} else {
from = this.route.first();
}
var result = new Result({from: from, to: model});
this.push(result);
this.total.add(result);
},
calculate: function (model, collection, options) {
var models = [];
this.total.reset();
var prev = null;
this.route.forEach(function (leg) {
if (prev) {
var result = new Result({from: prev, to: leg});
models.push(result);
this.total.add(result);
}
prev = leg;
}.bind(this));
this.reset(models);
}
});
return Results;
});

View File

@@ -0,0 +1,19 @@
define([
'lodash',
'backbone',
'../models/leg'
], function (_, Backbone, Leg) {
// var stages = [];
// var data = {};
var num = 1;
var Route = Backbone.Collection.extend({
model: Leg,
comparator: function (model) {
return model.get('order');
}
});
return Route;
});

64
app/scripts/config.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
define(function () {
return {
api: 'http://work.lentolaskuri.fi/api',
R: 6371,
radiativeForceFactor: function (dist) {
return (dist >= 500) ? 2.0 : 1.0;
},
distanceRanges: [{
name: '0',
}, {
name: '500',
min: 500
}, {
name: '1400',
min: 1400
}, {
name: '5500',
min: 5500,
}, {
name: '9000',
min: 9000
}],
parameters: [{
name: 'Suomi',
regex: /^EF/,
// Parameters for different distances
co2factor: [0.08, 0.07, 0.07, 0.07, 0.07],
ltoCycle: [15, 15, 15, 15, 15],
load: [0.6, 0.65, 0.65, 0.65, 0.65],
freight: [0.05, 0.05, 0.05, 0.05, 0.05]
}, {
name: 'Pohjois-Eurooppa',
regex: /^E/,
co2factor: [0.07, 0.06, 0.06, 0.06, 0.06],
ltoCycle: [22, 22, 22, 22, 22],
load: [0.65, 0.7, 0.75, 0.75, 0.75],
freight: [0.05, 0.1, 0.1, 0.1, 0.1]
}, {
name: 'Etelä-Eurooppa',
regex: /^L/,
co2factor: [0.07, 0.07, 0.07, 0.07, 0.07],
ltoCycle: [22, 22, 22, 22, 22],
load: [0.65, 0.7, 0.8, 0.8, 0.8],
freight: [0.05, 0.1, 0.1, 0.1, 0.1],
}, {
name: 'Euroopasta/aan',
co2factor: [0.07, 0.07, 0.08, 0.09, 0.09],
ltoCycle: [21, 24, 22, 26, 26], // 3rd value?
load: [0.7, 0.7, 0.8, 0.8, 0.8],
freight: [0.05, 0.15, 0.15, 0.15, 0.15]
}, {
name: 'Euroopan ulkopuoliset',
co2factor: [0.08, 0.07, 0.08, 0.08, 0.09], // 2th value?
ltoCycle: [21, 22, 23, 26, 26],
load: [0.7, 0.7, 0.8, 0.8, 0.8],
freight: [0.05, 0.1, 0.15, 0.2, 0.2]
}],
indirectRouteMultiplier: 1.09,
// http://co2-raportti.fi/?heading=EU:n-p%C3%A4%C3%A4st%C3%B6kaupan-arvo-laski-kolmanneksen-t%C3%A4n%C3%A4-vuonna&page=ilmastouutisia&news_id=3665
priceCO2: 7.5, // euros per tonne of CO2
};
});

View File

@@ -0,0 +1,75 @@
'use strict';
define([
'jquery',
'lodash',
'config'
], function ($, _, config) {
var airportById = function (element, callback) {
$.get('http://localhost:8000/search.php?i=' + element.val(), null, function (data) {
callback(data[0]);
});
};
var data = function (term, page) {
return {
s: term
};
};
var results = function (data, page) {
return {results: data};
};
var findArea = function (icao) {
for (var i = 0; i < config.parameters.length; ++i) {
var param = config.parameters[i];
if (param.regex && icao.match(param.regex)) {
return i;
}
}
return config.parameters.length - 2;
};
var selectArea = function (fromCode, toCode) {
var outside = config.parameters.length - 2;
if (fromCode === outside && toCode === outside) {
return config.parameters.length - 1;
}
return Math.max(fromCode, toCode);
};
var selectRange = function (dist) {
for (var i = 0; i < config.distanceRanges.length; ++i) {
if (config.distanceRanges[i].min && dist <= config.distanceRanges[i].min) {
return i;
}
}
return 0;
};
// Should select "greater of two", finland to western europe -> west europe
// west europe to south europe -> south europe
var parameters = function (from, to, range) {
var i = selectArea(findArea(from.icao), findArea(to.icao));
var p = config.parameters[i];
return {
name: p.name,
co2factor: p.co2factor[range],
ltoCycle: p.ltoCycle[range],
load: p.load[range],
freight: p.freight[range]
};
};
return {
ajax: {
url: config.api + '/search.php',
dataType: 'json',
data: data,
results: results
},
airportById: airportById,
selectRange: selectRange,
parameters: parameters
};
});

View File

@@ -0,0 +1,9 @@
'use strict';
// convert Google Maps into an AMD module
define([
'async!http://maps.google.com/maps/api/js?v=3&sensor=false'
], function() {
// return the gmaps namespace for brevity
return window.google.maps;
});

View File

@@ -0,0 +1,10 @@
'use strict';
define([
'handlebars'
], function (Handlebars) {
Handlebars.registerHelper('displayFloat', function (num, precision) {
precision = precision;
return ''+(num.toFixed(precision)).replace('.', ',');
});
});

120
app/scripts/libs/maps.js Normal file
View File

@@ -0,0 +1,120 @@
'use strict';
define([
'require',
'jquery',
'lodash',
'backbone'
], function (require, $, _, Backbone) {
var gmaps;
function glatlng(latlng) {
if (_.isArray(latlng)) {
return new gmaps.LatLng(latlng[0], latlng[1]);
} else if (latlng instanceof Backbone.Model) {
return new gmaps.LatLng(latlng.get('lat'), latlng.get('long'));
} else {
return new gmaps.LatLng(latlng.lat, latlng.long);
}
}
var maps = {};
var deferred = $.Deferred();
var i = 0;
var Map = function (el, opts) {
var num = i;
deferred.done(function () {
maps[num] = new gmaps.Map(el, {
center: glatlng([0, 0]),
mapTypeId: gmaps.MapTypeId.ROADMAP,
zoom: 0,
streetViewControl: false,
mapTypeControl: false,
draggable: false,
scrollwheel: false,
zoomControl: false
});
});
return {
id: i++
}
};
var Line = function (attr) {
var data;
deferred.done(function () {
attr = attr || {};
attr.map = maps[attr.map.id];
data = new gmaps.Polyline(attr);
});
return {
clear: function () {
deferred.done(function () {
data.setPath([]);
});
},
add: function (latlng) {
deferred.done(function () {
data.getPath().push(glatlng(latlng));
});
}
};
};
var Bounds = function (map) {
var map;
var i;
var data;
var reset = function () {
i = 0;
map.setZoom(1);
map.setCenter(glatlng([0, 0]));
};
deferred.done(function () {
data = new gmaps.LatLngBounds();
map = maps[map.id];
reset();
});
return {
clear: function () {
deferred.done(function () {
reset();
});
},
add: function (latlng) {
deferred.done(function () {
data.extend(glatlng(latlng));
++i;
});
},
use: function () {
deferred.done(function () {
if (i >= 2) {
map.fitBounds(data);
}
});
}
};
};
require(['libs/gmaps'], function (_gmaps) {
gmaps = _gmaps;
deferred.resolve();
});
return {
Map: Map,
Line: Line,
Bounds: Bounds
};
});

26
app/scripts/libs/math.js Normal file
View File

@@ -0,0 +1,26 @@
'use strict';
define([
'config',
'libs/airports'
], function (cfg, airports) {
var toRad = function toRad(n) {
return n * Math.PI / 180;
};
var haversine = function haversine(from, to) {
var dLat = toRad(to.get('lat') - from.get('lat'));
var dLon = toRad(to.get('long') - from.get('long'));
var lat1 = toRad(from.get('lat'));
var lat2 = toRad(to.get('lat'));
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return cfg.R * c;
};
return {
toRad: toRad,
haversine: haversine
};
});

58
app/scripts/main.js Normal file
View File

@@ -0,0 +1,58 @@
'use strict';
require.config({
map: {
'*': {
'underscore': 'lodash'
}
},
paths: {
jquery: '../components/jquery/jquery',
bootstrap: '../scripts/bootstrap',
select2: '../components/select2/select2',
lodash: '../components/lodash/lodash',
handlebars: '../components/handlebars/handlebars.runtime',
Template: '../scripts/templates',
'jquery-ui-core': '../components/jquery-ui/ui/jquery.ui.core',
'jquery-ui-mouse': '../components/jquery-ui/ui/jquery.ui.mouse',
'jquery-ui-widget': '../components/jquery-ui/ui/jquery.ui.widget',
'jquery-ui-sortable': '../components/jquery-ui/ui/jquery.ui.sortable',
async: '../components/requirejs-plugins/src/async',
backbone: '../components/backbone/backbone',
'backbone-mediator': '../components/Backbone-Mediator/backbone-mediator'
},
shim: {
backbone: {
deps: ['lodash'],
exports: 'Backbone'
},
bootstrap: {
deps: ['jquery'],
exports: 'jquery'
},
select2: {
deps: ['jquery'],
exports: 'jquery'
},
'jquery-ui-core': {
deps: ['jquery'],
exports: 'jquery'
},
'jquery-ui-sortable': {
deps: ['jquery-ui-core', 'jquery-ui-widget', 'jquery-ui-mouse'],
exports: 'jquery'
},
handlebars: {
exports: 'Handlebars'
}
}
});
require([
'jquery',
'app',
'bootstrap',
'libs/handlebar_helpers'
], function ($, App) {
var app = new App();
});

20
app/scripts/models/leg.js Normal file
View File

@@ -0,0 +1,20 @@
define([
'backbone'
], function (Backbone) {
var num = 1;
var Leg = Backbone.Model.extend({
initialize: function () {
this.set('airportId', this.get('id'));
this.set('id', num);
++num;
},
destroy: function () {
this.trigger('destroy', this, this.collection, {});
}
});
return Leg;
});

View File

@@ -0,0 +1,45 @@
'use strict';
define([
'backbone',
'backbone-mediator',
'libs/math',
'libs/airports',
'config'
], function (Backbone, BackboneMediator, Math, airports, config) {
var Result = Backbone.Model.extend({
defaults: {
dist: 0,
total: 0,
lto: 0,
co2factor: 0,
ltoCycle: 0,
load: 0,
freight: 0
},
initialize: function (attr, options) {
if (this.get('from') && this.get('to')) {
this.set('dist', Math.haversine(this.get('from'), this.get('to')));
}
this.calculate.apply(this);
},
calculate: function () {
var roundtrip = 0;
if (this.get('from') && this.get('to')) {
var roundtripFactor = (roundtrip) ? 2.0 : 1.0; // fuu
this.set('range', airports.selectRange(this.get('dist')));
this.set(airports.parameters(this.get('from').toJSON(), this.get('to').toJSON(), this.get('range')));
var dist = this.get('dist') * config.indirectRouteMultiplier;
var m = config.radiativeForceFactor(dist) * (1 - this.get('freight')) * (1 / this.get('load'));
this.set('lto', m * this.get('ltoCycle'));
this.set('total', m * this.get('co2factor') * dist + this.get('lto'));
}
}
});
return Result;
});

View File

@@ -0,0 +1,54 @@
'use strict';
define([
'backbone',
'backbone-mediator',
'libs/math',
'libs/airports',
'config'
], function (Backbone, BackboneMediator, Math, airports, config) {
var Result = Backbone.Model.extend({
defaults: {
dist: 0,
total: 0,
rawTotal: 0,
alone: true,
roundtrip: false,
passengers: 1,
price: 0
},
initialize: function () {
Backbone.Mediator.subscribe('roundtrip:change', this.set.bind(this, 'roundtrip'));
Backbone.Mediator.subscribe('passengers:change', this.set.bind(this, 'passengers'));
Backbone.Mediator.subscribe('passengers:change', function (num) {
this.set('alone', num === 1);
}.bind(this));
this.on('change:roundtrip change:passengers', this.calc, this);
},
reset: function () {
this.set('dist', 0);
this.set('total', 0);
this.set('rawTotal', 0);
this.set('price', 0);
},
calc: function () {
var mult = this.get('roundtrip') ? 2 : 1;
this.set('total', mult * this.get('passengers') * this.get('rawTotal'));
this.set('price', this.get('total') / 1000 * config.priceCO2);
},
add: function (another) {
var keys = {
'dist': 'dist',
'total': 'rawTotal'
};
for (var i in keys) {
this.set(keys[i], this.get(keys[i]) + another.get(i));
}
this.calc();
}
});
return Result;
});

View File

@@ -0,0 +1,22 @@
define([
'backbone',
'select2'
], function (Backbone) {
var Dropdown = Backbone.View.extend({
events: {
'change': 'change'
},
initialize: function (opts) {
this.$el.select2(opts.select2);
},
open: function () {
this.$el.select2('open');
},
change: function () {
this.trigger('new-leg', this.$el.select2('data'));
this.$el.select2('data', {});
}
});
return Dropdown;
});

29
app/scripts/views/leg.js Normal file
View File

@@ -0,0 +1,29 @@
'use strict';
define([
'backbone',
'Template'
], function (Backbone, Template) {
var LegView = Backbone.View.extend({
tagName: 'div',
className: 'leg',
events: {
'click a.delete': 'destroy'
},
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
},
destroy: function () {
this.model.destroy();
},
render: function () {
this.$el.data('id', this.model.get('id'));
this.$el.attr('id', 'leg-' + this.model.get('id')); // fuu
this.$el.html(Template.leg(this.model.toJSON()));
return this;
}
});
return LegView;
});

View File

@@ -0,0 +1,39 @@
'use strict';
define([
'backbone',
'models/leg',
'views/dropdown',
'libs/airports',
'Template'
], function (Backbone, Leg, Dropdown, airports, Template) {
var LegInput = Backbone.View.extend({
el: '.legInputWidget',
events: {
'click button': 'openDropdown'
},
initialize: function () {
this.dropdown = new Dropdown({
el: this.$el.find('input'),
select2: {
initSelection: airports.airportById,
formatResult: Template.choice,
formatSelection: Template.choice,
minimumInputLength: 1,
ajax: airports.ajax
}
});
this.dropdown.bind('new-leg', this.newLeg, this);
},
openDropdown: function () {
this.dropdown.open();
},
newLeg: function (data) {
this.collection.push(new Leg(data));
}
});
return LegInput;
});

41
app/scripts/views/map.js Normal file
View File

@@ -0,0 +1,41 @@
'use strict';
define([
'backbone',
'libs/maps'
], function (Backbone, Maps) {
var MapView = Backbone.View.extend({
el: '#gmap',
initialize: function () {
this.collection.on('sort', this.render, this);
this.collection.on('remove', this.render, this);
this.collection.on('add', this.add, this);
this.map = new Maps.Map(this.el);
this.route = new Maps.Line({
geodesic: true,
map: this.map,
strokeColor: '#000'
});
this.bounds = new Maps.Bounds(this.map);
},
render: function () {
this.route.clear();
this.bounds.clear();
this.collection.forEach(this.route.add);
this.collection.forEach(this.bounds.add);
this.bounds.use();
},
add: function (model, collection, options) {
this.route.add(model);
this.bounds.add(model);
this.bounds.use();
}
});
return MapView;
});

View File

@@ -0,0 +1,22 @@
'use strict';
define([
'backbone',
'Template',
'config',
'bootstrap'
], function (Backbone, Template, config) {
var Operation = Backbone.View.extend({
el: '#operation',
initialize: function () {
this.render.apply(this);
},
render: function () {
this.$el.html(Template.operation(config));
this.$el.find('abbr').tooltip();
return this;
}
});
return Operation;
});

View File

@@ -0,0 +1,28 @@
define([
'backbone',
'backbone-mediator'
], function (Backbone) {
var PassengersInput = Backbone.View.extend({
el: '.passengersWidget',
events: {
'change input': 'change',
'keyup input': 'change'
},
initialize: function () {
this.value = 1;
},
change: function (event) {
var val = this.$el.find('input').val();
if (!val) {
val = 1;
}
val = Number(val);
if (val !== this.value) {
this.value = val;
Backbone.Mediator.publish('passengers:change', this.value);
}
}
});
return PassengersInput;
});

View File

@@ -0,0 +1,22 @@
'use strict';
define([
'backbone',
'backbone-mediator'
], function (Backbone) {
var RoundtripInput = Backbone.View.extend({
el: '.roundtripWidget',
events: {
'click button': 'click'
},
click: function (event, el) {
this.$el.find('button').removeClass('active');
var current = this.$el.find(event.currentTarget);
current.addClass('active');
Backbone.Mediator.publish('roundtrip:change', current.hasClass('roundtrip'));
}
});
return RoundtripInput;
});

View File

@@ -0,0 +1,49 @@
'use strict';
define([
'backbone',
'views/leg',
'jquery-ui-sortable'
], function (Backbone, LegView) {
var RouteView = Backbone.View.extend({
el: '.route',
events: {
'sortstart': 'sortStart',
'sortstop': 'sortStop'
},
initialize: function () {
this.collection.bind('add', this.add, this);
this.collection.bind('sort', this.render, this);
this.$el.sortable({
handle: 'div > div',
scroll: false,
});
},
sortStart: function () {
// fadeOut results
},
sortStop: function (event, ui) {
this.updateSort();
// fadeIn results
},
updateSort: function () {
// jQuery UI already updates DOM order,
// so we only have to update Backbone collection to match that
var self = this;
this.$el.find('.leg').each(function (index) {
// Get id of model from element
var id = $(this).data('id');
self.collection.get(id).set('order', index);
});
this.collection.sort();
},
add: function (model, collection, options) {
var item = new LegView({model: model});
this.$el.append(item.render().el);
}
});
return RouteView;
});

View File

@@ -0,0 +1,19 @@
'use strict';
define([
'backbone',
'Template'
], function (Backbone, Template) {
var TotalView = Backbone.View.extend({
el: '.totalWidget',
initialize: function () {
this.render.apply(this);
this.model.bind('change', this.render, this);
},
render: function () {
this.$el.html(Template.total(this.model.toJSON()));
}
});
return TotalView;
});