commit fbf8fb087a916d61275cbdf0556b864291d06428 Author: Juho Teperi Date: Mon May 13 11:43:34 2013 +0300 Initial commit diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..f594df7 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "app/components" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5268735 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_size = 4 + + +[*.md] +trim_trailing_whitespace = false + +[*.sublime-project] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f21884f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.tmp +app/components +*.sublime-workspace diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..9af1726 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,26 @@ +{ + "node": true, + "browser": true, + "es5": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + "$": true, + "define": true + } +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..7134df9 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,320 @@ +'use strict'; +var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet; +var mountFolder = function (connect, dir) { + return connect.static(require('path').resolve(dir)); +}; + +module.exports = function (grunt) { + // load all grunt tasks + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); + + // configurable paths + var yeomanConfig = { + app: 'app', + dist: 'dist' + }; + + grunt.initConfig({ + yeoman: yeomanConfig, + watch: { + less: { + files: ['<%= yeoman.app %>/styles/*.less'], + tasks: ['less'] + }, + livereload: { + files: [ + '<%= yeoman.app %>/*.html', + '{.tmp,<%= yeoman.app %>}/styles/*.css', + '{.tmp,<%= yeoman.app %>}/scripts/**/*.js', + '<%= yeoman.app %>/images/*.{png,jpg,jpeg,webp}' + ], + tasks: ['livereload'] + }, + handlebars: { + files: [ + 'app/templates/**/*.hbs' + ], + tasks: ['handlebars', 'livereload'] + } + }, + handlebars: { + compile: { + files: { + '.tmp/scripts/templates.js': [ + 'app/templates/**/*.hbs' + ] + }, + options: { + // namespace: false, + amd: true, + processName: function (filename) { + return filename.replace(/^app\/templates\//, '').replace(/\.hbs/, ''); + } + } + } + }, + connect: { + options: { + port: 9000, + // change this to '0.0.0.0' to access the server from outside + hostname: 'localhost' + }, + livereload: { + options: { + middleware: function (connect) { + return [ + lrSnippet, + mountFolder(connect, '.tmp'), + mountFolder(connect, 'app') + ]; + } + } + }, + dist: { + options: { + middleware: function (connect) { + return [ + mountFolder(connect, 'dist') + ]; + } + } + } + }, + open: { + server: { + url: 'http://localhost:<%= connect.options.port %>' + } + }, + clean: { + dist: ['.tmp', '<%= yeoman.dist %>/*'], + server: '.tmp' + }, + jshint: { + options: { + jshintrc: '.jshintrc' + }, + all: [ + 'Gruntfile.js', + '<%= yeoman.app %>/scripts/*.js' + ] + }, + less: { + dist: { + options: { + paths: ['app/components'], + yuicompress: true, + strictUnits: false, + strictMaths: false + }, + files: { + '.tmp/styles/main.css': '<%= yeoman.app %>/styles/main.less' + } + }, + server: { + options: { + paths: ['app/components'], + strictUnits: false, + strictMaths: false + }, + files: { + '.tmp/styles/main.css': '<%= yeoman.app %>/styles/main.less' + } + } + }, + requirejs: { + dist: { + // Options: https://github.com/jrburke/r.js/blob/master/build/example.build.js + options: { + // `name` and `out` is set by grunt-usemin + baseUrl: 'app/scripts', + paths: { + 'bootstrap': '../../.tmp/scripts/bootstrap', + 'Template': '../../.tmp/scripts/templates' + }, + optimize: 'none', + // TODO: Figure out how to make sourcemaps work with grunt-usemin + // https://github.com/yeoman/grunt-usemin/issues/30 + //generateSourceMaps: true, + // required to support SourceMaps + // http://requirejs.org/docs/errors.html#sourcemapcomments + preserveLicenseComments: false, + useStrict: true, + wrap: true, + include: [ + 'libs/gmaps' + ] + } + } + }, + useminPrepare: { + html: '<%= yeoman.app %>/index.html', + options: { + dest: '<%= yeoman.dist %>' + } + }, + usemin: { + html: ['<%= yeoman.dist %>/*.html'], + css: ['<%= yeoman.dist %>/styles/*.css'], + options: { + dirs: ['<%= yeoman.dist %>'] + } + }, + imagemin: { + dist: { + files: [{ + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '*.{png,jpg,jpeg}', + dest: '<%= yeoman.dist %>/images' + }] + } + }, + cssmin: { + dist: { + files: { + '<%= yeoman.dist %>/styles/main.css': [ + '.tmp/styles/*.css', + '<%= yeoman.app %>/styles/*.css' + ] + } + } + }, + htmlmin: { + dist: { + options: { + /*removeCommentsFromCDATA: true, + // https://github.com/yeoman/grunt-usemin/issues/44 + //collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeOptionalTags: true*/ + }, + files: [{ + expand: true, + cwd: '<%= yeoman.app %>', + src: '*.html', + dest: '<%= yeoman.dist %>' + }] + } + }, + copy: { + dist: { + files: [{ + expand: true, + dot: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>', + src: [ + 'api/*', + '*.{ico,txt}', + '.htaccess', + ] + }, { + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>/font', + src: [ + 'components/font-awesome/build/assets/font-awesome/font/*.{ttf,woff,otf,eot,svg}' + ] + }, { + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>/styles', + src: [ + 'components/select2/*.{png,gif}' + ] + }] + }, + livereload: { + files: [{ + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '.tmp/font', + src: [ + 'components/font-awesome/build/assets/font-awesome/font/*.{ttf,woff,otf,eot,svg}' + ] + }, { + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '.tmp/styles', + src: [ + 'components/select2/*.{png,gif}' + ] + }] + } + }, + bower: { + all: { + rjsConfig: '<%= yeoman.app %>/scripts/main.js' + } + }, + concat: { + bootstrap: { + src: [ + 'app/components/bootstrap/js/bootstrap-transition.js', + 'app/components/bootstrap/js/bootstrap-alert.js', + 'app/components/bootstrap/js/bootstrap-button.js', + 'app/components/bootstrap/js/bootstrap-carousel.js', + 'app/components/bootstrap/js/bootstrap-collapse.js', + 'app/components/bootstrap/js/bootstrap-dropdown.js', + 'app/components/bootstrap/js/bootstrap-modal.js', + 'app/components/bootstrap/js/bootstrap-tooltip.js', + 'app/components/bootstrap/js/bootstrap-popover.js', + 'app/components/bootstrap/js/bootstrap-scrollspy.js', + 'app/components/bootstrap/js/bootstrap-tab.js', + 'app/components/bootstrap/js/bootstrap-typeahead.js', + 'app/components/bootstrap/js/bootstrap-affix.js' + ], + dest: '.tmp/scripts/bootstrap.js' + } + } + }); + + grunt.renameTask('regarde', 'watch'); + + grunt.registerTask('server', function (target) { + if (target === 'dist') { + return grunt.task.run(['build', 'open', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'clean:server', + 'less:server', + 'handlebars', + 'concat', + 'copy:livereload', + 'livereload-start', + 'connect:livereload', + 'open', + 'watch' + ]); + }); + + grunt.registerTask('build', [ + 'clean:dist', + 'less:dist', + 'handlebars', + 'concat', + 'useminPrepare', + 'requirejs', + 'imagemin', + 'htmlmin', + 'concat', + 'cssmin', + 'uglify', + 'copy', + 'usemin' + ]); + + grunt.registerTask('default', [ + 'jshint', + 'build' + ]); +}; diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..ccc4c14 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,543 @@ +# Apache configuration file +# httpd.apache.org/docs/2.2/mod/quickreference.html + +# Note .htaccess files are an overhead, this logic should be in your Apache +# config if possible: httpd.apache.org/docs/2.2/howto/htaccess.html + +# Techniques in here adapted from all over, including: +# Kroc Camen: camendesign.com/.htaccess +# perishablepress.com/press/2006/01/10/stupid-htaccess-tricks/ +# Sample .htaccess file of CMS MODx: modxcms.com + + +# ---------------------------------------------------------------------- +# Better website experience for IE users +# ---------------------------------------------------------------------- + +# Force the latest IE version, in various cases when it may fall back to IE7 mode +# github.com/rails/rails/commit/123eb25#commitcomment-118920 +# Use ChromeFrame if it's installed for a better experience for the poor IE folk + + + Header set X-UA-Compatible "IE=Edge,chrome=1" + # mod_headers can't match by content-type, but we don't want to send this header on *everything*... + + Header unset X-UA-Compatible + + + + +# ---------------------------------------------------------------------- +# Cross-domain AJAX requests +# ---------------------------------------------------------------------- + +# Serve cross-domain Ajax requests, disabled by default. +# enable-cors.org +# code.google.com/p/html5security/wiki/CrossOriginRequestSecurity + +# +# Header set Access-Control-Allow-Origin "*" +# + + +# ---------------------------------------------------------------------- +# CORS-enabled images (@crossorigin) +# ---------------------------------------------------------------------- + +# Send CORS headers if browsers request them; enabled by default for images. +# developer.mozilla.org/en/CORS_Enabled_Image +# blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html +# hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ +# wiki.mozilla.org/Security/Reviews/crossoriginAttribute + + + + # mod_headers, y u no match by Content-Type?! + + SetEnvIf Origin ":" IS_CORS + Header set Access-Control-Allow-Origin "*" env=IS_CORS + + + + + +# ---------------------------------------------------------------------- +# Webfont access +# ---------------------------------------------------------------------- + +# Allow access from all domains for webfonts. +# Alternatively you could only whitelist your +# subdomains like "subdomain.example.com". + + + + Header set Access-Control-Allow-Origin "*" + + + + +# ---------------------------------------------------------------------- +# Proper MIME type for all files +# ---------------------------------------------------------------------- + +# JavaScript +# Normalize to standard type (it's sniffed in IE anyways) +# tools.ietf.org/html/rfc4329#section-7.2 +AddType application/javascript js jsonp +AddType application/json json + +# Audio +AddType audio/mp4 m4a f4a f4b +AddType audio/ogg oga ogg + +# Video +AddType video/mp4 mp4 m4v f4v f4p +AddType video/ogg ogv +AddType video/webm webm +AddType video/x-flv flv + +# SVG +# Required for svg webfonts on iPad +# twitter.com/FontSquirrel/status/14855840545 +AddType image/svg+xml svg svgz +AddEncoding gzip svgz + +# Webfonts +AddType application/vnd.ms-fontobject eot +AddType application/x-font-ttf ttf ttc +AddType application/x-font-woff woff +AddType font/opentype otf + +# Assorted types +AddType application/octet-stream safariextz +AddType application/x-chrome-extension crx +AddType application/x-opera-extension oex +AddType application/x-shockwave-flash swf +AddType application/x-web-app-manifest+json webapp +AddType application/x-xpinstall xpi +AddType application/xml rss atom xml rdf +AddType image/webp webp +AddType image/x-icon ico +AddType text/cache-manifest appcache manifest +AddType text/vtt vtt +AddType text/x-component htc +AddType text/x-vcard vcf + + +# ---------------------------------------------------------------------- +# Allow concatenation from within specific js and css files +# ---------------------------------------------------------------------- + +# e.g. Inside of script.combined.js you could have +# +# +# and they would be included into this single file. + +# This is not in use in the boilerplate as it stands. You may +# choose to use this technique if you do not have a build process. + +# +# Options +Includes +# AddOutputFilterByType INCLUDES application/javascript application/json +# SetOutputFilter INCLUDES +# + +# +# Options +Includes +# AddOutputFilterByType INCLUDES text/css +# SetOutputFilter INCLUDES +# + + +# ---------------------------------------------------------------------- +# Gzip compression +# ---------------------------------------------------------------------- + + + + # Force deflate for mangled headers developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping/ + + + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + + + + # Compress all output labeled with one of the following MIME-types + # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` + # and can remove the `` and `` lines as + # `AddOutputFilterByType` is still in the core directives) + + AddOutputFilterByType DEFLATE application/atom+xml \ + application/javascript \ + application/json \ + application/rss+xml \ + application/vnd.ms-fontobject \ + application/x-font-ttf \ + application/xhtml+xml \ + application/xml \ + font/opentype \ + image/svg+xml \ + image/x-icon \ + text/css \ + text/html \ + text/plain \ + text/x-component \ + text/xml + + + + + +# ---------------------------------------------------------------------- +# Expires headers (for better cache control) +# ---------------------------------------------------------------------- + +# These are pretty far-future expires headers. +# They assume you control versioning with filename-based cache busting +# Additionally, consider that outdated proxies may miscache +# www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ + +# If you don't use filenames to version, lower the CSS and JS to something like +# "access plus 1 week". + + + ExpiresActive on + +# Perhaps better to whitelist expires rules? Perhaps. + ExpiresDefault "access plus 1 month" + +# cache.appcache needs re-requests in FF 3.6 (thanks Remy ~Introducing HTML5) + ExpiresByType text/cache-manifest "access plus 0 seconds" + +# Your document html + ExpiresByType text/html "access plus 0 seconds" + +# Data + ExpiresByType application/json "access plus 0 seconds" + ExpiresByType application/xml "access plus 0 seconds" + ExpiresByType text/xml "access plus 0 seconds" + +# Feed + ExpiresByType application/atom+xml "access plus 1 hour" + ExpiresByType application/rss+xml "access plus 1 hour" + +# Favicon (cannot be renamed) + ExpiresByType image/x-icon "access plus 1 week" + +# Media: images, video, audio + ExpiresByType audio/ogg "access plus 1 month" + ExpiresByType image/gif "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType video/mp4 "access plus 1 month" + ExpiresByType video/ogg "access plus 1 month" + ExpiresByType video/webm "access plus 1 month" + +# HTC files (css3pie) + ExpiresByType text/x-component "access plus 1 month" + +# Webfonts + ExpiresByType application/vnd.ms-fontobject "access plus 1 month" + ExpiresByType application/x-font-ttf "access plus 1 month" + ExpiresByType application/x-font-woff "access plus 1 month" + ExpiresByType font/opentype "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + +# CSS and JavaScript + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType text/css "access plus 1 year" + + + + +# ---------------------------------------------------------------------- +# Prevent mobile network providers from modifying your site +# ---------------------------------------------------------------------- + +# The following header prevents modification of your code over 3G on some +# European providers. +# This is the official 'bypass' suggested by O2 in the UK. + +# +# Header set Cache-Control "no-transform" +# + + +# ---------------------------------------------------------------------- +# ETag removal +# ---------------------------------------------------------------------- + +# FileETag None is not enough for every server. + + Header unset ETag + + +# Since we're sending far-future expires, we don't need ETags for +# static content. +# developer.yahoo.com/performance/rules.html#etags +FileETag None + + +# ---------------------------------------------------------------------- +# Stop screen flicker in IE on CSS rollovers +# ---------------------------------------------------------------------- + +# The following directives stop screen flicker in IE on CSS rollovers - in +# combination with the "ExpiresByType" rules for images (see above). + +# BrowserMatch "MSIE" brokenvary=1 +# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 +# BrowserMatch "Opera" !brokenvary +# SetEnvIf brokenvary 1 force-no-vary + + +# ---------------------------------------------------------------------- +# Set Keep-Alive Header +# ---------------------------------------------------------------------- + +# Keep-Alive allows the server to send multiple requests through one +# TCP-connection. Be aware of possible disadvantages of this setting. Turn on +# if you serve a lot of static content. + +# +# Header set Connection Keep-Alive +# + + +# ---------------------------------------------------------------------- +# Cookie setting from iframes +# ---------------------------------------------------------------------- + +# Allow cookies to be set from iframes (for IE only) +# If needed, specify a path or regex in the Location directive. + +# +# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" +# + + +# ---------------------------------------------------------------------- +# Start rewrite engine +# ---------------------------------------------------------------------- + +# Turning on the rewrite engine is necessary for the following rules and +# features. FollowSymLinks must be enabled for this to work. + +# Some cloud hosting services require RewriteBase to be set: goo.gl/HOcPN +# If using the h5bp in a subdirectory, use `RewriteBase /foo` instead where +# 'foo' is your directory. + +# If your web host doesn't allow the FollowSymlinks option, you may need to +# comment it out and use `Options +SymLinksIfOwnerMatch`, but be aware of the +# performance impact: http://goo.gl/Mluzd + + + Options +FollowSymlinks +# Options +SymLinksIfOwnerMatch + RewriteEngine On +# RewriteBase / + + + +# ---------------------------------------------------------------------- +# Suppress or force the "www." at the beginning of URLs +# ---------------------------------------------------------------------- + +# The same content should never be available under two different URLs - +# especially not with and without "www." at the beginning, since this can cause +# SEO problems (duplicate content). That's why you should choose one of the +# alternatives and redirect the other one. + +# By default option 1 (no "www.") is activated. +# no-www.org/faq.php?q=class_b + +# If you'd prefer to use option 2, just comment out all option 1 lines +# and uncomment option 2. + +# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! + +# ---------------------------------------------------------------------- + +# Option 1: +# Rewrite "www.example.com -> example.com". + + + RewriteCond %{HTTPS} !=on + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + + +# ---------------------------------------------------------------------- + +# Option 2: +# Rewrite "example.com -> www.example.com". +# Be aware that the following rule might not be a good idea if you use "real" +# subdomains for certain parts of your website. + +# +# RewriteCond %{HTTPS} !=on +# RewriteCond %{HTTP_HOST} !^www\..+$ [NC] +# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] +# + + +# ---------------------------------------------------------------------- +# Built-in filename-based cache busting +# ---------------------------------------------------------------------- + +# If you're not using the build script to manage your filename version revving, +# you might want to consider enabling this, which will route requests for +# `/css/style.20110203.css` to `/css/style.css`. + +# To understand why this is important and a better idea than all.css?v1231, +# please refer to the bundled documentation about `.htaccess`. + +# +# RewriteCond %{REQUEST_FILENAME} !-f +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] +# + + +# ---------------------------------------------------------------------- +# Prevent SSL cert warnings +# ---------------------------------------------------------------------- + +# Rewrite secure requests properly to prevent SSL cert warnings, e.g. prevent +# https://www.example.com when your cert only allows https://secure.example.com + +# +# RewriteCond %{SERVER_PORT} !^443 +# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] +# + + +# ---------------------------------------------------------------------- +# Prevent 404 errors for non-existing redirected folders +# ---------------------------------------------------------------------- + +# without -MultiViews, Apache will give a 404 for a rewrite if a folder of the +# same name does not exist. +# webmasterworld.com/apache/3808792.htm + +Options -MultiViews + + +# ---------------------------------------------------------------------- +# Custom 404 page +# ---------------------------------------------------------------------- + +# You can add custom pages to handle 500 or 403 pretty easily, if you like. +# If you are hosting your site in subdirectory, adjust this accordingly +# e.g. ErrorDocument 404 /subdir/404.html +ErrorDocument 404 /404.html + + +# ---------------------------------------------------------------------- +# UTF-8 encoding +# ---------------------------------------------------------------------- + +# Use UTF-8 encoding for anything served text/plain or text/html +AddDefaultCharset utf-8 + +# Force UTF-8 for a number of file formats +AddCharset utf-8 .atom .css .js .json .rss .vtt .xml + + +# ---------------------------------------------------------------------- +# A little more security +# ---------------------------------------------------------------------- + +# To avoid displaying the exact version number of Apache being used, add the +# following to httpd.conf (it will not work in .htaccess): +# ServerTokens Prod + +# "-Indexes" will have Apache block users from browsing folders without a +# default document Usually you should leave this activated, because you +# shouldn't allow everybody to surf through every folder on your server (which +# includes rather private places like CMS system folders). + + Options -Indexes + + +# Block access to "hidden" directories or files whose names begin with a +# period. This includes directories used by version control systems such as +# Subversion or Git. + + RewriteCond %{SCRIPT_FILENAME} -d [OR] + RewriteCond %{SCRIPT_FILENAME} -f + RewriteRule "(^|/)\." - [F] + + +# Block access to backup and source files. These files may be left by some +# text/html editors and pose a great security danger, when anyone can access +# them. + + Order allow,deny + Deny from all + Satisfy All + + +# If your server is not already configured as such, the following directive +# should be uncommented in order to set PHP's register_globals option to OFF. +# This closes a major security hole that is abused by most XSS (cross-site +# scripting) attacks. For more information: http://php.net/register_globals +# +# IF REGISTER_GLOBALS DIRECTIVE CAUSES 500 INTERNAL SERVER ERRORS: +# +# Your server does not allow PHP directives to be set via .htaccess. In that +# case you must make this change in your php.ini file instead. If you are +# using a commercial web host, contact the administrators for assistance in +# doing this. Not all servers allow local php.ini files, and they should +# include all PHP configurations (not just this one), or you will effectively +# reset everything to PHP defaults. Consult www.php.net for more detailed +# information about setting PHP directives. + +# php_flag register_globals Off + +# Rename session cookie to something else, than PHPSESSID +# php_value session.name sid + +# Disable magic quotes (This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.) +# php_flag magic_quotes_gpc Off + +# Do not show you are using PHP +# Note: Move this line to php.ini since it won't work in .htaccess +# php_flag expose_php Off + +# Level of log detail - log all errors +# php_value error_reporting -1 + +# Write errors to log file +# php_flag log_errors On + +# Do not display errors in browser (production - Off, development - On) +# php_flag display_errors Off + +# Do not display startup errors (production - Off, development - On) +# php_flag display_startup_errors Off + +# Format errors in plain text +# Note: Leave this setting 'On' for xdebug's var_dump() output +# php_flag html_errors Off + +# Show multiple occurrence of error +# php_flag ignore_repeated_errors Off + +# Show same errors from different sources +# php_flag ignore_repeated_source Off + +# Size limit for error messages +# php_value log_errors_max_len 1024 + +# Don't precede error with string (doesn't accept empty string, use whitespace if you need) +# php_value error_prepend_string " " + +# Don't prepend to error (doesn't accept empty string, use whitespace if you need) +# php_value error_append_string " " + +# Increase cookie security + + php_value session.cookie_httponly true + diff --git a/app/404.html b/app/404.html new file mode 100644 index 0000000..0446544 --- /dev/null +++ b/app/404.html @@ -0,0 +1,157 @@ + + + + + Page Not Found :( + + + +
+

Not found :(

+

Sorry, but the page you were trying to view does not exist.

+

It looks like this was the result of either:

+ + + +
+ + diff --git a/app/api/config.php b/app/api/config.php new file mode 100644 index 0000000..4b6e4d4 --- /dev/null +++ b/app/api/config.php @@ -0,0 +1,20 @@ + 'localhost', + 'db' => 'lentolaskuri2', + 'user' => 'lentolaskuri2', + 'password' => '8@89z~UIwavn', +); + +// ---- + +$mysqli = new mysqli($config['server'], $config['user'], $config['password'], $config['db']); + +/* check connection */ +if (mysqli_connect_errno()) { + printf("Connect failed: %s\n", mysqli_connect_error()); + exit(); +} diff --git a/app/api/import.php b/app/api/import.php new file mode 100644 index 0000000..d8cf52d --- /dev/null +++ b/app/api/import.php @@ -0,0 +1,28 @@ +real_escape_string($data[0]); + $name = $mysqli->real_escape_string($data[1]); + $city = $mysqli->real_escape_string($data[2]); + $country = $mysqli->real_escape_string($data[3]); + $iata = $mysqli->real_escape_string($data[4]); + $icao = $mysqli->real_escape_string($data[5]); + if (empty($iata)) { + continue; + } + $lat = $mysqli->real_escape_string($data[6]); + $long = $mysqli->real_escape_string($data[7]); + + $query = "REPLACE INTO airports (id, name, city, country, iata, icao, lat, `long`) VALUES ('$id', '$name', '$city', '$country', '$iata', '$icao', '$lat', '$long')"; + if (!$mysqli->query($query)) { + printf("Error: %s\n", $mysqli->sqlstate); + exit(); + } + } + fclose($handle); +} + +$mysqli->close(); diff --git a/app/api/search.php b/app/api/search.php new file mode 100644 index 0000000..6b9a42f --- /dev/null +++ b/app/api/search.php @@ -0,0 +1,54 @@ +real_escape_string($_GET['s']); +$id = $mysqli->real_escape_string($_GET['i']); +if (empty($search) && empty($id)) { + exit(); +} + +$query = ""; +if (!empty($search)) { + $query = " + SELECT *, + CASE + WHEN iata LIKE '$search%' THEN 100 + WHEN name LIKE '$search%' THEN 75 + WHEN name LIKE '%$search%' THEN 74 + WHEN city LIKE '$search%' THEN 70 + WHEN country LIKE '$search%' THEN 65 + WHEN city LIKE '%$search%' THEN 60 + WHEN country LIKE '%$search%' THEN 55 + ELSE 0 END + AS score + FROM airports + WHERE + name LIKE '$search%' OR + name LIKE '%$search%' OR + country LIKE '$search%' OR + country LIKE '%$search%' OR + city LIKE '$search%' OR + city LIKE '%$search%' OR + iata LIKE '$search%' + ORDER BY score DESC + LIMIT 10; + "; +} else if (!empty($id)) { + $query ="SELECT * FROM airports WHERE id LIKE '$id';"; +} + +$results = array(); +if ($result = $mysqli->query($query)) { + + while ($row = $result->fetch_assoc()) { + $results[] = $row; + } + + print(json_encode($results)); + +} else { + printf("Error: %s\n", $mysqli->sqlstate); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..6527905 Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..36bd99e --- /dev/null +++ b/app/index.html @@ -0,0 +1,77 @@ + + + + + + + + + Lentolaskuri 2 + + + + + + +
+
+
+

Lentolaskuri2

+
+
+ +
+
+ + Matkustajia + + +
+
+ +   +
+ + +
+
+
+ +
+
+ Reitti +
+
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + + + + + diff --git a/app/robots.txt b/app/robots.txt new file mode 100644 index 0000000..9417495 --- /dev/null +++ b/app/robots.txt @@ -0,0 +1,3 @@ +# robotstxt.org + +User-agent: * diff --git a/app/scripts/app.js b/app/scripts/app.js new file mode 100644 index 0000000..e88d69d --- /dev/null +++ b/app/scripts/app.js @@ -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; + +}); diff --git a/app/scripts/collections/results.js b/app/scripts/collections/results.js new file mode 100644 index 0000000..85ca4ba --- /dev/null +++ b/app/scripts/collections/results.js @@ -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; +}); diff --git a/app/scripts/collections/route.js b/app/scripts/collections/route.js new file mode 100644 index 0000000..7fd71b1 --- /dev/null +++ b/app/scripts/collections/route.js @@ -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; +}); diff --git a/app/scripts/config.js b/app/scripts/config.js new file mode 100644 index 0000000..3251c01 --- /dev/null +++ b/app/scripts/config.js @@ -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 + }; +}); diff --git a/app/scripts/libs/airports.js b/app/scripts/libs/airports.js new file mode 100644 index 0000000..c88e5c9 --- /dev/null +++ b/app/scripts/libs/airports.js @@ -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 + }; +}); diff --git a/app/scripts/libs/gmaps.js b/app/scripts/libs/gmaps.js new file mode 100644 index 0000000..1f957b1 --- /dev/null +++ b/app/scripts/libs/gmaps.js @@ -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; +}); diff --git a/app/scripts/libs/handlebar_helpers.js b/app/scripts/libs/handlebar_helpers.js new file mode 100644 index 0000000..de2d6fa --- /dev/null +++ b/app/scripts/libs/handlebar_helpers.js @@ -0,0 +1,10 @@ +'use strict'; + +define([ + 'handlebars' +], function (Handlebars) { + Handlebars.registerHelper('displayFloat', function (num, precision) { + precision = precision; + return ''+(num.toFixed(precision)).replace('.', ','); + }); +}); diff --git a/app/scripts/libs/maps.js b/app/scripts/libs/maps.js new file mode 100644 index 0000000..014226f --- /dev/null +++ b/app/scripts/libs/maps.js @@ -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 + }; + +}); diff --git a/app/scripts/libs/math.js b/app/scripts/libs/math.js new file mode 100644 index 0000000..bae5331 --- /dev/null +++ b/app/scripts/libs/math.js @@ -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 + }; +}); diff --git a/app/scripts/main.js b/app/scripts/main.js new file mode 100644 index 0000000..1638b7c --- /dev/null +++ b/app/scripts/main.js @@ -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(); +}); diff --git a/app/scripts/models/leg.js b/app/scripts/models/leg.js new file mode 100644 index 0000000..3e6c66a --- /dev/null +++ b/app/scripts/models/leg.js @@ -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; + +}); diff --git a/app/scripts/models/result.js b/app/scripts/models/result.js new file mode 100644 index 0000000..9a149cb --- /dev/null +++ b/app/scripts/models/result.js @@ -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; +}); diff --git a/app/scripts/models/total.js b/app/scripts/models/total.js new file mode 100644 index 0000000..833b0cc --- /dev/null +++ b/app/scripts/models/total.js @@ -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; +}); diff --git a/app/scripts/views/dropdown.js b/app/scripts/views/dropdown.js new file mode 100644 index 0000000..e342b9f --- /dev/null +++ b/app/scripts/views/dropdown.js @@ -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; +}); diff --git a/app/scripts/views/leg.js b/app/scripts/views/leg.js new file mode 100644 index 0000000..d7b5170 --- /dev/null +++ b/app/scripts/views/leg.js @@ -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; +}); diff --git a/app/scripts/views/leginput.js b/app/scripts/views/leginput.js new file mode 100644 index 0000000..c4557eb --- /dev/null +++ b/app/scripts/views/leginput.js @@ -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; +}); + diff --git a/app/scripts/views/map.js b/app/scripts/views/map.js new file mode 100644 index 0000000..da0d779 --- /dev/null +++ b/app/scripts/views/map.js @@ -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; + +}); diff --git a/app/scripts/views/operation.js b/app/scripts/views/operation.js new file mode 100644 index 0000000..7d03d16 --- /dev/null +++ b/app/scripts/views/operation.js @@ -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; +}); diff --git a/app/scripts/views/passengers.js b/app/scripts/views/passengers.js new file mode 100644 index 0000000..79d620a --- /dev/null +++ b/app/scripts/views/passengers.js @@ -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; +}); diff --git a/app/scripts/views/roundtrip.js b/app/scripts/views/roundtrip.js new file mode 100644 index 0000000..7b5f081 --- /dev/null +++ b/app/scripts/views/roundtrip.js @@ -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; +}); diff --git a/app/scripts/views/route.js b/app/scripts/views/route.js new file mode 100644 index 0000000..641387b --- /dev/null +++ b/app/scripts/views/route.js @@ -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; +}); diff --git a/app/scripts/views/total.js b/app/scripts/views/total.js new file mode 100644 index 0000000..f72d174 --- /dev/null +++ b/app/scripts/views/total.js @@ -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; +}); diff --git a/app/styles/main.less b/app/styles/main.less new file mode 100644 index 0000000..cd7505b --- /dev/null +++ b/app/styles/main.less @@ -0,0 +1,141 @@ +@import 'bootstrap/less/bootstrap'; +@import 'font-awesome/build/assets/font-awesome/less/font-awesome'; +@import (less) 'select2/select2.css'; + +.calculator { + .inactive { + opacity: 0.5; + } + .nav-tabs { + a { + font-size: 21px; + } + } +} + +.select2-container { + display: block; + .select2-choice { + display: none; + } +} + +.select2-result { + abbr { + display: block; + width: 3em; + float: left; + color: #1657A1; + } +} + +.select2-drop { + border-top: 1px solid #aaa; + .select2-search input { + margin-top: 4px; + } + &.select2-drop-above { + border-bottom: 1px solid #aaa; + } +} + +.leg { + text-align: left; + position: relative; + border-bottom: 1px solid #e5e5e5; + padding: 10px 40px 10px 0; + margin: 0 0; + & > div { + & > div { + padding-left: 24px; + } + } + i.icon-sort, .delete { + position: absolute; + top: 50%; + margin-top: -7px; + float: right; + } + i.icon-sort { + left: 0; + } + .delete { + right: 0; + margin-top: -15px; + } + + h2 { + font-size: 18px; + margin: 0; + line-height: 22px; + } + abbr { + float: right; + font-weight: normal; + } + i { + margin-top: -7px; + } +} + +.route { + position: relative; + .leg { + position: relative; + left: 0 !important; + &:last-child { + border-bottom: 0; + } + } +} + +.roundtripWidget { + .btn-group { + width: 100%; + } + button { + width: 50%; + } +} + +.resultsWidget { + position: relative; +} + +.legInputWidget { + .btn { + text-align: left; + span { + padding-left: 12px; + } + } +} + +.totalWidget { + margin: 20px 0 0 0; + padding: 20px 0; + border-top: 1px solid #e5e5e5; + font-size: 28px; + font-weight: bold; + line-height: 28px; + text-align: center; + .distance { + display: block; + } + .raw { + font-size: 20px; + line-height: 20px; + } + .co2 { + font-size: 48px; + line-height: 48px; + } +} + +input[type=number] { + text-align: right; +} + +.map { + height: 300px; +} diff --git a/app/templates/choice.hbs b/app/templates/choice.hbs new file mode 100644 index 0000000..dd7eddd --- /dev/null +++ b/app/templates/choice.hbs @@ -0,0 +1 @@ +{{name}}{{codes}} diff --git a/app/templates/leg.hbs b/app/templates/leg.hbs new file mode 100644 index 0000000..9d9f3ff --- /dev/null +++ b/app/templates/leg.hbs @@ -0,0 +1,13 @@ +
+
+ + +

{{name}}

+ {{city}}, + {{country}} +
+
+ + + +
diff --git a/app/templates/operation.hbs b/app/templates/operation.hbs new file mode 100644 index 0000000..ebad51d --- /dev/null +++ b/app/templates/operation.hbs @@ -0,0 +1,58 @@ +

+ Laskuri noutaa tietokannasta, jonka lähteenä on http://openflights.org/, lentokenttien sijainnit. + Etäisyys kahden koordinaatin välillä lasketaan Haversinen kaavalla. +

+ +

+ Lähtö- ja kohdekentälle valitaan lentoalue niiden ICAO-tunnusten perusteella. + Mikäli toinen kenttä on esimerkiksi Suomessa ja toinen Pohjois-Euroopassa, valitaan muuttujat jälkimmäisen mukaan. +

+ +
    +
  • Säteilypakotekerroin 1,0 tai matkan ollessa yli 500km 2,0.
  • +
  • Epäsuoramatkakerroin – koska reitit eivät kulje suorinta mahdollista reittiä, kerrotaan matka kertoimella {{displayFloat indirectRouteMultiplier 2}}. +
  • CO2 päästötonnin hinta {{displayFloat priceCO2 2}}€
  • +
+ +

+ Muuttujista rakennetaan kerroin m seuraavasti säteilypakotekerroin * (1 - rahtikerroin) * (1 / kuormakerroin). + Lopulliset co2 päästöt koostuvat kahdesta osasta: laskeutuminen ja nousu sekä lento. Lennon + päästöt lasketaan kertomalla kuljettu etäisyys kertoimilla m ja co2. Laskeutumisen ja nousut päästöt saadaan kertomalla lto-muuttuja kertoimella m. +

+ + + + + + {{#each distanceRanges}} + + {{/each}} + + + + + + + + + {{#each parameters}} + + + + {{#each co2factor}}{{/each}} + + + + {{#each ltoCycle}}{{/each}} + + + + {{#each load}}{{/each}} + + + + {{#each freight}}{{/each}} + + {{/each}} + +
Etäisyys [km]{{name}}
LentoalueMuuttuja
{{name}}co2{{displayFloat this 2}}
lto{{this}}
kuorma{{displayFloat this 2}}
rahti{{displayFloat this 2}}
diff --git a/app/templates/result.hbs b/app/templates/result.hbs new file mode 100644 index 0000000..2aba34f --- /dev/null +++ b/app/templates/result.hbs @@ -0,0 +1 @@ +{{displayFloat dist 2}} km{{displayFloat total 2}} Kg CO² diff --git a/app/templates/total.hbs b/app/templates/total.hbs new file mode 100644 index 0000000..a3f5e9d --- /dev/null +++ b/app/templates/total.hbs @@ -0,0 +1,15 @@ +

Matkaa yhteensä {{displayFloat dist 1}} km.

+ +

{{#if alone}} + {{#if roundtrip}} + Päästöjä per suunta {{displayFloat rawTotal 0}} Kg CO2. + {{/if}} +{{else}} + {{#if roundtrip}} + Päästöjä per henkilö per suunta {{displayFloat rawTotal 0}} Kg CO². + {{else}} + Päästöjä per henkilö {{displayFloat rawTotal 0}} Kg CO2. + {{/if}} +{{/if}}

+

{{displayFloat total 0}} kg CO2

+

{{displayFloat price 2}}

diff --git a/component.json b/component.json new file mode 100644 index 0000000..2dd7877 --- /dev/null +++ b/component.json @@ -0,0 +1,18 @@ +{ + "name": "lentolaskuri2", + "version": "0.0.0", + "dependencies": { + "backbone": "git://github.com/documentcloud/backbone.git#1.0.0", + "bootstrap": "git://github.com/twitter/bootstrap.git#2.3.1", + "requirejs": "git://github.com/jrburke/requirejs#2.1.5", + "jquery": "git://github.com/components/jquery.git#1.9.1", + "select2": "git://github.com/ivaynberg/select2.git#~3.3.1", + "lodash": "git://github.com/bestiejs/lodash.git#1.0.1", + "font-awesome": "~3.1.1", + "handlebars": "git://github.com/components/handlebars.js#1.0.0-rc.3", + "jquery-ui": "git://github.com/components/jqueryui#1.10.1", + "requirejs-plugins": "git://github.com/millermedeiros/requirejs-plugins.git", + "Backbone-Mediator": "latest", + "i18next": "~1.6.0" + } +} diff --git a/lentolaskuri.sublime-project b/lentolaskuri.sublime-project new file mode 100644 index 0000000..db73789 --- /dev/null +++ b/lentolaskuri.sublime-project @@ -0,0 +1,18 @@ +{ + "folders": + [ + { + "path": "/home/juho/Source/tyo/lentolaskuri2", + "folder_exclude_patterns": ["node_modules", ".cache", ".tmp", "app/components"] + } + ], + "ternjs": { + "exclude": ["node_modules/**", "app/components/**"], + "libs": ["jquery"], + "plugins": { + "requirejs": { + "baseURL": "app/components" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..75febb1 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "lentolaskuri2", + "version": "0.0.0", + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-copy": "~0.4.0", + "grunt-contrib-concat": "~0.1.2", + "grunt-contrib-coffee": "~0.4.0", + "grunt-contrib-uglify": "~0.1.1", + "grunt-contrib-less": "~0.5.0", + "grunt-contrib-jshint": "~0.1.1", + "grunt-contrib-cssmin": "~0.4.1", + "grunt-contrib-connect": "0.1.2", + "grunt-contrib-clean": "0.4.0", + "grunt-contrib-htmlmin": "0.1.1", + "grunt-contrib-imagemin": "0.1.2", + "grunt-contrib-livereload": "0.1.1", + "grunt-bower-hooks": "~0.2.0", + "grunt-usemin": "~0.1.9", + "grunt-regarde": "~0.1.1", + "grunt-requirejs": "~0.3.1", + "grunt-mocha": "~0.2.2", + "grunt-open": "~0.2.0", + "matchdep": "~0.1.1", + "grunt-contrib-handlebars": "~0.5.7" + }, + "engines": { + "node": ">=0.8.0" + } +}