mirror of
https://github.com/Ekokumppanit/Bicyclesim.git
synced 2026-01-26 03:04:09 +00:00
Initial commit.
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.sublime-workspace
|
||||||
|
.cache
|
||||||
|
*node_modules*
|
||||||
1
.meteor/.gitignore
vendored
Normal file
1
.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
13
.meteor/packages
Normal file
13
.meteor/packages
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Meteor packages used by this project, one per line.
|
||||||
|
#
|
||||||
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
|
autopublish
|
||||||
|
preserve-inputs
|
||||||
|
jquery
|
||||||
|
backbone
|
||||||
|
stylus
|
||||||
|
accounts-google
|
||||||
|
underscore
|
||||||
|
accounts-ui
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright (c) 2012 Juho Teperi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Bicyclesim
|
||||||
|
|
||||||
|
This is a prototype web app developed to be demonstrated at Energia 2012 expo
|
||||||
|
(http://www.expomark.fi/fi/messut/energia2012).
|
||||||
|
|
||||||
|
This application is built using Meteor JavaScript platform (http://meteor.com/).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can try the app at http://bicyclesim.ekokumppanit.fi/. Instructions there are
|
||||||
|
currently on finnish so here's a short version:
|
||||||
|
|
||||||
|
Select a route from "Simulaatio"-tab and start hitting the space key.
|
||||||
|
For the best experience connect a bicycle with a trainer to computer using
|
||||||
|
a speed sensor.
|
||||||
|
|
||||||
|
## Speed sensor
|
||||||
|
|
||||||
|
Most basic speed sensor would be "Keyboard" that sends a keypress whenever
|
||||||
|
the wheel has turned one revolution.
|
||||||
|
This kind of sensor can be built from a old keyboard by soldering a reed switch
|
||||||
|
to right conductors at keyboard circuit board. Some instructions here:
|
||||||
|
http://www.instructables.com/id/Hacking-a-USB-Keyboard/.
|
||||||
|
|
||||||
|
It is also possible to built equivalent device from Arduino Uno:
|
||||||
|
http://mitchtech.net/arduino-usb-hid-keyboard/.
|
||||||
|
Or more easily with the new Arduino Due:
|
||||||
|
http://www.i-programmer.info/news/91-hardware/4965-new-powerful-arduino-due-.html.
|
||||||
40
bicyclesim.js
Normal file
40
bicyclesim.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
var Routes = new Meteor.Collection('routes');
|
||||||
|
var Points = new Meteor.Collection('points');
|
||||||
|
|
||||||
|
Routes.allow({
|
||||||
|
insert: function (userId, doc) {
|
||||||
|
// logged in + owner of route
|
||||||
|
return (userId && doc.owner === userId);
|
||||||
|
},
|
||||||
|
update: function (userId, docs, fields, modifier) {
|
||||||
|
return _.all(docs, function (doc) {
|
||||||
|
return doc.owner === userId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: function (userId, docs) {
|
||||||
|
return _.all(docs, function (doc) {
|
||||||
|
return doc.owner === userId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetch: ['owner']
|
||||||
|
});
|
||||||
|
|
||||||
|
Points.allow({
|
||||||
|
insert: function (userId, doc) {
|
||||||
|
var route = Routes.findOne({_id: doc.route});
|
||||||
|
return (userId && route && route.owner === userId);
|
||||||
|
},
|
||||||
|
update: function (userId, docs, fields, modifier) {
|
||||||
|
return _.all(docs, function (doc) {
|
||||||
|
var route = Routes.findOne({_id: doc.route});
|
||||||
|
return (route && route.owner === userId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: function (userId, docs) {
|
||||||
|
return _.all(docs, function (doc) {
|
||||||
|
var route = Routes.findOne({_id: doc.route});
|
||||||
|
return (route && route.owner === userId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetch: ['route']
|
||||||
|
});
|
||||||
20
bicyclesim.sublime-project
Normal file
20
bicyclesim.sublime-project
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"folders":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file_exclude_patterns":
|
||||||
|
[
|
||||||
|
"*.sublime-workspace"
|
||||||
|
],
|
||||||
|
"folder_exclude_patterns":
|
||||||
|
[
|
||||||
|
".cache",
|
||||||
|
".meteor"
|
||||||
|
],
|
||||||
|
"path": "/home/juho/Source/bicyclesim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/home/juho/Source/bicyclesim-bundle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
197
client/bicyclesim-meteor.html
Normal file
197
client/bicyclesim-meteor.html
Normal 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>
|
||||||
216
client/bicyclesim-meteor.styl
Normal file
216
client/bicyclesim-meteor.styl
Normal 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
188
client/edit.js
Normal 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
106
client/jquery.hotkeys.js
Normal 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
46
client/lib/helpers.js
Normal 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
180
client/lib/maps.js
Normal 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
35
client/lib/settings.js
Normal 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
185
client/main.js
Normal 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
101
client/main.sim.js
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
94
server/lib/maps.js
Normal file
94
server/lib/maps.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Latitude/longitude spherical geodesy formulae & scripts (c) Chris Veness 2002-2012
|
||||||
|
* From: http://www.movable-type.co.uk/scripts/latlong.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (typeof Number.prototype.toRad == 'undefined') {
|
||||||
|
Number.prototype.toRad = function() {
|
||||||
|
return this * Math.PI / 180;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Number.prototype.toDeg == 'undefined') {
|
||||||
|
Number.prototype.toDeg = function() {
|
||||||
|
return this * 180 / Math.PI;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDistance(from, to) {
|
||||||
|
// Equirectangular approximation, should be enough as distances are small
|
||||||
|
var R = 6371000; // km
|
||||||
|
var x = (to[1].toRad() - from[1].toRad()) * Math.cos((from[0].toRad() + to[0].toRad()) / 2);
|
||||||
|
var y = (from[0].toRad() - to[0].toRad());
|
||||||
|
return Math.sqrt(x * x + y * y) * R;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeInitialBearing(from, to) {
|
||||||
|
var dLat = (to[0] - from[0]).toRad();
|
||||||
|
var dLon = (to[1] - from[1]).toRad();
|
||||||
|
var lat1 = from[0].toRad();
|
||||||
|
var lat2 = to[0].toRad();
|
||||||
|
|
||||||
|
var y = Math.sin(dLon) * Math.cos(lat2);
|
||||||
|
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
|
||||||
|
return (Math.atan2(y, x).toDeg() + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeFinalBearing(from, to) {
|
||||||
|
return (computeInitialBearing(to, from) + 180) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest are copied from: https://github.com/moshen/node-googlemaps/blob/master/lib/googlemaps.js
|
||||||
|
function createEncodedPolyline(points) {
|
||||||
|
// Dear maintainer:
|
||||||
|
//
|
||||||
|
// Once you are done trying to 'optimize' this routine,
|
||||||
|
// and have realized what a terrible mistake that was,
|
||||||
|
// please increment the following counter as a warning
|
||||||
|
// to the next guy:
|
||||||
|
//
|
||||||
|
// total_hours_wasted_here = 11
|
||||||
|
//
|
||||||
|
var i, dlat, dlng;
|
||||||
|
var plat = 0;
|
||||||
|
var plng = 0;
|
||||||
|
var encoded_points = "";
|
||||||
|
if(typeof points === 'string'){
|
||||||
|
points = points.split('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
for(i = 0; i < points.length; i++) {
|
||||||
|
var point = points[i];//.split(',');
|
||||||
|
var lat = point[0];
|
||||||
|
var lng = point[1];
|
||||||
|
var late5 = Math.round(lat * 1e5);
|
||||||
|
var lnge5 = Math.round(lng * 1e5);
|
||||||
|
dlat = late5 - plat;
|
||||||
|
dlng = lnge5 - plng;
|
||||||
|
plat = late5;
|
||||||
|
plng = lnge5;
|
||||||
|
encoded_points += encodeSignedNumber(dlat) + encodeSignedNumber(dlng);
|
||||||
|
}
|
||||||
|
return encoded_points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeNumber(num) {
|
||||||
|
var encodeString = "";
|
||||||
|
var nextValue, finalValue;
|
||||||
|
while (num >= 0x20) {
|
||||||
|
nextValue = (0x20 | (num & 0x1f)) + 63;
|
||||||
|
encodeString += (String.fromCharCode(nextValue));
|
||||||
|
num >>= 5;
|
||||||
|
}
|
||||||
|
finalValue = num + 63;
|
||||||
|
encodeString += (String.fromCharCode(finalValue));
|
||||||
|
return encodeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeSignedNumber(num) {
|
||||||
|
var sgn_num = num << 1;
|
||||||
|
if (num < 0) {
|
||||||
|
sgn_num = ~(sgn_num);
|
||||||
|
}
|
||||||
|
return(encodeNumber(sgn_num));
|
||||||
|
}
|
||||||
66
server/main.js
Normal file
66
server/main.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
function update_route(route_id) {
|
||||||
|
var path = [];
|
||||||
|
var route = Routes.findOne({_id: route_id});
|
||||||
|
var len = 0;
|
||||||
|
if (route) {
|
||||||
|
var point = Points.findOne({_id: route.first});
|
||||||
|
var next = null;
|
||||||
|
var heading = 0;
|
||||||
|
while (point !== undefined) {
|
||||||
|
path.push(point.latlng);
|
||||||
|
|
||||||
|
next = Points.findOne({_id: point.next});
|
||||||
|
|
||||||
|
var distance = 0;
|
||||||
|
// Uses previous heading if no next point
|
||||||
|
if (next) {
|
||||||
|
heading = computeFinalBearing(point.latlng, next.latlng);
|
||||||
|
distance = computeDistance(point.latlng, next.latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
len += distance;
|
||||||
|
Points.update({_id: point._id}, {$set: {
|
||||||
|
heading: heading,
|
||||||
|
distance: distance
|
||||||
|
}});
|
||||||
|
|
||||||
|
point = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Routes.update({_id: route_id}, {$set: {
|
||||||
|
path: createEncodedPolyline(path),
|
||||||
|
route_length: len
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
insert_point: function (latlng, route_id) {
|
||||||
|
var route = Routes.findOne({_id: route_id});
|
||||||
|
if (!route) return;
|
||||||
|
|
||||||
|
var point_id = Points.insert({latlng: latlng, route: route_id, distance: 0, next: null});
|
||||||
|
|
||||||
|
// Add new point after last point.
|
||||||
|
Points.update({route: route_id, next: null, _id: {$ne: point_id}}, {$set: {next: point_id}});
|
||||||
|
|
||||||
|
// If route didn't have any point, update first point,
|
||||||
|
Routes.update({_id: route_id, first: null}, {$set: {first: point_id}});
|
||||||
|
|
||||||
|
update_route(route_id);
|
||||||
|
},
|
||||||
|
remove_point: function (point_id) {
|
||||||
|
var point = Points.findOne({_id: point_id});
|
||||||
|
// Update previous point to point to point after removed one
|
||||||
|
Points.update({next: point._id}, {$set: {next: point.next}});
|
||||||
|
|
||||||
|
// Point was first of route, update routes first to removed points next.
|
||||||
|
Routes.update({first: point._id}, {$set: {first: point.next}});
|
||||||
|
|
||||||
|
Points.remove({_id: point_id});
|
||||||
|
|
||||||
|
update_route(point.route);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user