Initial commit.

This commit is contained in:
Juho Teperi
2012-10-22 22:48:26 +03:00
commit 86c3fb0537
18 changed files with 1526 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
<head>
<title>Polkupyöräsimulaattori</title>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap.no-icons.min.css" rel="stylesheet">
<link href="//netdna.bootstrapcdn.com/font-awesome/2.0/css/font-awesome.css" rel="stylesheet">
</head>
<body>
<div id="street" class="street"></div>
<div id="map" class="map"></div>
{{> main}}
</body>
<template name="main">
{{#if editing}}
{{> editing}}
{{/if}}
{{#if sim}}
{{> sim}}
{{/if}}
{{#if frontpage}}
{{> frontpage}}
{{/if}}
{{#if frontpage_edit}}
{{> frontpage}}
{{/if}}
{{#if help}}
{{> help}}
{{/if}}
{{#if settings}}
{{> settings}}
{{/if}}
<div class="navbar navbar-inverse navbar-fixed-bottom">
<div class="navbar-inner">
<a class="brand" href="/">Bicycle sim <span>v0.1</span></a>
<ul class="nav">
<li{{#if frontpage}} class="active"{{/if}}{{#if sim}} class="active"{{/if}}>
<a href="/">
<i class="icon-road"></i> Simulaatio
{{#if sim}} <span class="divider">/</span> {{sim.name}}{{/if}}
</a>
</li>
<li{{#if frontpage_edit}} class="active"{{/if}}{{#if editing}} class="active"{{/if}}>
<a href="/edit">
<i class="icon-wrench"></i> Muokkaa
{{#if editing}} <span class="divider">/</span> {{editing.name}}{{/if}}
</a>
</li>
<li{{#if settings}} class="active"{{/if}}><a href="/settings"><i class="icon-cogs"></i> Asetukset</a></li>
<li{{#if help}} class="active"{{/if}}><a href="/help"><i class="icon-question-sign"></i> Ohjeet</a></li>
</ul>
<ul class="nav pull-right">
<li class="login">{{loginButtons}}</li>
<li><a href="http://github.com/Ekokumppanit/Bicyclesim" target="new"><i class="icon-github"></i> Github</a></li>
</ul>
</div>
</div>
</template>
<template name="help">
<div class="help">
<div class="container">
<div class="fluid-row">
<h1>Polkupyöräsimulaattori Kuka, Mitä?</h1>
<p>
<a href="http://www.ekokumppanit.fi" target="new">Ekokumppanit Oy:n</a> työntekijä Juho Teperi on toteuttanut tämän prototyyppi web-sovelluksen
<a href="http://www.expomark.fi/fi/messut/energia2012" target="new">Energia 2012</a> messuja varten.
Ohjelmassa voit liikkua (polkea) ympäri maailmaa (tai ainakin valmiita reittejä).
</p>
<h1>Käyttö</h1>
<p>
Omalla koneellasi voit kokeilla toimintaa valitsemalla <em>Simulaatio</em>-välilehdeltä reitin ja hakkaamalla välilyöntiä.
Parhaan kokemuksen saat kun liität tietokoneeseen polkupyörän telineen <small>(traineri tai rolleri)</small> ja nopeusanturin <small>(laite joka lähettää näppäinpainalluksen kun rengas on pyörähtänyt kierroksen)</small> avulla.
</p>
<h1>Toteutus</h1>
<p>
Ohjelma on toteutettu käyttäen <a href="http://meteor.com" target="new">Meteor</a> JavaSript sovelluskehystä <small>(käyttää palvelinpuolella Node.js)</small>.
Kuvat tulevat Google Streetview palvelusta Googlen tarjoaman JavaScript rajapinnan kautta.
</p>
<h1>Vähän siistiä, mistä saan tämän itselleni?</h1>
<p>
Voit joko käyttää valmista ohjelmaa osoitteessa <a href="http://bicyclesim.ekokumppanit.fi">bicyclesim.ekokumppanit.fi</a> tai
hakea ohjelman lähdekoodin <a href="http://github.com/Ekokumppanit/Bicyclesim" target="new">Githubista</a>,
voit myös tehdä vapaasti muutoksia koodiin ja lähettää parannuksesi viralliseen versioon jotta muutkin hyötyvät niistä.
</p>
</div>
</div>
</div>
</template>
<template name="settings">
<div class="help">
<div class="container">
<div class="fluid-row">
<form>
<legend>Asetukset</legend>
<label>Liikekerroin. Vaikuttaa nopeuteen jolla reitillä liikutaan. Ei vaikuta nopeuslukemaan tai kuljettuun matkaan.</label>
<input type="text" value="{{settings.multiplier}}" id="settings_multiplier"/>
<label>Renkaan koko, tuumia</label>
<input type="text" value="{{settings.diameter}}" id="settings_diameter"/>
</form>
</div>
</div>
</div>
</template>
<template name="frontpage">
<div class="frontpage">
<div class="container">
<div class="fluid-row">
<ul class="thumbnails">
{{#each routes}}
<li class="span3">
{{#if editing}}
<div class="thumbnail">
<img src="{{staticmap}}" alt=""/>
<span>
{{#if editing_route_name}}
<div class="input"><input type="text" id="route-name" value="{{name}}"/></div>
{{else}}
{{name}}
{{#if can_edit}}<a href="#" class="remove"><i class="icon-remove-sign"></i></a>{{/if}}
{{/if}}
</span>
</div>
{{else}}
<a href="/{{_id}}" class="thumbnail">
<img src="{{staticmap}}" alt=""/>
<span>{{name}} <span>{{km route_length}}km</span></span>
</a>
{{/if}}
</li>
{{/each}}
{{#if editing}}
<li class="span3 new-route">
{{#if logged_in}}<div class="input"><input type="text" id="new-route" placeholder="New route"/></div>{{/if}}
</li>
{{/if}}
</ul>
</div>
<div class="fluid-row">
<a href="/help" class="btn btn-huge"><i class="icon-question-sign"></i> Ohjeet</a>
<a href="http://github.com/Ekokumppanit/Bicyclesim" class="btn btn-huge" target="new"><i class="icon-github"></i> Projekti Githubissa</a>
</div>
</div>
</div>
</template>
<template name="editing">
<div class="sidebar">
<div class="sidebar-inner">
<h3>Owner</h3>
<p>{{owner}}</p>
{{> points}}
</div>
</div>
</template>
<template name="sim">
<ul class="speedometer">
<li class="distance">{{km distance}} km</li>
<li class="speed">{{kmh speed}} <sup>km</sup>/<sub>h<sub></li>
<li class="calories"></li>
</ul>
</template>
<template name="distance">
<span>{{distance}}</span> km
</template>
<template name="speed">
<span>{{speed}}</span> <sup>km</sup>/<sub>h</sub>
</template>
<template name="calories">
<span>{{calories}}</span> cal
</template>
<template name="points">
<h3>Points
<span><label class="checkbox inline"><input type="checkbox" id="points-autoadd" {{#if autoadd}}checked="checked"{{/if}}/>Auto add points</label></span>
</h3>
<ol class="points">
{{#each points}}
<li class="point{{#if active}} active{{/if}}">
<span class="location">{{lat latlng}}, {{lng latlng}}</span>,
<span class="heading">{{round heading 2}}°</span>,
<span class="distance">{{round distance 2}}m</span>
{{#if can_edit}}<a href="#" class="remove"><i class="icon-remove-sign"></i></a>{{/if}}
</li>
{{/each}}
{{#if can_edit}}<li class="new-point"><button type="button" class="btn"><i class="icon-plus-sign"></i></button></li>{{/if}}
</ol>
</template>

View File

@@ -0,0 +1,216 @@
// --- Variables ---
$sidebar_width = 300px
// --- Functions ---
filter(args ...)
-webkit-filter args
-moz-filter args
filter args
// ---
body
overflow hidden
.navbar-inverse .brand span
color #FFB0B0
font-size 0.8em
.navbar-fixed-bottom
position absolute
.navbar-inner
padding-left 15px !important
padding-right 15px !important
i
color #fff
.sidebar
li
i
color #000
a:hover i
text-decoration none
.icon-remove, .icon-remove-sign
&:hover
color #FFAFAF
.btn.btn-huge
font-size 34px
line-height 34px
padding 12px 20px
margin-right 30px
.nav
.divider
color #666
padding 0 10px
.login
padding-top 6px
div.input
padding-right 14px
& > input
margin-bottom 0
width 100%
.thumbnail
background #fff
& > span
display block
background #000
background rgba(0, 0, 0, 0.7)
color #fff
padding 2px 10px
line-height 32px
font-weight bold
& > a
float right
& > span
font-weight normal
color #C8C8C8
float right
&:hover
text-decoration none
i
color #fff
a.thumbnail
&:hover
border-color #fff !important
box-shadow 0 0 25px #fff !important
.sidebar
position absolute
width $sidebar_width
top 0
bottom 42px
left 0
overflow-y auto
.sidebar-inner
padding 20px
.street, .map
img
max-width none
border none !important
.street
position absolute
left 0
right 0
bottom 42px
top 0
z-index 0
.map
position absolute
width 300px
height 250px
left 0
top 0
opacity 0.8
display none
.street
filter(blur(4px))
.show-map
.street
filter(blur(0))
.map
display block
.show-sidebar
.street, .map
left $sidebar_width
.frontpage, .help
z-index 100
position absolute
top 0
bottom 42px
left 0
right 0
background rgba(0, 0, 0, 0.5)
padding-top 30px
overflow-y auto
.help
background #fff
font-size 1.5em
line-height 1.5em
h1
margin-top 40px
.sidebar
h3
span
float right
ol.points
margin 0
li
font-size 0.9em
border-top 1px solid #ccc
padding 2px 0px
list-style-position inside
&:first-child, &.new-point
border-top 0
&.new-point
list-style none
span.location
font-size 0.8em
a.remove
float right
.speedometer
position absolute
top 0
right 0
background rgba(0, 0, 0, 0.6)
width 190px
padding 15px
border-bottom-left-radius 25px
li
list-style none
text-align right
font-size 2.5em
color #fff
font-weight bold
line-height 150%
::-webkit-scrollbar
background transparent
border none
::-webkit-scrollbar:vertical
border-width 0 0 0 1px
width 11px
::-webkit-scrollbar-corner
background transparent
/* These rules are for scrollbar draggable panel */
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.6)
box-shadow 0 0 2px rgba(0, 0, 0, 0.9) inset
::-webkit-scrollbar-thumb:hover
background-color rgba(0, 0, 0, 0.8)
::-webkit-scrollbar-thumb:active
background-color rgba(0, 0, 0, 1)
/* These rules are for buttons */
::-webkit-scrollbar-button:start
display none
::-webkit-scrollbar-button:end
display none

188
client/edit.js Normal file
View File

@@ -0,0 +1,188 @@
Session.set('points-autoadd', false);
var full_clear_required = true;
function add_point(latlng, num, trigger) {
maps.lines.route.add(latlng);
maps.markers.add(latlng, {type: 'icon', text: num});
}
var num = 1;
function new_point(latlng) {
Meteor.call('insert_point', latlng, Session.get('route'), function () {
add_point(latlng, num, true);
++num;
$('.sidebar').animate({scrollTop: $('.sidebar-inner').height()}, 'fast');
});
}
// --- Route management ---
Template.frontpage.editing_route_name = function () {
return Session.equals('editing_route_name', this._id);
};
Template.frontpage.can_edit = function () {
var route = Routes.findOne({_id: this._id});
return (route && Meteor.user() && route.owner === Meteor.user()._id);
};
Template.frontpage.logged_in = function () {
return Meteor.user();
};
Template.frontpage.events({
'click div.thumbnail img': function (event, template) {
Router.navigate('/edit/' + this._id, {trigger: true});
},
'click a.remove': function () {
Points.remove({route: this._id});
Routes.remove({_id: this._id});
},
'dblclick div.thumbnail span': function (event, template) {
if (Meteor.user() && this.owner === Meteor.user()._id) {
Session.set('editing_route_name', this._id);
Meteor.flush();
activateInput(template.find('#route-name'));
}
}
});
Template.frontpage.events(
okCancelEvents('#new-route', {
ok: function (text, event) {
Routes.insert({owner: Meteor.user()._id, name: text, first: null});
event.target.value = "";
}
})
);
Template.frontpage.events(
okCancelEvents('#route-name', {
ok: function (text) {
Routes.update({_id: this._id}, {$set: {name: text}});
Session.set('editing_route_name', null);
},
cancel: function () {
Session.set('editing_route_name', null);
}
})
);
// --- Points ---
Template.points.points = function () {
var points = [];
var route = Routes.findOne({_id: Session.get('route')});
if (route) {
var point = Points.findOne({_id: route.first});
while (point !== undefined) {
points.push(point);
point = Points.findOne({_id: point.next});
}
}
return points;
};
Template.points.autoadd = function () {
return Session.get('points-autoadd');
};
Template.points.can_edit = function () {
var route = Routes.findOne({_id: Session.get('route')});
return (route && Meteor.user() && route.owner === Meteor.user()._id);
};
Template.points.helpers(helpers);
Template.points.events({
'click li.point': function () {
maps.travel(this._id, {center: true});
},
'click a.remove': function () {
Meteor.call('remove_point', this._id, function () {
full_clear_required = true;
});
},
'click .new-point button': function () {
new_point(maps.getLatLng());
},
'change #points-autoadd': function (event) {
Session.set('points-autoadd', event.currentTarget.checked);
}
});
Template.editing.owner = function () {
var route = Routes.findOne({_id: Session.get('route')});
var user = Meteor.users.findOne({_id: route.owner});
if (!user) return;
return user.profile.name;
};
function init_edit() {
Meteor.autorun(function () {
if (Session.equals('page', 'edit') && Session.get('route')) {
debug('sivu tai route vaihtui, route: ' + Session.get('route'));
full_clear_required = true;
Meteor.deps.isolate(function () {
var route = Routes.findOne({_id: Session.get('route')});
if (route) {
maps.travel(route.first, {route: true});
} else {
maps.default_pos();
}
});
}
});
Meteor.autosubscribe(function () {
Session.set('points-autoadd', false);
if (Session.equals('page', 'edit') && Session.get('route')) {
if (full_clear_required) {
Meteor.deps.isolate(function () {
debug('full clear');
maps.lines.route.clear();
maps.markers.clear();
num = 1;
var route = Routes.findOne({_id: Session.get('route')});
if (route) {
var point = Points.findOne({_id: route.first});
while (point !== undefined) {
add_point(point.latlng, num, false);
point = Points.findOne({_id: point.next});
++num;
}
full_clear_required = false;
}
});
}
}
});
function autoadd_listener() {
var route = Routes.findOne({_id: Session.get('route')});
if (Session.get('points-autoadd') && route && Meteor.user() && Meteor.user()._id === route.owner) {
var latlng = maps.getLatLng();
// Do not add duplicate points.
if (Points.findOne({route: route._id, latlng: latlng}) !== undefined) return;
new_point(latlng);
}
}
window.autoadd_listener = null;
Meteor.autosubscribe(function () {
if (Session.get('points-autoadd')) {
maps.listeners.add('points-autoadd', 'street', 'position_changed', autoadd_listener);
} else {
maps.listeners.remove('points-autoadd');
}
});
}

106
client/jquery.hotkeys.js Normal file
View File

@@ -0,0 +1,106 @@
/*
* jQuery Hotkeys Plugin
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Based upon the plugin by Tzury Bar Yochay:
* http://github.com/tzuryby/hotkeys
*
* Original idea by:
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
*/
(function(jQuery){
jQuery.hotkeys = {
version: "0.8+",
specialKeys: {
8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 188: ",", 190: ".",
191: "/", 224: "meta"
},
shiftNums: {
"`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
"8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
".": ">", "/": "?", "\\": "|"
}
};
function keyHandler( handleObj ) {
var origHandler = handleObj.handler,
//use namespace as keys so it works with event delegation as well
//will also allow removing listeners of a specific key combination
//and support data objects
keys = (handleObj.namespace || "").toLowerCase().split(" ");
keys = jQuery.map(keys, function(key) { return key.split("."); });
//no need to modify handler if no keys specified
if (keys.length === 1 && (keys[0] === "" || keys[0] === "autocomplete")) {
return;
}
handleObj.handler = function( event ) {
// Don't fire in text-accepting inputs that we didn't directly bind to
// important to note that $.fn.prop is only available on jquery 1.6+
if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) ||
event.target.type === "text" || $(event.target).prop('contenteditable') == 'true' )) {
return;
}
// Keypress represents characters, not special keys
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
character = String.fromCharCode( event.which ).toLowerCase(),
key, modif = "", possible = {};
// check combinations (alt|ctrl|shift+anything)
if ( event.altKey && special !== "alt" ) {
modif += "alt_";
}
if ( event.ctrlKey && special !== "ctrl" ) {
modif += "ctrl_";
}
// TODO: Need to make sure this works consistently across platforms
if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
modif += "meta_";
}
if ( event.shiftKey && special !== "shift" ) {
modif += "shift_";
}
if ( special ) {
possible[ modif + special ] = true;
} else {
possible[ modif + character ] = true;
possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true;
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
if ( modif === "shift_" ) {
possible[ jQuery.hotkeys.shiftNums[ character ] ] = true;
}
}
for ( var i = 0, l = keys.length; i < l; i++ ) {
if ( possible[ keys[i] ] ) {
return origHandler.apply( this, arguments );
}
}
};
}
jQuery.each([ "keydown", "keyup", "keypress" ], function() {
jQuery.event.special[ this ] = { add: keyHandler };
});
})( jQuery );

46
client/lib/helpers.js Normal file
View File

@@ -0,0 +1,46 @@
helpers = {
round: function (num, dig) {
return parseFloat(num).toFixed(dig);
},
lat: function (latlng) {
return latlng[0].toFixed(5);
},
lng: function (latlng) {
return latlng[1].toFixed(5);
}
};
var activateInput = function (input) {
input.focus();
input.select();
};
var okCancelEvents = function (selector, callbacks) {
var ok = callbacks.ok || function () {};
var cancel = callbacks.cancel || function () {};
var events = {};
events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
function (evt) {
if (evt.type === "keydown" && evt.which === 27) {
// escape = cancel
cancel.call(this, evt);
} else if (evt.type === "keyup" && evt.which === 13 ||
evt.type === "focusout") {
// blur/return/enter = ok/submit if non-empty
var value = String(evt.target.value || "");
if (value)
ok.call(this, value, evt);
else
cancel.call(this, evt);
}
};
return events;
};
// From: https://github.com/tmeasday/meteor-deps-extensions
Meteor.deps.isolate = function(fn) {
var context = new Meteor.deps.Context();
return context.run(fn);
};

180
client/lib/maps.js Normal file
View File

@@ -0,0 +1,180 @@
function maps_loaded() {
window.markers = []; // fuu
var Maps = function () {
function glatlng(latlng) {
return new google.maps.LatLng(latlng[0], latlng[1]);
}
var Line = function (attr) {
attr = attr || {};
var data = new google.maps.Polyline(attr);
return {
clear: function () {
data.setPath([]);
},
add: function (latlng) {
data.getPath().push(glatlng(latlng));
}
};
};
var Markers = function (map) {
var data = [];
return {
clear: function () {
for (var i = data.length - 1; i >= 0; --i) {
data[i].setMap(null);
}
data.length = 0;
},
add: function (latlng, arg) {
var attr = {
map: map,
position: glatlng(latlng)
};
if (arg.type == 'icon') {
attr['icon'] = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + arg.text + '|FF0000|000000';
}
data.push(new google.maps.Marker(attr));
}
};
};
var Listeners = function (instances) {
var handles = {};
return {
add: function (id, instance, event, cb) {
if (_.has(handles, id)) {
google.maps.event.removeListener(handles[id]);
}
handles[id] = google.maps.event.addListener(instances[instance], event, cb);
},
remove: function (id) {
google.maps.event.removeListener(handles[id]);
}
};
};
var street = new google.maps.StreetViewPanorama(document.getElementById("street"), {
position: glatlng(settings.default_latlng),
pov: settings.default_pov,
panControl: false,
imageDateControl: false,
scrollwheel: false,
zoomControl: false,
addressControl: false
});
var map = new google.maps.Map(document.getElementById("map"), {
center: glatlng(settings.default_latlng),
mapTypeId: google.maps.MapTypeId.ROADMAP,
zoom: 15,
mapTypeControl: false,
draggable: false,
streetView: street,
streetViewControl: false,
scrollwheel: false,
zoomControl: false
});
window.map = map;
var bicycling = new google.maps.BicyclingLayer();
bicycling.setMap(map);
var lines = {
'route': new Line({
map: map,
strokeColor: '#51B5FF'
}),
'traveled': new Line({
map: map
})
};
var markers = new Markers(map);
var listeners = new Listeners({
map: map,
street: street
});
return {
lines: lines,
markers: markers,
listeners: listeners,
getLatLng: function () {
return [street.getPosition().lat(), street.getPosition().lng()];
},
mode: function (mode) {
if (mode == 'edit') {
map.setOptions({
scrollwheel: true,
draggable: true,
streetViewControl: true
});
street.setOptions({
clickToGo: true,
linksControl: true
});
} else if (mode == 'sim') {
map.setOptions({
scrollwheel: false,
draggable: false,
streetViewControl: false
});
street.setOptions({
clickToGo: false,
linksControl: false
});
}
},
default_pos: function () {
street.setPosition(glatlng(settings.default_latlng));
street.setPov(settings.default_pov);
},
/*
* Travel to a location.
* Updates streetview.
* Optionally centers the map.
*/
travel: function (point_id, arg) {
arg = arg || {};
_.defaults(arg, {
route: false
});
var point = Points.findOne({_id: point_id});
if (point) {
street.setPosition(glatlng(point.latlng));
map.setCenter(glatlng(point.latlng));
if (point.heading) street.setPov({zoom: 1, pitch: 0, heading: point.heading});
if (arg.route) lines.traveled.add(point.latlng);
}
}
};
};
$(document).ready(function () {
window.maps = new Maps();
init_main();
init_sim();
init_edit();
});
}
window.onload = function () {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "http://maps.googleapis.com/maps/api/js?key=" + settings.maps_key + "&sensor=false&callback=maps_loaded";
document.body.appendChild(script);
};

35
client/lib/settings.js Normal file
View File

@@ -0,0 +1,35 @@
window.settings = {
'default_latlng': [61.501043, 23.763035],
'default_pov': {
heading: 200,
pitch: 0,
zoom: 1
},
'debug': false,
'maps_key': 'AIzaSyDtGhiAnSdg9TaGZC_daNcQe43BS8Ws7Iw',
'staticmaps_key': 'AIzaSyDtGhiAnSdg9TaGZC_daNcQe43BS8Ws7Iw'
};
window.c = function () {
return Math.PI * localStorage.diameter * 2.54 / 100;
};
if (_.has(localStorage, 'diameter') && !_.isNumber(localStorage.diameter)) {
delete localStorage.diameter;
}
if (_.has(localStorage, 'multiplier') && !_.isNumber(localStorage.multiplier)) {
delete localStorage.multiplier;
}
_.defaults(localStorage, {
diameter: 28,
multiplier: 2.5
});
window.debug = function () {
if (this.console && settings.debug) {
console.log(Array.prototype.slice.call(arguments));
}
};

185
client/main.js Normal file
View File

@@ -0,0 +1,185 @@
// --- Body classes ---
Meteor.autosubscribe(function () {
if (Session.equals('page', 'edit')) {
$("body").addClass('show-sidebar');
} else {
$("body").removeClass('show-sidebar');
}
if (Session.equals('page', 'sim') || Session.equals('page', 'edit')) {
$("body").addClass('show-map');
} else {
$("body").removeClass('show-map');
}
});
$(document).ready(function () {
if (Session.equals('page', 'edit')) {
$("body").addClass('show-sidebar');
}
if (Session.equals('page', 'sim') || Session.equals('page', 'edit')) {
$("body").addClass('show-map');
}
});
// --- Main ---
Template.main.frontpage = function () {
return Session.equals('page', 'frontpage');
};
Template.main.frontpage_edit = function () {
return Session.equals('page', 'frontpage_edit');
};
Template.main.editing = function () {
if (Session.equals('page', 'edit')) {
return Routes.findOne({_id: Session.get('route')});
}
return false;
};
Template.main.help = function () {
return Session.equals('page', 'help');
};
Template.main.settings = function () {
return Session.equals('page', 'settings');
};
Template.main.sim = function () {
if (Session.equals('page', 'sim')) {
return Routes.findOne({_id: Session.get('route')});
}
return false;
};
// --- Frontpage ---
Template.frontpage.routes = function () {
return Routes.find();
};
Template.frontpage.editing = function () {
return Session.equals('page', 'frontpage_edit');
};
Template.frontpage.staticmap = function () {
return 'http://maps.googleapis.com/maps/api/staticmap?path=enc:' + this.path + '&size=360x270&key=' + settings.staticmaps_key + '&sensor=false';
};
Template.frontpage.helpers({
'km': function (m) {
return (m / 1000).toFixed(2);
}
});
// --- Settings ---
Template.settings.settings = function () {
return localStorage;
};
Template.settings.events({
'change #settings_multiplier': function (event) {
localStorage.setItem('multiplier', parseFloat(event.target.value));
},
'change #settings_diameter': function (event) {
localStorage.setItem('diameter', parseInt(event.target.value, 10));
}
});
// --- Keybinds ---
$(document).bind('keydown.esc', function () {
Router.navigate('/', {trigger: true});
});
// --- Routes ---
$(document).on("click", "a[href^='/']", function (event) {
var href = $(event.currentTarget).attr('href');
// chain 'or's for other black list routes
var passThrough = href.indexOf('sign_out') >= 0;
// Allow shift+click for new tabs, etc.
if (!passThrough && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
event.preventDefault();
// Remove leading slashes and hash bangs (backward compatablility)
var url = href.replace('/^\//', '').replace('#!', '');
// Instruct Backbone to trigger routing events
Router.navigate(url, {trigger: true});
return false;
}
});
var RoutesRouter = Backbone.Router.extend({
routes: {
"": "index",
"help": "help",
"settings": "settings",
"edit": "index_edit",
":route_id": "sim",
"edit/:route_id": "edit"
},
index: function () {
debug('frontpage');
Session.set('page', 'frontpage');
Session.set('route', null);
},
help: function () {
debug('frontpage');
Session.set('page', 'help');
Session.set('route', null);
},
settings: function () {
debug('frontpage');
Session.set('page', 'settings');
Session.set('route', null);
},
index_edit: function () {
debug('frontpage edit');
Session.set('page', 'frontpage_edit');
Session.set('route', null);
},
sim: function (route_id) {
debug('simulation, route: ' + route_id);
Session.set('page', 'sim');
Session.set('route', route_id);
},
edit: function (route_id) {
debug('edit, route: ' + route_id);
Session.set('page', 'edit');
Session.set('route', route_id);
}
});
Router = new RoutesRouter();
Meteor.startup(function () {
Backbone.history.start({pushState: true});
});
function init_main() {
Meteor.autosubscribe(function () {
// When changing page
maps.markers.clear();
maps.lines.traveled.clear();
maps.lines.route.clear();
if (Session.equals('page', 'frontpage') || Session.equals('page', 'frontpage_edit')) {
maps.default_pos();
}
if (Session.equals('page', 'edit')) {
maps.mode('edit');
} else {
maps.mode('sim');
}
});
}

101
client/main.sim.js Normal file
View File

@@ -0,0 +1,101 @@
window.point = null;
window.traveled = 0;
var createRingBuffer = function (length){
var pointer = 0, buffer = [], sum = 0;
return {
push: function (item) {
if (buffer[pointer] > 0) {
sum -= buffer[pointer];
if (sum <= 0) sum = 0;
}
buffer[pointer] = item;
sum += item;
pointer = (length + pointer + 1) % length;
},
sum: function () {
return sum;
}
};
};
var revs = 0;
$(document).bind('keydown.space', function () {
revs += 1;
var dist = localStorage['multiplier'] * c();
Session.set('distance', Session.get('distance') + dist);
window.traveled += dist;
if (window.traveled >= window.point.distance) {
var next = Points.findOne({_id: window.point.next});
// Go to next point, if one exists
if (next) {
window.traveled -= next.distance;
maps.travel(next._id, {route: true});
window.point = next;
}
}
});
function speedo() {
speed_buffer.push(revs * c());
revs = 0;
Session.set('speed', speed_buffer.sum() / 5);
}
setInterval(speedo, 500);
// 5sec, 2 values / sec.
var speed_buffer = createRingBuffer(5 * 2);
var speed_sum = 0;
var speed_avg = 0;
Template.sim.speed = function () {
return Session.get('speed');
};
Template.sim.distance = function () {
return Session.get('distance');
};
Template.sim.helpers({
kmh: function (ms) {
return (ms * 60 * 60 / 1000).toFixed(1);
},
km: function (m) {
return (m / 1000).toFixed(2);
}
});
function init_sim() {
Meteor.autosubscribe(function () {
if (Session.equals('page', 'sim')) {
var route = Routes.findOne({_id: Session.get('route')});
if (route) {
Session.set('distance', 0);
Session.set('speed', 0);
window.traveled = 0;
maps.lines.traveled.clear();
maps.lines.route.clear();
// Siirytään reitin alkuun
window.point = Points.findOne({_id: route.first});
maps.travel(route.first, {route: true});
// Show full route on map
var p = window.point;
while (p !== undefined) {
maps.lines.route.add(p.latlng);
p = Points.findOne({_id: p.next});
}
}
}
});
}