├── .idea ├── .name ├── encodings.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── modules.xml ├── timelapseRails.iml ├── runConfigurations │ └── bin_www.xml ├── libraries │ └── timelapseRails_node_modules.xml ├── misc.xml └── watcherTasks.xml ├── public └── app │ ├── railconf.json │ ├── assets │ ├── img │ │ ├── logo.jpg │ │ ├── logo.png │ │ ├── logo_solid.png │ │ └── ic_settings_black_24px.svg │ ├── css │ │ ├── iconfont.woff2 │ │ ├── main.css │ │ ├── style.css.map │ │ ├── style.scss │ │ ├── style.css │ │ ├── index.css │ │ └── angular-material-icons.css │ └── lib │ │ ├── ngTouch.min.js │ │ ├── ngStorage.min.js │ │ ├── angular-aria.min.js │ │ ├── angular-animate.min.js │ │ ├── angular-ui-router.min.js │ │ └── moment.min.js │ ├── views │ ├── error.ejs │ └── index.ejs │ ├── lapseconf.json │ ├── shared │ └── shared-service.js │ ├── routes.js │ ├── app.js │ └── components │ └── main │ ├── main.html │ └── main-controller.js ├── samples ├── circuit.jpg ├── mosfet.jpg ├── screen1.jpg └── screen2.jpg ├── routes ├── users.js └── index.js ├── .gitattributes ├── package.json ├── .gitignore ├── app.js ├── README.md └── bin └── www.js /.idea/.name: -------------------------------------------------------------------------------- 1 | timelapseRails -------------------------------------------------------------------------------- /public/app/railconf.json: -------------------------------------------------------------------------------- 1 | {"focusEnabled":false,"rememberPosition":false,"railLength":10000} -------------------------------------------------------------------------------- /samples/circuit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/samples/circuit.jpg -------------------------------------------------------------------------------- /samples/mosfet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/samples/mosfet.jpg -------------------------------------------------------------------------------- /samples/screen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/samples/screen1.jpg -------------------------------------------------------------------------------- /samples/screen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/samples/screen2.jpg -------------------------------------------------------------------------------- /public/app/assets/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/public/app/assets/img/logo.jpg -------------------------------------------------------------------------------- /public/app/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/public/app/assets/img/logo.png -------------------------------------------------------------------------------- /public/app/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /public/app/assets/css/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/public/app/assets/css/iconfont.woff2 -------------------------------------------------------------------------------- /public/app/assets/img/logo_solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equilerex/PiLapseRails/HEAD/public/app/assets/img/logo_solid.png -------------------------------------------------------------------------------- /public/app/lapseconf.json: -------------------------------------------------------------------------------- 1 | {"bulbMode":false,"shutterSpeed":1000,"focusLength":0,"motorPulse":1000,"waitLength":1000,"direction":true,"loopEnabled":false,"loopCount":0} -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/app/shared/shared-service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module("ngapp").service("shared", function(){ // One of The Ways To Share Informations Across the Controllers 4 | this.info = { 5 | title: "Raspberry pi Timelaps Rail", 6 | auth: "koivistik.com" 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/timelapseRails.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations/bin_www.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/app/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module("ngapp").config(["$stateProvider", "$urlRouterProvider", function($stateProvider, $urlRouterProvider){ 4 | 5 | $urlRouterProvider.otherwise("/main"); 6 | 7 | $stateProvider.state("main", { 8 | url: "/main", 9 | templateUrl: "public/app/components/main/main.html", 10 | title: "TimePiRail", 11 | controller: "MainController", 12 | controllerAs: "main" 13 | }); 14 | }]); 15 | -------------------------------------------------------------------------------- /.idea/libraries/timelapseRails_node_modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timelapseRails", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.6.9", 12 | "ejs": "~2.5.5", 13 | "express": "~4.13.1", 14 | "morgan": "~1.6.1", 15 | "node-sass-middleware": "0.8.0", 16 | "serve-favicon": "~2.3.0", 17 | "ws": "^1.0.1", 18 | "socket.io":"1.4.5", 19 | "gpio":"~0.2.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/app/assets/lib/ngTouch.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("ngTouch",[]).directive("ngTouchstart",function(){return{controller:["$scope","$element",function(n,t){function e(e){var o=t.attr("ng-touchstart");n.$event=e,n.$apply(o)}t.bind("touchstart",e)}]}}).directive("ngTouchmove",function(){return{controller:["$scope","$element",function(n,t){function e(n){n.preventDefault(),t.bind("touchmove",o),t.bind("touchend",c)}function o(e){var o=t.attr("ng-touchmove");n.$event=e,n.$apply(o)}function c(n){n.preventDefault(),t.unbind("touchmove",o),t.unbind("touchend",c)}t.bind("touchstart",e)}]}}).directive("ngTouchend",function(){return{controller:["$scope","$element",function(n,t){function e(e){var o=t.attr("ng-touchend");n.$event=e,n.$apply(o)}t.bind("touchend",e)}]}}); 2 | -------------------------------------------------------------------------------- /public/app/assets/img/ic_settings_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | #dev specific 46 | node_modules 47 | public/app/lapseconf.json 48 | public/app/railconf.json 49 | .idea/workspace.xml -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /public/app/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | Errno::ENOENT: No such file or directory @ rb_sysopen - main.scss 3 | 4 | Backtrace: 5 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin/compiler.rb:482:in `read' 6 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin/compiler.rb:482:in `update_stylesheet' 7 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin/compiler.rb:215:in `block in update_stylesheets' 8 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin/compiler.rb:209:in `each' 9 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin/compiler.rb:209:in `update_stylesheets' 10 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/plugin.rb:82:in `update_stylesheets' 11 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/exec/sass_scss.rb:363:in `watch_or_update' 12 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/exec/sass_scss.rb:51:in `process_result' 13 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/exec/base.rb:52:in `parse' 14 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/lib/sass/exec/base.rb:19:in `parse!' 15 | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/sass-3.4.21/bin/scss:13:in `' 16 | C:/Ruby22-x64/bin/scss:23:in `load' 17 | C:/Ruby22-x64/bin/scss:23:in `
' 18 | */ 19 | body:before { 20 | white-space: pre; 21 | font-family: monospace; 22 | content: "Errno::ENOENT: No such file or directory @ rb_sysopen - main.scss"; } 23 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module("ngapp", [ "ngTouch", "ui.router", "ngMdIcons", "ngMaterial", "ngStorage" ]) 4 | // ngTouch is No Longer Supported by Angular-Material 5 | 6 | .run(function($rootScope){ 7 | /* Hijack Android Back Button (You Can Set Different Functions for Each View by Checking the $state.current) 8 | document.addEventListener("backbutton", function (e) { 9 | if($state.is('init')){ 10 | navigator.app.exitApp(); 11 | } else{ 12 | e.preventDefault(); 13 | } 14 | }, false);*/ 15 | }) 16 | .config(function($mdThemingProvider) { 17 | $mdThemingProvider.definePalette('PiLapseRailsTheme', { 18 | '50': '#e5f7f9', 19 | '100': '#a8e4ea', 20 | '200': '#2AA0AB', 21 | '300': '#42c4d0', 22 | '400': '#30b7c4', 23 | '500': '#2aa0ab', 24 | '600': '#248992', 25 | '700': '#1e727a', 26 | '800': '#185b61', 27 | '900': '#124449', 28 | 'A100': '#e5f7f9', 29 | 'A200': '#a8e4ea', 30 | 'A400': '#30b7c4', 31 | 'A700': '#1e727a', 32 | 'contrastDefaultColor': 'light', 33 | 'contrastDarkColors': '50 100 200 300 400 A100 A200 A400' 34 | }); 35 | $mdThemingProvider.theme('default') 36 | .primaryPalette('PiLapseRailsTheme') 37 | .accentPalette('PiLapseRailsTheme', { 38 | 'default': '200' // use shade 200 for default, and keep all other shades the same 39 | }); 40 | }) 41 | -------------------------------------------------------------------------------- /public/app/assets/css/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,UAKC;EAJC,WAAW,EAAE,gBAAgB;EAC7B,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,GAAG;EAChB,GAAG,EAAE,8FAA8F;AAGrG,eAAgB;EACd,WAAW,EAAE,gBAAgB;EAC7B,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,MAAM;EAClB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,CAAC;EACd,cAAc,EAAE,MAAM;EACtB,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,YAAY;EACrB,WAAW,EAAE,MAAM;EACnB,SAAS,EAAE,MAAM;EACjB,SAAS,EAAE,GAAG;EACd,6BAA6B,EAAE,MAAM;EACrC,sBAAsB,EAAE,WAAW;;AAKrC,uBAAwB;EACtB,KAAK,EAAE,OAAO;;AAEhB,iBAAkB;EAChB,aAAa,EAAE,GAAG;;AAIpB,iBAAkB;EAChB,OAAO,EAAE,IAAI;;AAEf,UAAW;EACT,KAAK,EAAE,IAAI;;AAEb,UAAW;EACT,MAAM,EAAE,GAAG;;AAEb,qBAAsB;EACpB,WAAW,EAAE,GAAG;EAChB,YAAY,EAAE,GAAG;;AAEnB,WAAY;EACV,OAAO,EAAE,GAAG;;AAEd,iBAAkB;EAChB,gBAAgB,EAAE,kBAAkB;EACpC,MAAM,EAAE,IAAI;;AAEd,KAAM;EACJ,YAAY,EAAE,GAAG;EACjB,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,OAAO;;AAEhB,aAAc;EACZ,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,IAAI;EAChB,0BAAe;IACb,MAAM,EAAE,cAAc;IACtB,YAAY,EAAE,OAAO;EAEvB,gBAAG;IACD,SAAS,EAAE,IAAI;EAGf,gCAAM;IACJ,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,GAAG;EAEb,qCAAW;IACT,SAAS,EAAE,IAAI;EAEjB,6BAAG;IACD,UAAU,EAAE,IAAI;;AAKtB,QAAS;EACP,GAAG,EAAE,GAAG;EACR,MAAM,EAAE,IAAI;EACZ,0BAAkB;IAChB,gBAAgB,EAAC,kBAAkB;;AAGvC,OAAQ;EACN,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,OAAO;EACd,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,MAAM;EAClB,OAAO,EAAE,KAAK;EACd,eAAe,EAAE,IAAI;;AAGvB,4BAA6B;EAC3B,UAAU,EAAE,KAAK;;AAEnB,oBAAqB;EACnB,UAAU,EAAE,KAAK;;AAEnB,UAAW;EACT,MAAM,EAAE,GAAG;;AAEb,YAAa;EACX,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,IAAI;EAChB,WAAW,EAAE,IAAI;;AAEnB,YAAa;EACX,UAAU,EAAE,IAAI", 4 | "sources": ["style.scss"], 5 | "names": [], 6 | "file": "style.css" 7 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var routes = require('./routes/index'); 9 | var users = require('./routes/users'); 10 | 11 | var app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'ejs'); 16 | app.use("/public", express.static(path.join(__dirname, 'public'))); 17 | // uncomment after placing your favicon in /public 18 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 19 | app.use(logger('dev')); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | app.use(cookieParser()); 23 | app.use(require('node-sass-middleware')({ 24 | src: path.join(__dirname, 'public'), 25 | dest: path.join(__dirname, 'public'), 26 | indentedSyntax: true, 27 | sourceMap: true 28 | })); 29 | app.use(express.static(path.join(__dirname, 'public'))); 30 | 31 | app.use('/', routes); 32 | app.use('/users', users); 33 | 34 | // catch 404 and forward to error handler 35 | app.use(function(req, res, next) { 36 | var err = new Error('Not Found'); 37 | err.status = 404; 38 | next(err); 39 | }); 40 | 41 | // error handlers 42 | 43 | // development error handler 44 | // will print stacktrace 45 | if (app.get('env') === 'development') { 46 | app.use(function(err, req, res, next) { 47 | res.status(err.status || 500); 48 | res.render('error', { 49 | message: err.message, 50 | error: err 51 | }); 52 | }); 53 | } 54 | 55 | // production error handler 56 | // no stacktraces leaked to user 57 | app.use(function(err, req, res, next) { 58 | res.status(err.status || 500); 59 | res.render('error', { 60 | message: err.message, 61 | error: {} 62 | }); 63 | }); 64 | 65 | 66 | module.exports = app; 67 | -------------------------------------------------------------------------------- /public/app/assets/lib/ngStorage.min.js: -------------------------------------------------------------------------------- 1 | /*! ngstorage 0.3.10 | Copyright (c) 2015 Gias Kay Lee | MIT License */!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["angular"],b):a.hasOwnProperty("angular")?b(a.angular):"object"==typeof exports&&(module.exports=b(require("angular")))}(this,function(a){"use strict";function b(b){return function(){var c="ngStorage-";this.setKeyPrefix=function(a){if("string"!=typeof a)throw new TypeError("[ngStorage] - "+b+"Provider.setKeyPrefix() expects a String.");c=a};var d=a.toJson,e=a.fromJson;this.setSerializer=function(a){if("function"!=typeof a)throw new TypeError("[ngStorage] - "+b+"Provider.setSerializer expects a function.");d=a},this.setDeserializer=function(a){if("function"!=typeof a)throw new TypeError("[ngStorage] - "+b+"Provider.setDeserializer expects a function.");e=a},this.get=function(a){return e(window[b].getItem(c+a))},this.set=function(a,e){return window[b].setItem(c+a,d(e))},this.$get=["$rootScope","$window","$log","$timeout","$document",function(f,g,h,i,j){function k(a){var b;try{b=g[a]}catch(c){b=!1}if(b&&"localStorage"===a){var d="__"+Math.round(1e7*Math.random());try{localStorage.setItem(d,d),localStorage.removeItem(d)}catch(c){b=!1}}return b}var l,m,n=c.length,o=k(b)||(h.warn("This browser does not support Web Storage!"),{setItem:a.noop,getItem:a.noop,removeItem:a.noop}),p={$default:function(b){for(var c in b)a.isDefined(p[c])||(p[c]=a.copy(b[c]));return p.$sync(),p},$reset:function(a){for(var b in p)"$"===b[0]||delete p[b]&&o.removeItem(c+b);return p.$default(a)},$sync:function(){for(var a,b=0,d=o.length;d>b;b++)(a=o.key(b))&&c===a.slice(0,n)&&(p[a.slice(n)]=e(o.getItem(a)))},$apply:function(){var b;if(m=null,!a.equals(p,l)){b=a.copy(l),a.forEach(p,function(e,f){a.isDefined(e)&&"$"!==f[0]&&(o.setItem(c+f,d(e)),delete b[f])});for(var e in b)o.removeItem(c+e);l=a.copy(p)}}};return p.$sync(),l=a.copy(p),f.$watch(function(){m||(m=i(p.$apply,100,!1))}),g.addEventListener&&g.addEventListener("storage",function(b){if(b.key){var d=j[0];d.hasFocus&&d.hasFocus()||c!==b.key.slice(0,n)||(b.newValue?p[b.key.slice(n)]=e(b.newValue):delete p[b.key.slice(n)],l=a.copy(p),f.$apply())}}),g.addEventListener&&g.addEventListener("beforeunload",function(){p.$apply()}),p}]}}return a=a&&a.module?a:window.angular,a.module("ngStorage",[]).provider("$localStorage",b("localStorage")).provider("$sessionStorage",b("sessionStorage"))}); -------------------------------------------------------------------------------- /public/app/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Material Icons'), local('MaterialIcons-Regular'), url("iconfont.woff2") format('woff2'); 6 | } 7 | 8 | .material-icons { 9 | font-family: 'Material Icons'; 10 | font-weight: normal; 11 | font-style: normal; 12 | font-size: 24px; 13 | line-height: 1; 14 | letter-spacing: normal; 15 | text-transform: none; 16 | display: inline-block; 17 | white-space: nowrap; 18 | word-wrap: normal; 19 | direction: ltr; 20 | -webkit-font-feature-settings: 'liga'; 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | 24 | 25 | 26 | .menu_button ng-md-icon { 27 | color: #ffffff; 28 | } 29 | .no_bottom_margin { 30 | margin-bottom: 0px; 31 | } 32 | md-switch { 33 | } 34 | .md-errors-spacer { 35 | display: none; 36 | } 37 | .width_100 { 38 | width: 100%; 39 | } 40 | .no_margin { 41 | margin: 0px; 42 | } 43 | .no_horizontal_margin { 44 | margin-left: 0px; 45 | margin-right: 0px; 46 | } 47 | .no_padding { 48 | padding: 0px; 49 | } 50 | .black_background { 51 | background-color: #333333 !important; 52 | height: 52px; 53 | } 54 | .icon { 55 | margin-right: 5px; 56 | font-size: 20px; 57 | color: #FFFFFF; 58 | } 59 | .main_counter { 60 | text-align: center; 61 | padding-top: 10px; 62 | margin-top: 10px; 63 | &:nth-child(2) { 64 | border: 2px solid #ccc; 65 | border-width: 0px 2px; 66 | } 67 | h1 { 68 | font-size: 20px; 69 | } 70 | &.time_counter { 71 | label { 72 | display: block; 73 | margin: 0px; 74 | } 75 | .sub_label { 76 | font-size: 11px; 77 | } 78 | h1 { 79 | margin-top: 10px; 80 | } 81 | } 82 | } 83 | 84 | md-toast { 85 | top: 0px; 86 | height: 60px; 87 | .md-toast-content { 88 | background-color:#D01F1F !important; 89 | } 90 | } 91 | .author { 92 | position: relative; 93 | bottom: 0px; 94 | padding: 10px; 95 | color: #2AA0AB; 96 | font-size: 15px; 97 | text-align: center; 98 | display: block; 99 | text-decoration: none; 100 | } 101 | 102 | .md-sidenav-left, md-sidenav { 103 | min-height: 900px; 104 | } 105 | .md-sidenav-backdrop { 106 | min-height: 900px; 107 | } 108 | .md-button { 109 | margin: 0px; 110 | } 111 | .test_button { 112 | min-height: 28px; 113 | margin-top: 21px; 114 | line-height: 28px; 115 | } 116 | .stop_button { 117 | margin-top: 10px; 118 | } -------------------------------------------------------------------------------- /public/app/assets/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local("Material Icons"), local("MaterialIcons-Regular"), url("iconfont.woff2") format("woff2"); } 6 | .material-icons { 7 | font-family: 'Material Icons'; 8 | font-weight: normal; 9 | font-style: normal; 10 | font-size: 24px; 11 | line-height: 1; 12 | letter-spacing: normal; 13 | text-transform: none; 14 | display: inline-block; 15 | white-space: nowrap; 16 | word-wrap: normal; 17 | direction: ltr; 18 | -webkit-font-feature-settings: 'liga'; 19 | -webkit-font-smoothing: antialiased; } 20 | 21 | .menu_button ng-md-icon { 22 | color: #ffffff; } 23 | 24 | .no_bottom_margin { 25 | margin-bottom: 0px; } 26 | 27 | .md-errors-spacer { 28 | display: none; } 29 | 30 | .width_100 { 31 | width: 100%; } 32 | 33 | .no_margin { 34 | margin: 0px; } 35 | 36 | .no_horizontal_margin { 37 | margin-left: 0px; 38 | margin-right: 0px; } 39 | 40 | .no_padding { 41 | padding: 0px; } 42 | 43 | .black_background { 44 | background-color: #333333 !important; 45 | height: 52px; } 46 | 47 | .icon { 48 | margin-right: 5px; 49 | font-size: 20px; 50 | color: #FFFFFF; } 51 | 52 | .main_counter { 53 | text-align: center; 54 | padding-top: 10px; 55 | margin-top: 10px; } 56 | .main_counter:nth-child(2) { 57 | border: 2px solid #ccc; 58 | border-width: 0px 2px; } 59 | .main_counter h1 { 60 | font-size: 20px; } 61 | .main_counter.time_counter label { 62 | display: block; 63 | margin: 0px; } 64 | .main_counter.time_counter .sub_label { 65 | font-size: 11px; } 66 | .main_counter.time_counter h1 { 67 | margin-top: 10px; } 68 | 69 | md-toast { 70 | top: 0px; 71 | height: 60px; } 72 | md-toast .md-toast-content { 73 | background-color: #D01F1F !important; } 74 | 75 | .author { 76 | position: relative; 77 | bottom: 0px; 78 | padding: 10px; 79 | color: #2AA0AB; 80 | font-size: 15px; 81 | text-align: center; 82 | display: block; 83 | text-decoration: none; } 84 | 85 | .md-sidenav-left, md-sidenav { 86 | min-height: 900px; } 87 | 88 | .md-sidenav-backdrop { 89 | min-height: 900px; } 90 | 91 | .md-button { 92 | margin: 0px; } 93 | 94 | .test_button { 95 | min-height: 28px; 96 | margin-top: 21px; 97 | line-height: 28px; } 98 | 99 | .stop_button { 100 | margin-top: 10px; } 101 | 102 | /*# sourceMappingURL=style.css.map */ 103 | -------------------------------------------------------------------------------- /public/app/assets/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adjust last value opacity 0 to 1.0 */ 3 | text-decoration: none; 4 | } 5 | 6 | body { 7 | -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ 8 | -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */ 9 | -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ 10 | background-attachment:fixed; 11 | font-family:"RobotoDraft", Helvetica, Arial, sans-serif; 12 | height:100%; 13 | margin:0px; 14 | padding:0px; 15 | width:100%; 16 | } 17 | 18 | /* Portrait layout (default) */ 19 | .app { 20 | background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */ 21 | position:absolute; /* position in the center of the screen */ 22 | left:50%; 23 | top:50%; 24 | height:50px; /* text area height */ 25 | width:225px; /* text area width */ 26 | text-align:center; 27 | padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped with text) */ 28 | margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text area height */ 29 | /* offset horizontal: half of text area width */ 30 | } 31 | 32 | /* Landscape layout (with min-width) */ 33 | @media screen and (min-aspect-ratio: 1/1) and (min-width:400px) { 34 | .app { 35 | background-position:left center; 36 | padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = image height */ 37 | margin:-90px 0px 0px -198px; /* offset vertical: half of image height */ 38 | /* offset horizontal: half of image width and text area width */ 39 | } 40 | } 41 | 42 | .event { 43 | border-radius:4px; 44 | -webkit-border-radius:4px; 45 | color:#FFFFFF; 46 | font-size:12px; 47 | margin:0px 30px; 48 | padding:2px 0px; 49 | } 50 | 51 | .event.listening { 52 | background-color:#333333; 53 | display:block; 54 | } 55 | 56 | .event.received { 57 | background-color:#4B946A; 58 | display:none; 59 | } 60 | 61 | @keyframes fade { 62 | from { opacity: 1.0; } 63 | 50% { opacity: 0.4; } 64 | to { opacity: 1.0; } 65 | } 66 | 67 | @-webkit-keyframes fade { 68 | from { opacity: 1.0; } 69 | 50% { opacity: 0.4; } 70 | to { opacity: 1.0; } 71 | } 72 | 73 | .blink { 74 | animation:fade 3000ms infinite; 75 | -webkit-animation:fade 3000ms infinite; 76 | } 77 | 78 | .my-datepicker .form-control { 79 | width: 1px; 80 | } 81 | -------------------------------------------------------------------------------- /public/app/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | NgApp 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/app/assets/lib/angular-aria.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.4.8 3 | (c) 2010-2015 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(u,n,v){'use strict';var r="BUTTON A INPUT TEXTAREA SELECT DETAILS SUMMARY".split(" "),p=function(a,c){if(-1!==c.indexOf(a[0].nodeName))return!0};n.module("ngAria",["ng"]).provider("$aria",function(){function a(a,f,l,m){return function(d,e,b){var g=b.$normalize(f);!c[g]||p(e,l)||b[g]||d.$watch(b[a],function(b){b=m?!b:!!b;e.attr(f,b)})}}var c={ariaHidden:!0,ariaChecked:!0,ariaDisabled:!0,ariaRequired:!0,ariaInvalid:!0,ariaMultiline:!0,ariaValue:!0,tabindex:!0,bindKeypress:!0,bindRoleForClick:!0}; 7 | this.config=function(a){c=n.extend(c,a)};this.$get=function(){return{config:function(a){return c[a]},$$watchExpr:a}}}).directive("ngShow",["$aria",function(a){return a.$$watchExpr("ngShow","aria-hidden",[],!0)}]).directive("ngHide",["$aria",function(a){return a.$$watchExpr("ngHide","aria-hidden",[],!1)}]).directive("ngModel",["$aria",function(a){function c(c,m,d){return a.config(m)&&!d.attr(c)}function k(a,c){return!c.attr("role")&&c.attr("type")===a&&"INPUT"!==c[0].nodeName}function f(a,c){var d= 8 | a.type,e=a.role;return"checkbox"===(d||e)||"menuitemcheckbox"===e?"checkbox":"radio"===(d||e)||"menuitemradio"===e?"radio":"range"===d||"progressbar"===e||"slider"===e?"range":"textbox"===(d||e)||"TEXTAREA"===c[0].nodeName?"multiline":""}return{restrict:"A",require:"?ngModel",priority:200,compile:function(l,m){var d=f(m,l);return{pre:function(a,b,c,h){"checkbox"===d&&"checkbox"!==c.type&&(h.$isEmpty=function(b){return!1===b})},post:function(e,b,g,h){function f(){return h.$modelValue}function m(){return q? 9 | (q=!1,function(a){a=g.value==h.$viewValue;b.attr("aria-checked",a);b.attr("tabindex",0-!a)}):function(a){b.attr("aria-checked",g.value==h.$viewValue)}}function l(){b.attr("aria-checked",!h.$isEmpty(h.$viewValue))}var q=c("tabindex","tabindex",b)&&!p(b,r);switch(d){case "radio":case "checkbox":k(d,b)&&b.attr("role",d);c("aria-checked","ariaChecked",b)&&e.$watch(f,"radio"===d?m():l);q&&b.attr("tabindex",0);break;case "range":k(d,b)&&b.attr("role","slider");if(a.config("ariaValue")){var n=!b.attr("aria-valuemin")&& 10 | (g.hasOwnProperty("min")||g.hasOwnProperty("ngMin")),s=!b.attr("aria-valuemax")&&(g.hasOwnProperty("max")||g.hasOwnProperty("ngMax")),t=!b.attr("aria-valuenow");n&&g.$observe("min",function(a){b.attr("aria-valuemin",a)});s&&g.$observe("max",function(a){b.attr("aria-valuemax",a)});t&&e.$watch(f,function(a){b.attr("aria-valuenow",a)})}q&&b.attr("tabindex",0);break;case "multiline":c("aria-multiline","ariaMultiline",b)&&b.attr("aria-multiline",!0)}h.$validators.required&&c("aria-required","ariaRequired", 11 | b)&&e.$watch(function(){return h.$error.required},function(a){b.attr("aria-required",!!a)});c("aria-invalid","ariaInvalid",b)&&e.$watch(function(){return h.$invalid},function(a){b.attr("aria-invalid",!!a)})}}}}}]).directive("ngDisabled",["$aria",function(a){return a.$$watchExpr("ngDisabled","aria-disabled",[])}]).directive("ngMessages",function(){return{restrict:"A",require:"?ngMessages",link:function(a,c,k,f){c.attr("aria-live")||c.attr("aria-live","assertive")}}}).directive("ngClick",["$aria","$parse", 12 | function(a,c){return{restrict:"A",compile:function(k,f){var l=c(f.ngClick,null,!0);return function(c,d,e){if(!p(d,r)&&(a.config("bindRoleForClick")&&!d.attr("role")&&d.attr("role","button"),a.config("tabindex")&&!d.attr("tabindex")&&d.attr("tabindex",0),a.config("bindKeypress")&&!e.ngKeypress))d.on("keypress",function(a){function d(){l(c,{$event:a})}var e=a.which||a.keyCode;32!==e&&13!==e||c.$apply(d)})}}}}]).directive("ngDblclick",["$aria",function(a){return function(c,k,f){!a.config("tabindex")|| 13 | k.attr("tabindex")||p(k,r)||k.attr("tabindex",0)}}])})(window,window.angular); 14 | //# sourceMappingURL=angular-aria.min.js.map 15 | -------------------------------------------------------------------------------- /public/app/assets/css/angular-material-icons.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Angular Material Design 3 | * https://github.com/angular/material 4 | * @license MIT 5 | * v0.9.7 6 | */ 7 | /* mixin definition ; sets LTR and RTL within the same style call */ 8 | 9 | md-autocomplete button ng-md-icon { 10 | position: absolute; 11 | top: 50%; 12 | left: 50%; 13 | -webkit-transform: translate3d(-50%, -50%, 0) scale(0.9); 14 | transform: translate3d(-50%, -50%, 0) scale(0.9); 15 | } 16 | 17 | md-autocomplete button ng-md-icon path { 18 | stroke-width: 0; 19 | } 20 | 21 | .md-button.ng-md-icon { 22 | padding: 0; 23 | background: none; 24 | } 25 | 26 | .md-button.md-fab ng-md-icon { 27 | margin-top: 0; 28 | } 29 | 30 | md-checkbox .ng-md-icon { 31 | transition: 240ms; 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | width: 18px; 36 | height: 18px; 37 | border: 2px solid; 38 | border-radius: 2px; 39 | } 40 | 41 | md-checkbox.md-checked .ng-md-icon { 42 | border: none; 43 | } 44 | 45 | md-checkbox.md-checked .ng-md-icon:after { 46 | -webkit-transform: rotate(45deg); 47 | transform: rotate(45deg); 48 | position: absolute; 49 | left: 6px; 50 | top: 2px; 51 | display: table; 52 | width: 6px; 53 | height: 12px; 54 | border: 2px solid; 55 | border-top: 0; 56 | border-left: 0; 57 | content: ''; 58 | } 59 | 60 | .md-chips .md-chip .md-chip-remove ng-md-icon { 61 | height: 18px; 62 | width: 18px; 63 | position: absolute; 64 | top: 50%; 65 | left: 50%; 66 | -webkit-transform: translate3d(-50%, -50%, 0); 67 | transform: translate3d(-50%, -50%, 0); 68 | } 69 | 70 | ng-md-icon { 71 | margin: auto; 72 | background-repeat: no-repeat no-repeat; 73 | display: inline-block; 74 | vertical-align: middle; 75 | fill: currentColor; 76 | height: 24px; 77 | width: 24px; 78 | } 79 | 80 | ng-md-icon svg { 81 | pointer-events: none; 82 | display: block; 83 | } 84 | 85 | ng-md-icon[md-font-icon] { 86 | line-height: 1; 87 | width: auto; 88 | } 89 | 90 | md-input-container > ng-md-icon { 91 | position: absolute; 92 | top: 5px; 93 | left: 2px; 94 | } 95 | 96 | md-input-container > ng-md-icon + input { 97 | margin-left: 36px; 98 | } 99 | 100 | md-input-container.md-icon-float > ng-md-icon { 101 | top: 26px; 102 | left: 2px; 103 | } 104 | 105 | md-input-container.md-icon-float > ng-md-icon + input { 106 | margin-left: 36px; 107 | } 108 | 109 | @media screen and (-ms-high-contrast: active) { 110 | md-input-container.md-default-theme > ng-md-icon { 111 | fill: #fff; 112 | } 113 | } 114 | 115 | md-list-item > div.md-primary > ng-md-icon, 116 | md-list-item > div.md-secondary > ng-md-icon, 117 | md-list-item > ng-md-icon:first-child, 118 | md-list-item > ng-md-icon.md-secondary, 119 | md-list-item .md-list-item-inner > div.md-primary > ng-md-icon, 120 | md-list-item .md-list-item-inner > div.md-secondary > ng-md-icon, 121 | md-list-item .md-list-item-inner > ng-md-icon:first-child, 122 | md-list-item .md-list-item-inner > ng-md-icon.md-secondary { 123 | width: 24px; 124 | margin-top: 16px; 125 | margin-bottom: 12px; 126 | box-sizing: content-box; 127 | } 128 | 129 | md-list-item > ng-md-icon:first-child, 130 | md-list-item .md-list-item-inner > ng-md-icon:first-child { 131 | margin-right: 32px; 132 | } 133 | 134 | md-list-item.md-2-line > ng-md-icon:first-child, 135 | md-list-item.md-2-line > .md-no-style > ng-md-icon:first-child { 136 | -webkit-align-self: flex-start; 137 | -ms-flex-item-align: start; 138 | align-self: flex-start; 139 | } 140 | 141 | md-list-item.md-3-line > ng-md-icon:first-child, 142 | md-list-item.md-3-line > .md-no-style > ng-md-icon:first-child { 143 | margin-top: 16px; 144 | } 145 | 146 | md-tabs-wrapper md-prev-button ng-md-icon, 147 | md-tabs-wrapper md-next-button ng-md-icon { 148 | position: absolute; 149 | top: 50%; 150 | left: 50%; 151 | -webkit-transform: translate3d(-50%, -50%, 0); 152 | transform: translate3d(-50%, -50%, 0); 153 | } 154 | 155 | md-tabs-wrapper md-next-button ng-md-icon { 156 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(180deg); 157 | transform: translate3d(-50%, -50%, 0) rotate(180deg); 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PiLapseRails 2 | Web app for controlling DIY raspberry time lapse rails 3 | 4 | ![screenshot](/samples/screen2.jpg?raw=true "screenshot") ![screenshot](/samples/screen1.jpg?raw=true "screenshot") 5 | 6 | [Hardware](/samples/circuit.jpg) 7 | 8 | [shutter schematics](/samples/mosfet.jpg) 9 | 10 | When looking into time lapse rails, i ran across I ran across [David Hunt's](http://www.davidhunt.ie/lapse-pi-touch-a-touchscreen-timelapse-controller/) post on building a diy version using raspberry Pi. 11 | The article was quite nice with a lot of information on the hardware... i however did not want to buy an extra touch screen and running it through command line sounded pretty insane so i went on to a quest to do something about it :) 12 | A couple of evenings later, there was a solution :) i've put together a little web app that runs on a nodeJS server on the PI which can then be accessed on your mobile device if it is sharing its wifi with the raspberry. 13 | The connection between the web app (on the phone) and the server(on the pi) is done through websockets so its more or less real time which makes it fast and responsive. 14 | The runtime logic is on the nodejs side so if you walk away or close the browser, the time lapse will keep on running and you just need to open the app again to regain control/feedback. 15 | 16 | And since i also had hell of a time trying to piece the whole picture together (there information on david's post but you had to figure a lot of things out on your own) then i figured that i'd create a dumb down guide on the hardware as well which someone with barely any knowledge on the subject should be able to follow: 17 | [http://blog.koivistik.com/pilapserails-raspberrypi-time-lapse-rails/](http://blog.koivistik.com/pilapserails-raspberrypi-time-lapse-rails/) 18 | 19 | 20 | ### Features 21 | * Remembers settings across sessions 22 | * Configure rail length (will limit the movement accordingly) 23 | * Shots/time/inteval estimation & feedback 24 | * configure a lapse by setting the direction, motor length & "wait" length 25 | * bulb mode which lets you define shutter speed 26 | * optional focus lenght 27 | * loop (will keep on lapsing back and fourth on the rail) 28 | * manual motor controls 29 | * flip motor pins 30 | * test shot 31 | 32 | ### Installing 33 | 34 | * Get a [rasbian](https://www.raspberrypi.org/help/quick-start-guide/) installed on your raspberry 35 | * connect to wifi/ethernet 36 | * open up terminal (the black monitor icon) and enter the following commands: 37 | * in case its a fresh rasbian installation, get your os updated with: 38 | 39 | 40 | ``` 41 | sudo apt-get update //(in case its a fresh rasbian installation) 42 | ``` 43 | 44 | * install nodejs (server side BE language) 45 | 46 | ``` 47 | sudo apt-get install nodejs npm 48 | ``` 49 | 50 | * web framework for providing the frontend 51 | 52 | ``` 53 | sudo npm install -g express 54 | 55 | ``` 56 | * provides permission to use pins 57 | 58 | ``` 59 | git clone git://github.com/equilerex/quick2wire-gpio-admin-permission-fix 60 | cd quick2wire-gpio-admin-permission-fix/ 61 | make 62 | sudo make install 63 | sudo adduser $USER gpio 64 | cd .. 65 | ``` 66 | 67 | * get our actual project files 68 | 69 | ``` 70 | git clone git://github.com/equilerex/PiLapseRails 71 | ``` 72 | 73 | * install pi-gpio pin controller software... including it in the package.json didnt work so have to do it here 74 | 75 | ``` 76 | cd PiLapseRails/ 77 | npm install pi-gpio 78 | ``` 79 | 80 | * project setup (downloads all dependencies) 81 | 82 | ``` 83 | npm install 84 | ``` 85 | 86 | 87 | * In order to start the app every time you boot raspberry, we need to add it to the startup process: 88 | 89 | ``` 90 | cd .. 91 | cd .. 92 | cd .. 93 | sudo nano etc/rc.local 94 | //use arrows to move to the end of the file and on a line before "exit 0" type: 95 | su pi -c 'node /home/pi/PiLapseRails/bin/www.js &' 96 | //hit ctrl + o to save 97 | //hit enter to confirm name 98 | ``` 99 | 100 | * reboot your raspberry 101 | 102 | ### Connecting to your mobile 103 | -make a tethered hotspot (portable wifi got-spot) on the phone which you'll be using 104 | -on pi, connect to that hotspot. 105 | -If you are at home and used the wifi for setting everything up, you might want to remove that wifi connection so the pi would always connect to the phone automatically 106 | -in terminal, write: 107 | 108 | ``` 109 | ifconfig 110 | ``` 111 | 112 | * somewhere around where it says wlan0, find the ip address listed behind "inet addr" mine for example was 192.168.43.80 113 | * now go on your phone and type what you found in a browser of your choice and add :8080 to the end of it, so in my case, it was 192.168.43.80:8080 114 | * You're done! have fun! 115 | 116 | ### Updating 117 | ``` 118 | cd PiLapsRails/ 119 | git pull 120 | *restart your pi* 121 | ``` 122 | 123 | ### Author 124 | 125 | **Joosep Kõivistik** - [homepage](http://koivistik.com) | [blog](http://blog.koivistik.com) 126 | 127 | ### Nice to know 128 | * Half of the "wait length" is used before the rail moves, the other after it stops to ensure most stable conditions 129 | * Reset button is there in case the software screws up the saved settings (should be bug free but you never know!) 130 | * wifi can ba a bitch since sometimes the pi just doesnt wanna automatically connect to your phone... might be good to look into making raspberry the hotspot/access point instead 131 | 132 | 133 | ## Acknowledgments 134 | 135 | Big thanks to: 136 | * [David Hunt](http://www.davidhunt.ie/lapse-pi-touch-a-touchscreen-timelapse-controller/) for good write-up on the hardware and inspiration for the project 137 | * [Donald Derek](http://blog.donaldderek.com/2013/06/build-your-own-google-tv-using-raspberrypi-nodejs-and-socket-io/) for inspiration on the websocket approach 138 | * Dad... thanks for the help on the hardware :) 139 | * [pi-gpio](https://github.com/rakeshpai/pi-gpio) 140 | -------------------------------------------------------------------------------- /public/app/components/main/main.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |

Noooope

12 |
13 |
14 | 15 |

16 | build SETTINGS

17 |
18 |
19 | 20 | 21 | 27 | 28 | 31 | Enable focusing 32 | 33 | 36 | Remember position on boot 37 | 38 | 41 | Flip back/forward 42 | 43 | 44 |
45 | 46 |
47 |
48 | Reset Settings 51 |
52 |
53 | 54 |

55 | compare_arrowsmanual control 56 |

57 |
58 |
59 |
60 | 61 |
62 | 68 | Move forward 69 | 70 | 74 | Move Backward 75 | 76 |
77 | 78 | 79 | 80 | 85 | 86 | 87 | 91 | Set as new 0 position 92 | 93 |
94 |
95 | 96 |

97 | settingsRaspberry 98 |

99 |
100 |
101 |
102 | 106 | Shut off Raspberry 107 | 108 |
109 |
110 | PiLapseRails © Koivistik.com 111 |
112 |
113 |
114 | 115 |
116 | 117 |

{{railStatus.count}} / {{railStatus.shotsLeft}}

118 |
119 |
120 | 121 | 122 |

{{railStatus.timeLeft}}

123 |
124 |
125 | 126 | 127 |

{{railStatus.interval}}

128 |
129 |
130 |
131 |
132 |

Please configure the length of your rails in the settings menu

133 |
134 |
135 |
136 |
137 | 138 | 139 | 145 | 146 |
147 |
148 |
149 |
150 | 151 | Bulb Mode 152 | 153 |
154 |
155 |
156 | 157 | 158 | 164 | 165 |
166 |
167 | 168 | Test Shot 169 | 170 |
171 |
172 |
173 |
174 |
175 | 176 | 177 | 183 | 184 |
185 |
186 | 187 | 188 | 194 | 195 |
196 |
197 |
198 |
199 | 202 | Loop 203 | 204 |
205 |
206 | 207 | 208 | 214 | 215 |
216 |
217 |
218 |
219 | 222 | {{lapseConf.direction === true?"Forward":"Backward"}} 223 | 224 |
225 |
226 |
227 | Start Timelapsechange direction! 229 | 230 |
231 |
232 |
233 |
234 | 235 |
236 |
237 | Stop 238 |
239 |
240 |
241 | 242 |
243 |

Safe to power off your raspberry in a few moments...

244 |
245 | 246 |
247 |

Connection with raspberry lost! Get closer, make sure it is working properly and refresh the browser

248 |
249 | 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /public/app/components/main/main-controller.js: -------------------------------------------------------------------------------- 1 | //*********************************************************** 2 | // PiLapseRails 3 | // raspberry Pi powered timelapse controller 4 | // author: Joosep Kõivistik // koivistik.com 5 | // repository: https://github.com/equilerex/PiLapseRails 6 | //*********************************************************** 7 | 8 | 9 | angular.module("ngapp").controller("MainController", function(shared, $state, $scope, $mdSidenav, $mdComponentRegistry, $timeout,$mdToast, $location){ 10 | //*********************************************************** 11 | // dev mode... set true in www.js and main-controller.js & uncomment http://localhost:8080/public/app/vendor/socket.js in index.html and comment out src="http://192.168.43.80:8080/public/app/vendor/socket.js 12 | //*********************************************************** 13 | var windowsDevEnvironment = true; //set true in() 14 | 15 | //*********************************************************** 16 | // settings slide out bar 17 | //*********************************************************** 18 | var ctrl = this; 19 | this.auth = shared.info.auth; 20 | this.toggle = angular.noop; 21 | this.title = $state.current.title; 22 | this.isOpen = function() { return false }; 23 | $mdComponentRegistry 24 | .when("left") 25 | .then( function(sideNav){ 26 | ctrl.isOpen = angular.bind( sideNav, sideNav.isOpen ); 27 | ctrl.toggle = angular.bind( sideNav, sideNav.toggle ); 28 | }); 29 | this.toggleRight = function() { 30 | $mdSidenav("left").toggle() 31 | .then(function(){ 32 | }); 33 | }; 34 | this.close = function() { 35 | $mdSidenav("right").close() 36 | .then(function(){ 37 | }); 38 | }; 39 | //close menu when moving back 40 | $scope.openMenu = function () { 41 | $location.search("menuOpen", true) 42 | }; 43 | $scope.$on('$locationChangeStart', function(event, next, current) { 44 | if(current.split("#")[1]==="/main?menuOpen" && next.split("#")[1]==="/main") { 45 | if($scope.main.isOpen()) { 46 | $scope.main.toggle() 47 | } 48 | } 49 | }); 50 | 51 | 52 | //*********************************************************** 53 | // Available settings / default values 54 | //*********************************************************** 55 | $scope.defaultLapseConf = { 56 | "bulbMode":false, 57 | "shutterSpeed":1000, 58 | "focusLength":0, 59 | "motorPulse":2000, 60 | "waitLength":1000, 61 | "direction":true, //backward 62 | "loopEnabled":false, 63 | "loopCount":0 64 | }; 65 | $scope.defaultRailConf = { 66 | "focusEnabled":false, 67 | "rememberPosition":false 68 | }; 69 | $scope.railStatus = { 70 | "shotsLeft":0, 71 | "timeLeft":0, 72 | "interval":0 73 | }; 74 | $scope.lapseConf = {}; 75 | $scope.railConf = {}; 76 | var timeFormats = { 77 | minute:{ 78 | "human":"min | sec | ms", 79 | "machine":"mm:ss:SS" 80 | }, 81 | hour: { 82 | "human":"hour | min | sec", 83 | "machine":"HH:mm:ss" 84 | }, 85 | day: { 86 | "human":"Days", 87 | "machine":"day" 88 | } 89 | }; 90 | $scope.timeLeftFormat = timeFormats.underHour; 91 | $scope.intervalFormat = timeFormats.underHour; 92 | 93 | 94 | //*********************************************************** 95 | // progress / calculation feedback 96 | //*********************************************************** 97 | var loopAddon = function() { 98 | var loopAddon = 0; 99 | var count = 0; 100 | if( $scope.railStatus.loopCount) { 101 | count = $scope.railStatus.loopCount 102 | } else { 103 | count = $scope.lapseConf.loopCount 104 | } 105 | if($scope.lapseConf.loopEnabled && count>0) { 106 | return loopAddon = $scope.railConf.railLength*count; 107 | } else { 108 | return 0 109 | } 110 | }; 111 | //progress bar 112 | $scope.loadBar = function() { 113 | var calculate = ($scope.railStatus.shotsLeft-$scope.railStatus.count)/($scope.railStatus.shotsLeft/100) 114 | if ($scope.lapseConf.direction) { 115 | return parseInt(100 - calculate) 116 | } else { 117 | return parseInt(calculate) 118 | } 119 | }; 120 | //choose the best format for estimates 121 | var chooseFormat = function(timeType, duration) { 122 | 123 | //select format 124 | var type = "minute"; 125 | if(duration > 86400000) { 126 | type ="day" 127 | } else if(duration > 3600000) { 128 | type ="hour" 129 | } 130 | //save human readable value 131 | $scope[timeType] = timeFormats[type].human; 132 | //return value into the source function 133 | return timeFormats[type].machine; 134 | }; 135 | //shots calculation 136 | $scope.shotsLeft = function() { 137 | //shots depend on the rail length divided by motor pulse 138 | var value = 0; 139 | if($scope.lapseConf.direction) { 140 | value = parseInt((($scope.railConf.railLength-$scope.railStatus.currentPosition)+loopAddon())/$scope.lapseConf.motorPulse) 141 | } else { 142 | value = parseInt(($scope.railStatus.currentPosition+loopAddon())/$scope.lapseConf.motorPulse) 143 | } 144 | //last shot behaves a bit fishy 145 | if (value > 0) { 146 | return value + 1 147 | } else { 148 | return 0 149 | } 150 | }; 151 | //remaining time calculation 152 | $scope.timeLeft = function() { 153 | var timeLeft = $scope.intervalNumber()*$scope.shotsLeft(); 154 | var momentFormat = chooseFormat("timeLeftFormat",timeLeft) 155 | if(momentFormat === "day") { 156 | $scope.railStatus.timeLeft = parseInt(moment.duration(timeLeft).asDays()); 157 | } else { 158 | $scope.railStatus.timeLeft = moment(moment.duration(timeLeft)._data).format(momentFormat) 159 | } 160 | }; 161 | //interval calculation 162 | $scope.intervalNumber = function() { 163 | var interval = 0; 164 | if($scope.railConf.focusEnabled) { 165 | interval += parseInt($scope.lapseConf.focusLength) 166 | } 167 | if($scope.lapseConf.bulbMode) { 168 | interval += parseInt($scope.lapseConf.shutterSpeed) 169 | } 170 | interval += parseInt($scope.lapseConf.motorPulse); 171 | interval += parseInt($scope.lapseConf.waitLength); 172 | return interval 173 | 174 | }; 175 | //update all calculations 176 | $scope.updateEstimate = function(){ 177 | var multiply = 0; 178 | if($scope.railConf.loopEnabled) { 179 | multiply = $scope.railConf.loopCount 180 | } 181 | if(!$scope.railStatus.lapseInProgress) { 182 | $scope.railStatus.count = 0; 183 | $scope.railStatus.shotsLeft = $scope.shotsLeft(); 184 | } 185 | $scope.timeLeft(); 186 | var intervalNumber = $scope.intervalNumber(); 187 | var momentFormat = chooseFormat("intervalFormat",intervalNumber) 188 | if(momentFormat === "day") { 189 | $scope.railStatus.interval = parseInt(moment.duration(intervalNumber).asDays()) 190 | } else { 191 | $scope.railStatus.interval = moment(moment.duration(intervalNumber)._data).format(momentFormat); 192 | } 193 | }; 194 | 195 | //*********************************************************** 196 | //Communicate with Raspberry runtime 197 | //*********************************************************** 198 | //connect to raspberry data feed 199 | var socket = ""; 200 | //socket is on current environment (used to switch between dev environment and raspberry) 201 | var url = window.location.href ; 202 | var arr = url.split("/"); 203 | var absoluteUrl = arr[0] + "//" + arr[2]; 204 | socket = io.connect(absoluteUrl); 205 | socket.on("connect_error", function () { 206 | $scope.$apply(function() { 207 | $scope.disconnected = true; 208 | }); 209 | }); 210 | //device has been connected event 211 | socket.on('connect', function(data){ 212 | socket.emit('pageLoaded'); 213 | $scope.$apply(function() { 214 | $scope.disconnected = false; 215 | }); 216 | }); 217 | socket.on("disconnect", function () { 218 | $scope.$apply(function() { 219 | $scope.disconnected = true; 220 | }); 221 | }); 222 | //fetch saved data upon loading 223 | socket.on('connectionEstablished', function(data){ 224 | console.log(data) 225 | //use saved data 226 | $scope.$apply(function() { 227 | if (data.railConf) { 228 | console.log(1) 229 | $scope.railConf = data.railConf; 230 | //use default 231 | } else { 232 | console.log(2) 233 | $scope.railConf = $scope.defaultRailConf; 234 | } 235 | if (data.lapseConf) { 236 | console.log(3) 237 | $scope.lapseConf = data.lapseConf; 238 | //use default 239 | } else { 240 | console.log(4) 241 | $scope.lapseConf = $scope.defaultLapseConf; 242 | } 243 | $timeout(function(){$scope.updateEstimate()},300) 244 | $scope.connectionEstablished = true; 245 | }); 246 | $scope.$apply(function() { 247 | $scope.disconnected = false; 248 | }); 249 | }); 250 | //active timelapse feedback info 251 | socket.on('timelapseStatus', function (data) { 252 | $scope.$apply(function() { 253 | $scope.railStatus = data; 254 | $scope.updateEstimate() 255 | }); 256 | }); 257 | //run the timelapse 258 | $scope.runTimelapse = function() { 259 | socket.emit('runTimelapse', {"lapseConf":$scope.lapseConf,"railConf":$scope.railConf, "railStatus":$scope.railStatus}); 260 | }; 261 | //cancel timelapse 262 | $scope.cancelTimelapse = function(source) { 263 | socket.emit('cancelTimelapse', {"lapseConf":$scope.lapseConf,"railConf":$scope.railConf}); 264 | }; 265 | //manual move 266 | $scope.manualSlide = function(direction, state) { 267 | socket.emit('manualSlide',{direction:direction, state:!state}); 268 | }; 269 | //reset RailConf 270 | $scope.resetConf = function(file, data) { 271 | $scope.railConf = angular.copy($scope.defaultRailConf) 272 | socket.emit('saveSettings',{"file":"railconf","data":$scope.defaultRailConf}); 273 | $scope.lapseConf = angular.copy($scope.defaultLapseConf); 274 | socket.emit('saveSettings',{"file":"lapseconf","data":$scope.defaultLapseConf}); 275 | }; 276 | //set current position as new "0" 277 | $scope.resetCurrentPosition = function() { 278 | $scope.railStatus.currentPosition = 0; 279 | socket.emit('resetPosition', $scope.railStatus); 280 | $scope.showSimpleToast("Position set to 0"); 281 | }; 282 | //save settings 283 | $scope.saveSettings = function(file, data) { 284 | socket.emit('saveSettings',{"file":file,"data":data}); 285 | }; 286 | //active timelapse feedback info 287 | socket.on('errorOnSave', function (data) { 288 | $scope.showSimpleToast('Something went wrong while saving settings, better hit "Reset settings" button to ensure nothing is screwed :)'); 289 | }); 290 | //confirmation messages 291 | $scope.showSimpleToast = function(message) { 292 | $mdToast.show( 293 | $mdToast.simple() 294 | .textContent(message) 295 | .position("top") 296 | .hideDelay(4000) 297 | ); 298 | }; 299 | //shut off 300 | $scope.shutOffPi = function() { 301 | $scope.main.toggle(); 302 | $scope.poweredOff = true; 303 | socket.emit('shutOffPi',"shut it!"); 304 | }; 305 | //save settings 306 | $scope.testShot = function() { 307 | socket.emit('testShot',{"lapseConf":$scope.lapseConf,"railConf":$scope.railConf, "railStatus":$scope.railStatus}); 308 | }; 309 | }) 310 | .directive("loseFocus", function() { 311 | return { 312 | restrict: 'A', 313 | link: function($scope,elem,attrs) { 314 | elem.bind('keydown', function(e) { 315 | var code = e.keyCode || e.which; 316 | if (code === 13) { 317 | e.preventDefault(); 318 | elem.blur(); 319 | } 320 | }); 321 | } 322 | } 323 | }); -------------------------------------------------------------------------------- /bin/www.js: -------------------------------------------------------------------------------- 1 | //*********************************************************** 2 | // PiLapseRails 3 | // raspberry Pi powered timelapse controller 4 | // author: Joosep Kõivistik // koivistik.com 5 | // repository: https://github.com/equilerex/PiLapseRails 6 | //*********************************************************** 7 | 8 | 9 | 10 | //*********************************************************** 11 | // dev mode... set true if windows 12 | var os = require('os') 13 | var windowsDevEnvironment = os.platform() === "win32"; 14 | 15 | //*********************************************************** 16 | // Dummy / demo function to test on windows, uncomment to test 17 | //*********************************************************** 18 | var gpio = { 19 | "read": function (nr, state) { 20 | console.log("read pin " + gpio.definitions[nr]) 21 | }, 22 | "open": function (nr, state, func) { 23 | console.log("opened pin " + gpio.definitions[nr]) 24 | func() 25 | }, 26 | "close": function (nr, state, func) { 27 | console.log("stop " + gpio.definitions[nr]) 28 | }, 29 | "write": function (nr, state, func) { 30 | console.log(gpio.definitions.state[state]+" "+gpio.definitions[nr]) 31 | func() 32 | }, 33 | "definitions": { 34 | 11: "focus", 35 | 12: "shutter", 36 | 15: "motor forward", 37 | 16: "motor back", 38 | "state":{ 39 | 1:"start", 40 | 0:"stop" 41 | } 42 | } 43 | }; 44 | //normal pin plugin (override dummy) 45 | if(!windowsDevEnvironment) { 46 | gpio = require("pi-gpio") 47 | } 48 | 49 | //*********************************************************** 50 | // node setup 51 | //*********************************************************** 52 | var express = require('express') 53 | , app = express() 54 | , server = require('http').createServer(app) 55 | , path = require('path') 56 | //websocket connection 57 | , io = require('socket.io').listen(server) 58 | , spawn = require('child_process').spawn 59 | ,exec = require('child_process').exec 60 | //pin accesss 61 | //saving settings locally 62 | , fs = require('fs'); 63 | app.engine('html', require('ejs').renderFile); 64 | app.set('view engine', 'ejs'); 65 | app.set('port', process.env.TEST_PORT || 8080); 66 | //set default route 67 | /* GET home page. */ 68 | app.get('/', function(req, res, next) { 69 | res.render(path.join(__dirname, '../public/app/views/index'), { root: path.join(__dirname, '../public/'), "absoluteUrl":req.headers.host}); 70 | }); 71 | 72 | //make files accessible in public folder 73 | app.use("/public", express.static(path.join(__dirname, '../public'))); 74 | 75 | 76 | //*********************************************************** 77 | // pin configuration (feel free to change) using values from here: https://github.com/rakeshpai/pi-gpio 78 | //*********************************************************** 79 | var pinConf = { 80 | "focus": 11, 81 | "shutter": 12, 82 | "forward": 15, 83 | "back": 16 84 | }; 85 | 86 | //*********************************************************** 87 | // Status feedback 88 | //*********************************************************** 89 | var railStatus = { 90 | "currentPosition":0, 91 | "lapseInProgress":false, 92 | "forward":false, 93 | "back":false, 94 | "count":0 95 | }; 96 | 97 | var serverRailConf = false; 98 | var serverLapseConf = false; 99 | 100 | //*********************************************************** 101 | // switch back and forward pins (in case you set up your rail the other way around) 102 | //*********************************************************** 103 | var flipDirection = function() { 104 | var newForward = pinConf.back; 105 | var newBack = pinConf.forward; 106 | pinConf.forward = newForward; 107 | pinConf.back = newBack; 108 | }; 109 | 110 | //*********************************************************** 111 | // get saved settings 112 | //*********************************************************** 113 | fs.readFile(path.join(__dirname, '../public/app/railconf.json'), 'utf8', function (err, savedRailconf) { 114 | fs.readFile(path.join(__dirname, '../public/app/lapseconf.json'), 'utf8', function (err2, savedLapseconf) { 115 | if (savedLapseconf && savedLapseconf.length > 0) { 116 | //store for socket connections 117 | serverLapseConf = JSON.parse(savedLapseconf); 118 | } 119 | if (savedRailconf && savedRailconf.length > 0) { 120 | //store for socket connections 121 | serverRailConf = JSON.parse(savedRailconf); 122 | //if position should be remembered on boot, restore it. 123 | if(serverRailConf.rememberPosition && serverRailConf.savedPosition) { 124 | railStatus.currentPosition = serverRailConf.savedPosition 125 | } 126 | //if position should be remembered on boot, restore it. 127 | if(serverRailConf.flipDirection ) { 128 | flipDirection() 129 | } 130 | }; 131 | }); 132 | 133 | }); 134 | 135 | //*********************************************************** 136 | //open pins for business 137 | //*********************************************************** 138 | var openErr = false; 139 | var openCount = 0; 140 | var openPins = function() { 141 | //failsafe 142 | openCount+=1 143 | var markError = function(err) { 144 | if(err) { 145 | openErr = true 146 | } 147 | }; 148 | gpio.open(pinConf.focus, "output", function (err) {markError()}); 149 | gpio.open(pinConf.shutter, "output", function (err) {markError()}); 150 | gpio.open(pinConf.forward, "output", function (err) {markError()}); 151 | gpio.open(pinConf.back, "output", function (err) {markError()}); 152 | //if outdated session then opening nodes that are open already produces error 153 | setTimeout(function () { 154 | if (openErr && openCount < 5) { 155 | console.log("try to close and open pins") 156 | gpio.close(pinConf.focus); 157 | gpio.close(pinConf.shutter); 158 | gpio.close(pinConf.forward); 159 | gpio.close(pinConf.back); 160 | openErr = false; 161 | 162 | setTimeout(function () { 163 | openPins() 164 | },2000); 165 | } 166 | },2000) 167 | }; 168 | openPins(); 169 | 170 | //*********************************************************** 171 | // if you want to save position on boot 172 | //*********************************************************** 173 | var railMoved = function() { 174 | if(serverRailConf.rememberPosition) { 175 | serverRailConf.savedPosition = railStatus.currentPosition; 176 | fs.writeFile(path.join(__dirname, '../public/app/railconf.json'), JSON.stringify(serverRailConf), "utf8", function (err) { 177 | if(err) { 178 | plr.emit("errorOnSave"); 179 | } 180 | }); 181 | } 182 | }; 183 | 184 | //*********************************************************** 185 | // cancel active timelapse 186 | //*********************************************************** 187 | var stopTimelapse = function(lapseConf) { 188 | lastMotorStop = new Date().getTime(); 189 | clearTimeout(focusEvent); 190 | clearTimeout(shutterEvent); 191 | clearTimeout(engineEvent); 192 | clearTimeout(intetvalEvent); 193 | clearTimeout(intetvalEvent2); 194 | 195 | 196 | gpio.write(pinConf.shutter, 0, function () {}); 197 | gpio.write(pinConf.focus, 0, function () {}); 198 | gpio.write(pinConf.forward, 0, function () {}); 199 | gpio.write(pinConf.back, 0, function () {}); 200 | 201 | 202 | // if stop was in the middle of a motor run, add that in the status 203 | if(lastMotorStart) { 204 | var motorRun = lastMotorStop - lastMotorStart; 205 | if (lapseConf.direction === true) { 206 | railStatus.currentPosition += parseInt(motorRun) 207 | } else if (lapseConf.direction === false) { 208 | railStatus.currentPosition -= parseInt(motorRun) 209 | } 210 | } 211 | //update status 212 | railStatus.lapseInProgress = false; 213 | delete railStatus.loopCount; 214 | //save state 215 | railMoved(); 216 | plr.emit("timelapseStatus", railStatus); 217 | }; 218 | 219 | //*********************************************************** 220 | // Running timelapse logic 221 | //*********************************************************** 222 | //set timeout values 223 | var focusEvent 224 | ,shutterEvent 225 | ,engineEvent 226 | ,intetvalEvent 227 | ,intetvalEvent2; 228 | 229 | //in case motor is stopped, we need the new locaton 230 | var lastMotorStart; 231 | var lastMotorStop; 232 | var runTimeLapse = function(data) { 233 | //shots left is static per session 234 | railStatus.shotsLeft = data.railStatus.shotsLeft; 235 | //make a local copy 236 | railStatus.loopCount = data.lapseConf.loopCount; 237 | //select right gpio pin for motor direction 238 | var motorGpio = pinConf.forward; 239 | var selectMotorPin = function(){ 240 | if (!data.lapseConf.direction) { 241 | motorGpio = pinConf.back; 242 | } else { 243 | motorGpio = pinConf.forward 244 | } 245 | }; 246 | selectMotorPin(); 247 | 248 | //store new settings locally in case of restart 249 | fs.writeFile(path.join(__dirname, '../public/app/lapseconf.json'), JSON.stringify(data.lapseConf), "utf8", function (err) { 250 | //save new settings server side 251 | serverLapseConf = data.lapseConf; 252 | if(err) { 253 | plr.emit("errorOnSave"); 254 | } 255 | }); 256 | 257 | 258 | //set shutter speed default length if bulb disabled 259 | if (!data.lapseConf.bulbMode) { 260 | data.lapseConf.shutterSpeed = 0 261 | } 262 | //single shot cycle 263 | var shutterCycle = function () { 264 | //trigger focus & wait for focus length 265 | gpio.write(pinConf.focus, 1, function () { 266 | focusEvent = setTimeout(function () { 267 | //trigger shutter & wait for shutter speed if any 268 | gpio.write(pinConf.shutter, 1, function () { 269 | shutterEvent = setTimeout(function () { 270 | //release shutter / focus 271 | gpio.write(pinConf.shutter, 0, function () {}); 272 | gpio.write(pinConf.focus, 0, function () {}); 273 | //update status 274 | railStatus.count = railStatus.count+1; 275 | plr.emit("timelapseStatus", railStatus); 276 | //half of "wait" before engine, half after in order to stabilize the rails 277 | intetvalEvent = setTimeout(function () { 278 | //if loop enabled and end of rails, switch direction 279 | if(data.lapseConf.loopEnabled && railStatus.loopCount>0) { 280 | 281 | if (data.lapseConf.direction && railStatus.currentPosition <= data.railConf.railLength - data.lapseConf.motorPulse || !data.lapseConf.direction && railStatus.currentPosition >= data.lapseConf.motorPulse) { 282 | //ignore 283 | } else { 284 | //count loop 285 | railStatus.loopCount -=1; 286 | //switch direction 287 | data.lapseConf.direction = !data.lapseConf.direction; 288 | //switch pins 289 | selectMotorPin(); 290 | } 291 | } 292 | //trigger motor if theres still room for it 293 | if (data.lapseConf.direction && railStatus.currentPosition <= data.railConf.railLength - data.lapseConf.motorPulse || !data.lapseConf.direction && railStatus.currentPosition >= data.lapseConf.motorPulse) { 294 | gpio.write(motorGpio, 1, function () { 295 | //log start time 296 | lastMotorStart = new Date().getTime(); 297 | //stop motor 298 | engineEvent = setTimeout(function () { 299 | lastMotorStart = false; 300 | gpio.write(motorGpio, 0, function () {}); 301 | //calculate new position 302 | if (data.lapseConf.direction === true) { 303 | railStatus.currentPosition += parseInt(data.lapseConf.motorPulse) 304 | } else if (data.lapseConf.direction === false) { 305 | railStatus.currentPosition -= parseInt(data.lapseConf.motorPulse) 306 | } 307 | plr.emit("timelapseStatus", railStatus); 308 | //wait til interval finishes before shooting again 309 | intetvalEvent2 = setTimeout(function () { 310 | //restart cycle 311 | shutterCycle() 312 | }, data.lapseConf.waitLength / 2); 313 | }, data.lapseConf.motorPulse); 314 | }); 315 | } else { 316 | stopTimelapse(data.lapseConf); 317 | } 318 | }, data.lapseConf.waitLength / 2); 319 | }, data.lapseConf.shutterSpeed) 320 | }) 321 | }, data.lapseConf.focusLength) 322 | }); 323 | }; 324 | 325 | //trigger the cycle for the first time 326 | shutterCycle(); 327 | //update status 328 | railStatus.lapseInProgress = true; 329 | railStatus.count = 0; 330 | plr.emit("timelapseStatus", railStatus); 331 | }; 332 | 333 | //*********************************************************** 334 | // manual rail slide 335 | //*********************************************************** 336 | var timer; 337 | var manualDirection = false; 338 | var updateStopWatch = function() { 339 | if(manualDirection==="forward") { 340 | railStatus.currentPosition += 100; 341 | } else { 342 | railStatus.currentPosition -= 100; 343 | } 344 | plr.emit("timelapseStatus", railStatus); 345 | timer = setTimeout(function() { 346 | updateStopWatch() 347 | }, 100); 348 | }; 349 | var runManualSlide = function(data) { 350 | //clear states 351 | gpio.write(pinConf.forward, 0, function () {}); 352 | gpio.write(pinConf.back, 0, function () {}); 353 | //start or stop motor 354 | railStatus[data.direction] = data.state; 355 | if (data.state) { 356 | railStatus.lapseInProgress = true; 357 | gpio.write(pinConf[data.direction], 1, function () {}); 358 | manualDirection = data.direction; 359 | updateStopWatch(); 360 | } else { 361 | railStatus.lapseInProgress = false; 362 | manualDirection = false; 363 | clearTimeout(timer); 364 | //save state 365 | railMoved(); 366 | plr.emit("timelapseStatus", railStatus); 367 | } 368 | }; 369 | 370 | //*********************************************************** 371 | // Socket.io Congfig 372 | //*********************************************************** 373 | server.listen(app.get('port'), function () { 374 | console.log('Express server listening on port ' + app.get('port')); 375 | }); 376 | //Save the Screen Socket in this variable 377 | var plr; 378 | //*********************************************************** 379 | // Socket listener events 380 | //*********************************************************** 381 | io.sockets.on('connection', function (socket) { 382 | //device connected 383 | socket.on("pageLoaded", function () { 384 | plr = socket; 385 | //If there are saved values from last session, send them to frontend 386 | var data = { 387 | "lapseConf":false, 388 | "railConf":false 389 | }; 390 | if (serverLapseConf) { 391 | //pass saved config to frontend 392 | data.lapseConf = serverLapseConf; 393 | } 394 | if (serverRailConf) { 395 | //pass saved config to frontend 396 | data.railConf = serverRailConf 397 | } 398 | //send current status 399 | plr.emit("connectionEstablished", data); 400 | plr.emit("timelapseStatus", railStatus); 401 | 402 | }); 403 | 404 | socket.on("saveSettings", function (data) { 405 | if(data.file === "railconf") { 406 | //in remember enabled, save status right away 407 | if(data.data.rememberPosition && serverRailConf.rememberPosition === false) { 408 | railMoved() 409 | } 410 | //flip motor pins 411 | if(serverRailConf.flipDirection !== data.data.flipDirection) { 412 | flipDirection() 413 | } 414 | //set new data 415 | serverRailConf = data.data; 416 | } 417 | //store settings locally for the next restart 418 | fs.writeFile(path.join(__dirname, '../public/app/'+data.file+'.json'), JSON.stringify(data.data), "utf8", function (err) { 419 | //save new settings on the server 420 | if(data.file === "railconf") { 421 | serverRailConf = data.data; 422 | } else if(data.file === "lapseconf") { 423 | serverLapseConf = data.data; 424 | } 425 | if(err) { 426 | plr.emit("errorOnSave"); 427 | } 428 | }); 429 | }); 430 | //running timelapse call 431 | socket.on("runTimelapse", function (conf) { 432 | runTimeLapse(conf) 433 | }); 434 | // cancel active timelapse call 435 | socket.on("cancelTimelapse", function (data) { 436 | stopTimelapse(data) 437 | }); 438 | //manual slide call 439 | socket.on("manualSlide", function (data) { 440 | runManualSlide(data) 441 | }); 442 | //update position 443 | socket.on("resetPosition", function (data) { 444 | railStatus.currentPosition = 0; 445 | }); 446 | //shut down 447 | socket.on("shutOffPi", function (data) { 448 | exec("sudo shutdown -h now", function (error, stdout, stderr) { 449 | return; 450 | }); 451 | }); 452 | //test shot 453 | socket.on("testShot", function (data) { 454 | if (!data.lapseConf.bulbMode) { 455 | data.lapseConf.shutterSpeed = 0 456 | } 457 | gpio.write(pinConf.focus, 1, function () { 458 | focusEvent = setTimeout(function () { 459 | //trigger shutter & wait for shutter speed if any 460 | gpio.write(pinConf.shutter, 1, function () { 461 | shutterEvent = setTimeout(function () { 462 | //release shutter / focus 463 | gpio.write(pinConf.shutter, 0, function () {}); 464 | gpio.write(pinConf.focus, 0, function () {}); 465 | }, data.lapseConf.shutterSpeed) 466 | }) 467 | }, data.lapseConf.focusLength) 468 | }); 469 | }); 470 | 471 | }); -------------------------------------------------------------------------------- /public/app/assets/lib/angular-animate.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.4.8 3 | (c) 2010-2015 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(H,u,Sa){'use strict';function wa(a,b,c){if(!a)throw ngMinErr("areq",b||"?",c||"required");return a}function xa(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;X(a)&&(a=a.join(" "));X(b)&&(b=b.join(" "));return a+" "+b}function Ia(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function T(a,b,c){var d="";a=X(a)?a:a&&I(a)&&a.length?a.split(/\s+/):[];q(a,function(a,s){a&&0=a&&(a=m,m=0,b.push(e),e=[]);e.push(h.fn);h.children.forEach(function(a){m++;c.push(a)});a--}e.length&&b.push(e);return b}(c)}var $=[],u=N(a);return function(g,C,D){function K(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];q(a,function(a){var c=a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b} 30 | function l(a){var b=[],c={};q(a,function(a,f){var d=B(a.element),t=0<=["enter","move"].indexOf(a.event),d=a.structural?K(d):[];if(d.length){var m=t?"to":"from";q(d,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]||{};c[b][m]={animationID:f,element:L(a)}})}else b.push(a)});var f={},d={};q(c,function(c,m){var w=c.from,e=c.to;if(w&&e){var h=a[w.animationID],g=a[e.animationID],x=w.animationID.toString();if(!d[x]){var A=d[x]={structural:!0,beforeStart:function(){h.beforeStart();g.beforeStart()}, 31 | close:function(){h.close();g.close()},classes:y(h.classes,g.classes),from:h,to:g,anchors:[]};A.classes.length?b.push(A):(b.push(h),b.push(g))}d[x].anchors.push({out:w.element,"in":e.element})}else w=w?w.animationID:e.animationID,e=w.toString(),f[e]||(f[e]=!0,b.push(a[w]))});return b}function y(a,b){a=a.split(" ");b=b.split(" ");for(var c=[],f=0;fG.expectedEndTime)?h.cancel(G.timer):l.push(m)}r&&(v=h(d,v,!1),l[0]={timer:v,expectedEndTime:k},l.push(m),a.data("$$animateCss",l));a.on(x.join(" "),g);c.to&&(c.cleanupStyles&&Ea(t,n,Object.keys(c.to)),za(a,c))}}function d(){var b=a.data("$$animateCss");if(b){for(var c=1;c=N&&b>=J&&(va=!0,m())}if(!ga)if(n.parentNode){var A,x=[],l=function(a){if(va)k&&a&&(k=!1,m());else if(k=!a,E.animationDuration)if(a=na(n,k),k)y.push(a);else{var b=y,c=b.indexOf(a);0<=a&&b.splice(c,1)}},v=0>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return M({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(s[c]=d,I(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);L(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return J(a)&&a.then&&a.$$promises}if(!J(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return L(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!G(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;L(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!J(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=m;var n=a.defer(),r=n.promise,s=r.$$promises={},t=M({},d),u=1+q.length/3,v=!1;if(G(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,l(f.$$inheritedValues,p)),M(s,f.$$promises),f.$$values?(v=e(t,l(f.$$values,p)),r.$$inheritedValues=l(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=l(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function p(a,b,c){this.fromConfig=function(a,b,c){return G(a.template)?this.fromString(a.template,b):G(a.templateUrl)?this.fromUrl(a.templateUrl,b):G(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return H(a)?a(b):a},this.fromUrl=function(c,d){return H(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function q(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+(-+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new O.Param(b,c,d,e),p[b]}function g(a,b,c){var d=["",""],e=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return e;switch(c){case!1:d=["(",")"];break;case!0:d=["?(",")?"];break;default:d=["("+c+"|",")?"]}return e+d[0]+b+d[1]}function h(c,e){var f,g,h,i,j;return f=c[2]||c[3],j=b.params[f],h=a.substring(m,c.index),g=e?c[4]:c[4]||("*"==c[1]?".*":null),i=O.type(g||"string")||d(O.type("string"),{pattern:new RegExp(g)}),{id:f,regexp:g,segment:h,type:i,cfg:j}}b=M({params:{}},J(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new O.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function r(a){M(this,a)}function s(){function a(a){return null!=a?a.toString().replace(/\//g,"%2F"):a}function e(a){return null!=a?a.toString().replace(/%2F/g,"/"):a}function f(a){return this.pattern.test(a)}function i(){return{strict:t,caseInsensitive:p}}function j(a){return H(a)||K(a)&&H(a[a.length-1])}function k(){for(;x.length;){var a=x.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(v[a.name],o.invoke(a.def))}}function l(a){M(this,a||{})}O=this;var o,p=!1,t=!0,u=!1,v={},w=!0,x=[],y={string:{encode:a,decode:e,is:f,pattern:/[^/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return G(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^/]*/},any:{encode:b.identity,decode:b.identity,is:b.identity,equals:b.equals,pattern:/.*/}};s.$$getDefaultValue=function(a){if(!j(a.value))return a.value;if(!o)throw new Error("Injectable functions cannot be called at configuration time");return o.invoke(a.value)},this.caseInsensitive=function(a){return G(a)&&(p=a),p},this.strictMode=function(a){return G(a)&&(t=a),t},this.defaultSquashPolicy=function(a){if(!G(a))return u;if(a!==!0&&a!==!1&&!I(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return u=a,a},this.compile=function(a,b){return new q(a,M(i(),b))},this.isMatcher=function(a){if(!J(a))return!1;var b=!0;return L(q.prototype,function(c,d){H(c)&&(b=b&&G(a[d])&&H(a[d]))}),b},this.type=function(a,b,c){if(!G(b))return v[a];if(v.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return v[a]=new r(M({name:a},b)),c&&(x.push({name:a,def:c}),w||k()),this},L(y,function(a,b){v[b]=new r(M({name:b},a))}),v=d(v,{}),this.$get=["$injector",function(a){return o=a,w=!1,k(),L(y,function(a,b){v[b]||(v[b]=new r(a))}),this}],this.Param=function(a,b,d,e){function f(a){var b=J(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=j(a.value)?a.value:function(){return a.value},a}function i(b,c,d){if(b.type&&c)throw new Error("Param '"+a+"' has two type configurations.");return c?c:b.type?b.type instanceof r?b.type:new r(b.type):"config"===d?v.any:v.string}function k(){var b={array:"search"===e?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return M(b,c,d).array}function l(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!G(c)||null==c)return u;if(c===!0||I(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function p(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=K(a.replace)?a.replace:[],I(e)&&f.push({from:e,to:c}),g=n(f,function(a){return a.from}),m(i,function(a){return-1===h(g,a.from)}).concat(f)}function q(){if(!o)throw new Error("Injectable functions cannot be called at configuration time");return o.invoke(d.$$fn)}function s(a){function b(a){return function(b){return b.from===a}}function c(a){var c=n(m(w.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),G(a)?w.type.decode(a):q()}function t(){return"{Param:"+a+" "+b+" squash: '"+z+"' optional: "+y+"}"}var w=this;d=f(d),b=i(d,b,e);var x=k();b=x?b.$asArray(x,"search"===e):b,"string"!==b.name||x||"path"!==e||d.value!==c||(d.value="");var y=d.value!==c,z=l(d,y),A=p(d,x,y,z);M(this,{id:a,type:b,location:e,array:x,squash:z,replace:A,isOptional:y,value:s,dynamic:c,config:d,toString:t})},l.prototype={$$new:function(){return d(this,M(new l,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(l.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),L(b,function(b){L(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return L(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return L(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var b,c,d,e=!0,f=this;return L(this.$$keys(),function(g){d=f[g],c=a[g],b=!c&&d.isOptional,e=e&&(b||!!d.type.is(c))}),e},$$parent:c},this.ParamSet=l}function t(a,d){function e(a){var b=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(a.source);return null!=b?b[1].replace(/\\(.)/g,"$1"):""}function f(a,b){return a.replace(/\$(\$|\d{1,2})/,function(a,c){return b["$"===c?0:Number(c)]})}function g(a,b,c){if(!c)return!1;var d=a.invoke(b,b,{$match:c});return G(d)?d:!0}function h(d,e,f,g){function h(a,b,c){return"/"===p?a:b?p.slice(0,-1)+a:c?p.slice(1)+a:a}function m(a){function b(a){var b=a(f,d);return b?(I(b)&&d.replace().url(b),!0):!1}if(!a||!a.defaultPrevented){var e=o&&d.url()===o;if(o=c,e)return!0;var g,h=j.length;for(g=0;h>g;g++)if(b(j[g]))return;k&&b(k)}}function n(){return i=i||e.$on("$locationChangeSuccess",m)}var o,p=g.baseHref(),q=d.url();return l||n(),{sync:function(){m()},listen:function(){return n()},update:function(a){return a?void(q=d.url()):void(d.url()!==q&&(d.url(q),d.replace()))},push:function(a,b,e){d.url(a.format(b||{})),o=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled);var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),i=h(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!H(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(I(a)){var b=a;a=function(){return b}}else if(!H(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=I(b);if(I(a)&&(a=d.compile(a)),!h&&!H(b)&&!K(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),M(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:I(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),M(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser"]}function u(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function l(a,b){if(!a)return c;var d=I(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=l(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var m=y[e];return!m||!d&&(d||m!==a&&m.self!==a)?c:m}function m(a,b){z[a]||(z[a]=[]),z[a].push(b)}function o(a){for(var b=z[a]||[];b.length;)p(b.shift())}function p(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!I(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(y.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):I(b.parent)?b.parent:J(b.parent)&&I(b.parent.name)?b.parent.name:"";if(e&&!y[e])return m(e,b.self);for(var f in B)H(B[f])&&(b[f]=B[f](b,B.$delegates[f]));return y[c]=b,!b[A]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){x.$current.navigable==b&&j(a,c)||x.transitionTo(b,a,{inherit:!0,location:!1})}]),o(c),b}function q(a){return a.indexOf("*")>-1}function r(a){var b=a.split("."),c=x.$current.name.split(".");if("**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length)return!1;for(var d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return c.join("")===b.join("")}function s(a,b){return I(a)&&!G(b)?B[a]:H(b)&&I(a)?(B[a]&&!B.$delegates[a]&&(B.$delegates[a]=B[a]),B[a]=b,this):this}function t(a,b){return J(a)?b=a:b.name=a,p(b),this}function u(a,e,f,h,m,o,p){function s(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),B;if(!g.retry)return null;if(f.$retry)return p.update(),C;var h=x.transition=e.when(g.retry);return h.then(function(){return h!==x.transition?u:(b.options.$retry=!0,x.transitionTo(b.to,b.toParams,b.options))},function(){return B}),p.update(),h}function t(a,c,d,g,i,j){var l=d?c:k(a.params.$$keys(),c),n={$stateParams:l};i.resolve=m.resolve(a.resolve,n,i.resolve,a);var o=[i.resolve.then(function(a){i.globals=a})];return g&&o.push(g),L(a.views,function(c,d){var e=c.resolve&&c.resolve!==a.resolve?c.resolve:{};e.$template=[function(){return f.load(d,{view:c,locals:n,params:l,notify:j.notify})||""}],o.push(m.resolve(e,n,i.resolve,a).then(function(f){if(H(c.controllerProvider)||K(c.controllerProvider)){var g=b.extend({},e,n);f.$$controller=h.invoke(c.controllerProvider,null,g)}else f.$$controller=c.controller;f.$$state=a,f.$$controllerAs=c.controllerAs,i[d]=f}))}),e.all(o).then(function(){return i})}var u=e.reject(new Error("transition superseded")),z=e.reject(new Error("transition prevented")),B=e.reject(new Error("transition aborted")),C=e.reject(new Error("transition failed"));return w.locals={resolve:null,globals:{$stateParams:{}}},x={params:{},current:w.self,$current:w,transition:null},x.reload=function(){return x.transitionTo(x.current,o,{reload:!0,inherit:!1,notify:!0})},x.go=function(a,b,c){return x.transitionTo(a,b,M({inherit:!0,relative:x.$current},c))},x.transitionTo=function(b,c,f){c=c||{},f=M({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=x.$current,m=x.params,n=j.path,q=l(b,f.relative);if(!G(q)){var r={to:b,toParams:c,options:f},y=s(r,j.self,m,f);if(y)return y;if(b=r.to,c=r.toParams,f=r.options,q=l(b,f.relative),!G(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[A])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(o,c||{},x.$current,q)),!q.params.$$validates(c))return C;c=q.params.$$values(c),b=q;var B=b.path,D=0,E=B[D],F=w.locals,H=[];if(!f.reload)for(;E&&E===n[D]&&E.ownParams.$$equals(c,m);)F=H[D]=E.locals,D++,E=B[D];if(v(b,j,F,f))return b.self.reloadOnSearch!==!1&&p.update(),x.transition=null,e.when(x.current);if(c=k(b.params.$$keys(),c||{}),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,m).defaultPrevented)return p.update(),z;for(var I=e.when(F),J=D;J=D;d--)g=n[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d=0?e:e+"@"+(f?f.state.name:"")}function A(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function B(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function C(a,c){var d=["location","inherit","reload"];return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(e,f,g,h){var i=A(g.uiSref,a.current.name),j=null,k=B(f)||a.$current,l=null,m="A"===f.prop("tagName"),n="FORM"===f[0].nodeName,o=n?"action":"href",p=!0,q={relative:k,inherit:!0},r=e.$eval(g.uiSrefOpts)||{};b.forEach(d,function(a){a in r&&(q[a]=r[a])});var s=function(c){if(c&&(j=b.copy(c)),p){l=a.href(i.state,j,q);var d=h[1]||h[0];return d&&d.$$setStateInfo(i.state,j),null===l?(p=!1,!1):void g.$set(o,l)}};i.paramExpr&&(e.$watch(i.paramExpr,function(a){a!==j&&s(a)},!0),j=b.copy(e.$eval(i.paramExpr))),s(),n||f.bind("click",function(b){var d=b.which||b.button;if(!(d>1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target"))){var e=c(function(){a.go(i.state,j,q)});b.preventDefault();var g=m&&!l?1:0;b.preventDefault=function(){g--<=0&&c.cancel(e)}}})}}}function D(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(b,d,e){function f(){g()?d.addClass(j):d.removeClass(j)}function g(){return"undefined"!=typeof e.uiSrefActiveEq?h&&a.is(h.name,i):h&&a.includes(h.name,i)}var h,i,j;j=c(e.uiSrefActiveEq||e.uiSrefActive||"",!1)(b),this.$$setStateInfo=function(b,c){h=a.get(b,B(d)),i=c,f()},b.$on("$stateChangeSuccess",f)}]}}function E(a){var b=function(b){return a.is(b)};return b.$stateful=!0,b}function F(a){var b=function(b){return a.includes(b)};return b.$stateful=!0,b}var G=b.isDefined,H=b.isFunction,I=b.isString,J=b.isObject,K=b.isArray,L=b.forEach,M=b.extend,N=b.copy;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),o.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",o),p.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",p);var O;q.prototype.concat=function(a,b){var c={caseInsensitive:O.caseInsensitive(),strict:O.strictMode(),squash:O.defaultSquashPolicy()};return new q(this.sourcePath+a+this.sourceSearch,M(c,b),this)},q.prototype.toString=function(){return this.source},q.prototype.exec=function(a,b){function c(a){function b(a){return a.split("").reverse().join("")}function c(a){return a.replace(/\\-/,"-")}var d=b(a).split(/-(?!\\)/),e=n(d,b);return n(e,c).reverse()}var d=this.regexp.exec(a);if(!d)return null;b=b||{};var e,f,g,h=this.parameters(),i=h.length,j=this.segments.length-1,k={};if(j!==d.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");for(e=0;j>e;e++){g=h[e];var l=this.params[g],m=d[e+1];for(f=0;fe;e++)g=h[e],k[g]=this.params[g].value(b[g]);return k},q.prototype.parameters=function(a){return G(a)?this.params[a]||null:this.$$paramNames},q.prototype.validates=function(a){return this.params.$$validates(a)},q.prototype.format=function(a){function b(a){return encodeURIComponent(a).replace(/-/g,function(a){return"%5C%"+a.charCodeAt(0).toString(16).toUpperCase()})}a=a||{};var c=this.segments,d=this.parameters(),e=this.params;if(!this.validates(a))return null;var f,g=!1,h=c.length-1,i=d.length,j=c[0];for(f=0;i>f;f++){var k=h>f,l=d[f],m=e[l],o=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),o),q=p?m.squash:!1,r=m.type.encode(o);if(k){var s=c[f+1];if(q===!1)null!=r&&(j+=K(r)?n(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var t=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(t)[1]}else I(q)&&(j+=q+s)}else{if(null==r||p&&q!==!1)continue;K(r)||(r=[r]),r=n(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},r.prototype.is=function(){return!0},r.prototype.encode=function(a){return a},r.prototype.decode=function(a){return a},r.prototype.equals=function(a,b){return a==b},r.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},r.prototype.pattern=/.*/,r.prototype.toString=function(){return"{Type:"+this.name+"}"},r.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return K(a)?a:G(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){c=e(c);var d=n(c,a);return b===!0?0===m(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g0)for(c in Wc)d=Wc[c],e=b[d],m(e)||(a[d]=e);return a}function o(b){n(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),Xc===!1&&(Xc=!0,a.updateOffset(this),Xc=!1)}function p(a){return a instanceof o||null!=a&&null!=a._isAMomentObject}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=q(b)),c}function s(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&r(a[d])!==r(b[d]))&&g++;return g+f}function t(){}function u(a){return a?a.toLowerCase().replace("_","-"):a}function v(a){for(var b,c,d,e,f=0;f0;){if(d=w(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&s(e,c,!0)>=b-1)break;b--}f++}return null}function w(a){var b=null;if(!Yc[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Vc._abbr,require("./locale/"+a),x(b)}catch(c){}return Yc[a]}function x(a,b){var c;return a&&(c=m(b)?z(a):y(a,b),c&&(Vc=c)),Vc._abbr}function y(a,b){return null!==b?(b.abbr=a,Yc[a]=Yc[a]||new t,Yc[a].set(b),x(a),Yc[a]):(delete Yc[a],null)}function z(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Vc;if(!c(a)){if(b=w(a))return b;a=[a]}return v(a)}function A(a,b){var c=a.toLowerCase();Zc[c]=Zc[c+"s"]=Zc[b]=a}function B(a){return"string"==typeof a?Zc[a]||Zc[a.toLowerCase()]:void 0}function C(a){var b,c,d={};for(c in a)f(a,c)&&(b=B(c),b&&(d[b]=a[c]));return d}function D(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function E(b,c){return function(d){return null!=d?(G(this,b,d),a.updateOffset(this,c),this):F(this,b)}}function F(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function G(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function H(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=B(a),D(this[a]))return this[a](b);return this}function I(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function J(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(bd[a]=e),b&&(bd[b[0]]=function(){return I(e.apply(this,arguments),b[1],b[2])}),c&&(bd[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function K(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function L(a){var b,c,d=a.match($c);for(b=0,c=d.length;c>b;b++)bd[d[b]]?d[b]=bd[d[b]]:d[b]=K(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function M(a,b){return a.isValid()?(b=N(b,a.localeData()),ad[b]=ad[b]||L(b),ad[b](a)):a.localeData().invalidDate()}function N(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(_c.lastIndex=0;d>=0&&_c.test(a);)a=a.replace(_c,c),_c.lastIndex=0,d-=1;return a}function O(a,b,c){td[a]=D(b)?b:function(a,d){return a&&c?c:b}}function P(a,b){return f(td,a)?td[a](b._strict,b._locale):new RegExp(Q(a))}function Q(a){return R(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function R(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function S(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=r(a)}),c=0;cd;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function Z(a,b){var c;return a.isValid()?"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),V(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a):a}function $(b){return null!=b?(Z(this,b),a.updateOffset(this,!0),this):F(this,"Month")}function _(){return V(this.year(),this.month())}function aa(a){return this._monthsParseExact?(f(this,"_monthsRegex")||ca.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex}function ba(a){return this._monthsParseExact?(f(this,"_monthsRegex")||ca.call(this),a?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex}function ca(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=h([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=R(d[b]),e[b]=R(e[b]),f[b]=R(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")$","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")$","i")}function da(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[wd]<0||c[wd]>11?wd:c[xd]<1||c[xd]>V(c[vd],c[wd])?xd:c[yd]<0||c[yd]>24||24===c[yd]&&(0!==c[zd]||0!==c[Ad]||0!==c[Bd])?yd:c[zd]<0||c[zd]>59?zd:c[Ad]<0||c[Ad]>59?Ad:c[Bd]<0||c[Bd]>999?Bd:-1,j(a)._overflowDayOfYear&&(vd>b||b>xd)&&(b=xd),j(a)._overflowWeeks&&-1===b&&(b=Cd),j(a)._overflowWeekday&&-1===b&&(b=Dd),j(a).overflow=b),a}function ea(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function fa(a,b){var c=!0;return g(function(){return c&&(ea(a+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),c=!1),b.apply(this,arguments)},b)}function ga(a,b){Jd[a]||(ea(b),Jd[a]=!0)}function ha(a){var b,c,d,e,f,g,h=a._i,i=Kd.exec(h)||Ld.exec(h);if(i){for(j(a).iso=!0,b=0,c=Nd.length;c>b;b++)if(Nd[b][1].exec(i[1])){e=Nd[b][0],d=Nd[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=Od.length;c>b;b++)if(Od[b][1].exec(i[3])){f=(i[2]||" ")+Od[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Md.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),wa(a)}else a._isValid=!1}function ia(b){var c=Pd.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(ha(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function ja(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ka(a){var b=new Date(Date.UTC.apply(null,arguments));return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function la(a){return ma(a)?366:365}function ma(a){return a%4===0&&a%100!==0||a%400===0}function na(){return ma(this.year())}function oa(a,b,c){var d=7+b-c,e=(7+ka(a,0,d).getUTCDay()-b)%7;return-e+d-1}function pa(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=oa(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=la(f)+j):j>la(a)?(f=a+1,g=j-la(a)):(f=a,g=j),{year:f,dayOfYear:g}}function qa(a,b,c){var d,e,f=oa(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+ra(e,b,c)):g>ra(a.year(),b,c)?(d=g-ra(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function ra(a,b,c){var d=oa(a,b,c),e=oa(a+1,b,c);return(la(a)-d+e)/7}function sa(a,b,c){return null!=a?a:null!=b?b:c}function ta(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function ua(a){var b,c,d,e,f=[];if(!a._d){for(d=ta(a),a._w&&null==a._a[xd]&&null==a._a[wd]&&va(a),a._dayOfYear&&(e=sa(a._a[vd],d[vd]),a._dayOfYear>la(e)&&(j(a)._overflowDayOfYear=!0),c=ka(e,0,a._dayOfYear),a._a[wd]=c.getUTCMonth(),a._a[xd]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[yd]&&0===a._a[zd]&&0===a._a[Ad]&&0===a._a[Bd]&&(a._nextDay=!0,a._a[yd]=0),a._d=(a._useUTC?ka:ja).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[yd]=24)}}function va(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=sa(b.GG,a._a[vd],qa(Ea(),1,4).year),d=sa(b.W,1),e=sa(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=sa(b.gg,a._a[vd],qa(Ea(),f,g).year),d=sa(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>ra(c,f,g)?j(a)._overflowWeeks=!0:null!=i?j(a)._overflowWeekday=!0:(h=pa(c,d,e,f,g),a._a[vd]=h.year,a._dayOfYear=h.dayOfYear)}function wa(b){if(b._f===a.ISO_8601)return void ha(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=N(b._f,b._locale).match($c)||[],c=0;c0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),bd[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),U(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[yd]<=12&&b._a[yd]>0&&(j(b).bigHour=void 0),b._a[yd]=xa(b._locale,b._a[yd],b._meridiem),ua(b),da(b)}function xa(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function ya(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function za(a){if(!a._d){var b=C(a._i);a._a=e([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),ua(a)}}function Aa(a){var b=new o(da(Ba(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Ba(a){var b=a._i,e=a._f;return a._locale=a._locale||z(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),p(b)?new o(da(b)):(c(e)?ya(a):e?wa(a):d(b)?a._d=b:Ca(a),k(a)||(a._d=null),a))}function Ca(b){var f=b._i;void 0===f?b._d=new Date(a.now()):d(f)?b._d=new Date(+f):"string"==typeof f?ia(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),ua(b)):"object"==typeof f?za(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Da(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,Aa(f)}function Ea(a,b,c,d){return Da(a,b,c,d,!1)}function Fa(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Ea();for(d=b[0],e=1;ea&&(a=-a,c="-"),c+I(~~(a/60),2)+b+I(~~a%60,2)})}function La(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(Ud)||["-",0,0],f=+(60*e[1])+r(e[2]);return"+"===e[0]?f:-f}function Ma(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(p(b)||d(b)?+b:+Ea(b))-+e,e._d.setTime(+e._d+f),a.updateOffset(e,!1),e):Ea(b).local()}function Na(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Oa(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=La(qd,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Na(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?cb(this,Za(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Na(this):null!=b?this:NaN}function Pa(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Qa(a){return this.utcOffset(0,a)}function Ra(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Na(this),"m")),this}function Sa(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(La(pd,this._i)),this}function Ta(a){return this.isValid()?(a=a?Ea(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function Ua(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Va(){if(!m(this._isDSTShifted))return this._isDSTShifted;var a={};if(n(a,this),a=Ba(a),a._a){var b=a._isUTC?h(a._a):Ea(a._a);this._isDSTShifted=this.isValid()&&s(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Wa(){return this.isValid()?!this._isUTC:!1}function Xa(){return this.isValid()?this._isUTC:!1}function Ya(){return this.isValid()?this._isUTC&&0===this._offset:!1}function Za(a,b){var c,d,e,g=a,h=null;return Ja(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=Vd.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:r(h[xd])*c,h:r(h[yd])*c,m:r(h[zd])*c,s:r(h[Ad])*c,ms:r(h[Bd])*c}):(h=Wd.exec(a))?(c="-"===h[1]?-1:1,g={y:$a(h[2],c),M:$a(h[3],c),d:$a(h[4],c),h:$a(h[5],c),m:$a(h[6],c),s:$a(h[7],c),w:$a(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=ab(Ea(g.from),Ea(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Ia(g),Ja(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function $a(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function _a(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function ab(a,b){var c;return a.isValid()&&b.isValid()?(b=Ma(b,a),a.isBefore(b)?c=_a(a,b):(c=_a(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function bb(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(ga(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Za(c,d),cb(this,e,a),this}}function cb(b,c,d,e){var f=c._milliseconds,g=c._days,h=c._months;b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(+b._d+f*d),g&&G(b,"Date",F(b,"Date")+g*d),h&&Z(b,F(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function db(a,b){var c=a||Ea(),d=Ma(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse",g=b&&(D(b[f])?b[f]():b[f]);return this.format(g||this.localeData().calendar(f,this,Ea(c)))}function eb(){return new o(this)}function fb(a,b){var c=p(a)?a:Ea(a);return this.isValid()&&c.isValid()?(b=B(m(b)?"millisecond":b),"millisecond"===b?+this>+c:+c<+this.clone().startOf(b)):!1}function gb(a,b){var c=p(a)?a:Ea(a);return this.isValid()&&c.isValid()?(b=B(m(b)?"millisecond":b),"millisecond"===b?+c>+this:+this.clone().endOf(b)<+c):!1}function hb(a,b,c){return this.isAfter(a,c)&&this.isBefore(b,c)}function ib(a,b){var c,d=p(a)?a:Ea(a);return this.isValid()&&d.isValid()?(b=B(b||"millisecond"),"millisecond"===b?+this===+d:(c=+d,+this.clone().startOf(b)<=c&&c<=+this.clone().endOf(b))):!1}function jb(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function kb(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function lb(a,b,c){var d,e,f,g;return this.isValid()?(d=Ma(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=B(b),"year"===b||"month"===b||"quarter"===b?(g=mb(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:q(g)):NaN):NaN}function mb(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function nb(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ob(){var a=this.clone().utc();return 0f&&(b=f),Ob.call(this,a,b,c,d,e))}function Ob(a,b,c,d,e){var f=pa(a,b,c,d,e),g=ka(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Pb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Qb(a){return qa(a,this._week.dow,this._week.doy).week}function Rb(){return this._week.dow}function Sb(){return this._week.doy}function Tb(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ub(a){var b=qa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function Vb(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Wb(a,b){return c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]}function Xb(a){return this._weekdaysShort[a.day()]}function Yb(a){return this._weekdaysMin[a.day()]}function Zb(a,b,c){var d,e,f;for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){if(e=Ea([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function $b(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Vb(a,this.localeData()),this.add(a-b,"d")):b}function _b(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function ac(a){return this.isValid()?null==a?this.day()||7:this.day(this.day()%7?a:a-7):null!=a?this:NaN}function bc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function cc(){return this.hours()%12||12}function dc(a,b){J(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function ec(a,b){return b._meridiemParse}function fc(a){return"p"===(a+"").toLowerCase().charAt(0)}function gc(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function hc(a,b){b[Bd]=r(1e3*("0."+a))}function ic(){return this._isUTC?"UTC":""}function jc(){return this._isUTC?"Coordinated Universal Time":""}function kc(a){return Ea(1e3*a)}function lc(){return Ea.apply(null,arguments).parseZone()}function mc(a,b,c){var d=this._calendar[a];return D(d)?d.call(b,c):d}function nc(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function oc(){return this._invalidDate}function pc(a){return this._ordinal.replace("%d",a)}function qc(a){return a}function rc(a,b,c,d){var e=this._relativeTime[c];return D(e)?e(a,b,c,d):e.replace(/%d/i,a)}function sc(a,b){var c=this._relativeTime[a>0?"future":"past"];return D(c)?c(b):c.replace(/%s/i,b)}function tc(a){var b,c;for(c in a)b=a[c],D(b)?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function uc(a,b,c,d){var e=z(),f=h().set(d,b);return e[c](f,a)}function vc(a,b,c,d,e){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return uc(a,b,c,e);var f,g=[];for(f=0;d>f;f++)g[f]=uc(a,f,c,e);return g}function wc(a,b){return vc(a,b,"months",12,"month")}function xc(a,b){return vc(a,b,"monthsShort",12,"month")}function yc(a,b){return vc(a,b,"weekdays",7,"day")}function zc(a,b){return vc(a,b,"weekdaysShort",7,"day")}function Ac(a,b){return vc(a,b,"weekdaysMin",7,"day")}function Bc(){var a=this._data;return this._milliseconds=se(this._milliseconds),this._days=se(this._days),this._months=se(this._months),a.milliseconds=se(a.milliseconds),a.seconds=se(a.seconds),a.minutes=se(a.minutes),a.hours=se(a.hours),a.months=se(a.months),a.years=se(a.years),this}function Cc(a,b,c,d){var e=Za(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function Dc(a,b){return Cc(this,a,b,1)}function Ec(a,b){return Cc(this,a,b,-1)}function Fc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Gc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Fc(Ic(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=q(f/1e3),i.seconds=a%60,b=q(a/60),i.minutes=b%60,c=q(b/60),i.hours=c%24,g+=q(c/24),e=q(Hc(g)),h+=e,g-=Fc(Ic(e)),d=q(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function Hc(a){return 4800*a/146097}function Ic(a){return 146097*a/4800}function Jc(a){var b,c,d=this._milliseconds;if(a=B(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+Hc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(Ic(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function Kc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*r(this._months/12)}function Lc(a){return function(){return this.as(a)}}function Mc(a){return a=B(a),this[a+"s"]()}function Nc(a){return function(){return this._data[a]}}function Oc(){return q(this.days()/7)}function Pc(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function Qc(a,b,c){var d=Za(a).abs(),e=Ie(d.as("s")),f=Ie(d.as("m")),g=Ie(d.as("h")),h=Ie(d.as("d")),i=Ie(d.as("M")),j=Ie(d.as("y")),k=e=f&&["m"]||f=g&&["h"]||g=h&&["d"]||h=i&&["M"]||i=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,Pc.apply(null,k)}function Rc(a,b){return void 0===Je[a]?!1:void 0===b?Je[a]:(Je[a]=b,!0)}function Sc(a){var b=this.localeData(),c=Qc(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function Tc(){var a,b,c,d=Ke(this._milliseconds)/1e3,e=Ke(this._days),f=Ke(this._months);a=q(d/60),b=q(a/60),d%=60,a%=60,c=q(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var Uc,Vc,Wc=a.momentProperties=[],Xc=!1,Yc={},Zc={},$c=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,_c=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,ad={},bd={},cd=/\d/,dd=/\d\d/,ed=/\d{3}/,fd=/\d{4}/,gd=/[+-]?\d{6}/,hd=/\d\d?/,id=/\d\d\d\d?/,jd=/\d\d\d\d\d\d?/,kd=/\d{1,3}/,ld=/\d{1,4}/,md=/[+-]?\d{1,6}/,nd=/\d+/,od=/[+-]?\d+/,pd=/Z|[+-]\d\d:?\d\d/gi,qd=/Z|[+-]\d\d(?::?\d\d)?/gi,rd=/[+-]?\d+(\.\d{1,3})?/,sd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,td={},ud={},vd=0,wd=1,xd=2,yd=3,zd=4,Ad=5,Bd=6,Cd=7,Dd=8;J("M",["MM",2],"Mo",function(){return this.month()+1}),J("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),J("MMMM",0,0,function(a){return this.localeData().months(this,a)}),A("month","M"),O("M",hd),O("MM",hd,dd),O("MMM",function(a,b){return b.monthsShortRegex(a)}),O("MMMM",function(a,b){return b.monthsRegex(a)}),S(["M","MM"],function(a,b){b[wd]=r(a)-1}),S(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[wd]=e:j(c).invalidMonth=a});var Ed=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/,Fd="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Gd="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),Hd=sd,Id=sd,Jd={};a.suppressDeprecationWarnings=!1;var Kd=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,Ld=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,Md=/Z|[+-]\d\d(?::?\d\d)?/,Nd=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Od=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Pd=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=fa("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),J("Y",0,0,function(){var a=this.year();return 9999>=a?""+a:"+"+a}),J(0,["YY",2],0,function(){return this.year()%100}),J(0,["YYYY",4],0,"year"),J(0,["YYYYY",5],0,"year"),J(0,["YYYYYY",6,!0],0,"year"),A("year","y"),O("Y",od),O("YY",hd,dd),O("YYYY",ld,fd),O("YYYYY",md,gd),O("YYYYYY",md,gd),S(["YYYYY","YYYYYY"],vd),S("YYYY",function(b,c){c[vd]=2===b.length?a.parseTwoDigitYear(b):r(b)}),S("YY",function(b,c){c[vd]=a.parseTwoDigitYear(b)}),S("Y",function(a,b){b[vd]=parseInt(a,10)}),a.parseTwoDigitYear=function(a){return r(a)+(r(a)>68?1900:2e3)};var Qd=E("FullYear",!1);a.ISO_8601=function(){};var Rd=fa("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=Ea.apply(null,arguments);return this.isValid()&&a.isValid()?this>a?this:a:l()}),Sd=fa("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=Ea.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:l()}),Td=function(){return Date.now?Date.now():+new Date};Ka("Z",":"),Ka("ZZ",""),O("Z",qd),O("ZZ",qd),S(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=La(qd,a)});var Ud=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var Vd=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,Wd=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; 7 | Za.fn=Ia.prototype;var Xd=bb(1,"add"),Yd=bb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Zd=fa("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});J(0,["gg",2],0,function(){return this.weekYear()%100}),J(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Ib("gggg","weekYear"),Ib("ggggg","weekYear"),Ib("GGGG","isoWeekYear"),Ib("GGGGG","isoWeekYear"),A("weekYear","gg"),A("isoWeekYear","GG"),O("G",od),O("g",od),O("GG",hd,dd),O("gg",hd,dd),O("GGGG",ld,fd),O("gggg",ld,fd),O("GGGGG",md,gd),O("ggggg",md,gd),T(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=r(a)}),T(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),J("Q",0,"Qo","quarter"),A("quarter","Q"),O("Q",cd),S("Q",function(a,b){b[wd]=3*(r(a)-1)}),J("w",["ww",2],"wo","week"),J("W",["WW",2],"Wo","isoWeek"),A("week","w"),A("isoWeek","W"),O("w",hd),O("ww",hd,dd),O("W",hd),O("WW",hd,dd),T(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=r(a)});var $d={dow:0,doy:6};J("D",["DD",2],"Do","date"),A("date","D"),O("D",hd),O("DD",hd,dd),O("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),S(["D","DD"],xd),S("Do",function(a,b){b[xd]=r(a.match(hd)[0],10)});var _d=E("Date",!0);J("d",0,"do","day"),J("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),J("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),J("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),J("e",0,0,"weekday"),J("E",0,0,"isoWeekday"),A("day","d"),A("weekday","e"),A("isoWeekday","E"),O("d",hd),O("e",hd),O("E",hd),O("dd",sd),O("ddd",sd),O("dddd",sd),T(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);null!=e?b.d=e:j(c).invalidWeekday=a}),T(["d","e","E"],function(a,b,c,d){b[d]=r(a)});var ae="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),be="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),ce="Su_Mo_Tu_We_Th_Fr_Sa".split("_");J("DDD",["DDDD",3],"DDDo","dayOfYear"),A("dayOfYear","DDD"),O("DDD",kd),O("DDDD",ed),S(["DDD","DDDD"],function(a,b,c){c._dayOfYear=r(a)}),J("H",["HH",2],0,"hour"),J("h",["hh",2],0,cc),J("hmm",0,0,function(){return""+cc.apply(this)+I(this.minutes(),2)}),J("hmmss",0,0,function(){return""+cc.apply(this)+I(this.minutes(),2)+I(this.seconds(),2)}),J("Hmm",0,0,function(){return""+this.hours()+I(this.minutes(),2)}),J("Hmmss",0,0,function(){return""+this.hours()+I(this.minutes(),2)+I(this.seconds(),2)}),dc("a",!0),dc("A",!1),A("hour","h"),O("a",ec),O("A",ec),O("H",hd),O("h",hd),O("HH",hd,dd),O("hh",hd,dd),O("hmm",id),O("hmmss",jd),O("Hmm",id),O("Hmmss",jd),S(["H","HH"],yd),S(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),S(["h","hh"],function(a,b,c){b[yd]=r(a),j(c).bigHour=!0}),S("hmm",function(a,b,c){var d=a.length-2;b[yd]=r(a.substr(0,d)),b[zd]=r(a.substr(d)),j(c).bigHour=!0}),S("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[yd]=r(a.substr(0,d)),b[zd]=r(a.substr(d,2)),b[Ad]=r(a.substr(e)),j(c).bigHour=!0}),S("Hmm",function(a,b,c){var d=a.length-2;b[yd]=r(a.substr(0,d)),b[zd]=r(a.substr(d))}),S("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[yd]=r(a.substr(0,d)),b[zd]=r(a.substr(d,2)),b[Ad]=r(a.substr(e))});var de=/[ap]\.?m?\.?/i,ee=E("Hours",!0);J("m",["mm",2],0,"minute"),A("minute","m"),O("m",hd),O("mm",hd,dd),S(["m","mm"],zd);var fe=E("Minutes",!1);J("s",["ss",2],0,"second"),A("second","s"),O("s",hd),O("ss",hd,dd),S(["s","ss"],Ad);var ge=E("Seconds",!1);J("S",0,0,function(){return~~(this.millisecond()/100)}),J(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),J(0,["SSS",3],0,"millisecond"),J(0,["SSSS",4],0,function(){return 10*this.millisecond()}),J(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),J(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),J(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),J(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),J(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),A("millisecond","ms"),O("S",kd,cd),O("SS",kd,dd),O("SSS",kd,ed);var he;for(he="SSSS";he.length<=9;he+="S")O(he,nd);for(he="S";he.length<=9;he+="S")S(he,hc);var ie=E("Milliseconds",!1);J("z",0,0,"zoneAbbr"),J("zz",0,0,"zoneName");var je=o.prototype;je.add=Xd,je.calendar=db,je.clone=eb,je.diff=lb,je.endOf=xb,je.format=pb,je.from=qb,je.fromNow=rb,je.to=sb,je.toNow=tb,je.get=H,je.invalidAt=Gb,je.isAfter=fb,je.isBefore=gb,je.isBetween=hb,je.isSame=ib,je.isSameOrAfter=jb,je.isSameOrBefore=kb,je.isValid=Eb,je.lang=Zd,je.locale=ub,je.localeData=vb,je.max=Sd,je.min=Rd,je.parsingFlags=Fb,je.set=H,je.startOf=wb,je.subtract=Yd,je.toArray=Bb,je.toObject=Cb,je.toDate=Ab,je.toISOString=ob,je.toJSON=Db,je.toString=nb,je.unix=zb,je.valueOf=yb,je.creationData=Hb,je.year=Qd,je.isLeapYear=na,je.weekYear=Jb,je.isoWeekYear=Kb,je.quarter=je.quarters=Pb,je.month=$,je.daysInMonth=_,je.week=je.weeks=Tb,je.isoWeek=je.isoWeeks=Ub,je.weeksInYear=Mb,je.isoWeeksInYear=Lb,je.date=_d,je.day=je.days=$b,je.weekday=_b,je.isoWeekday=ac,je.dayOfYear=bc,je.hour=je.hours=ee,je.minute=je.minutes=fe,je.second=je.seconds=ge,je.millisecond=je.milliseconds=ie,je.utcOffset=Oa,je.utc=Qa,je.local=Ra,je.parseZone=Sa,je.hasAlignedHourOffset=Ta,je.isDST=Ua,je.isDSTShifted=Va,je.isLocal=Wa,je.isUtcOffset=Xa,je.isUtc=Ya,je.isUTC=Ya,je.zoneAbbr=ic,je.zoneName=jc,je.dates=fa("dates accessor is deprecated. Use date instead.",_d),je.months=fa("months accessor is deprecated. Use month instead",$),je.years=fa("years accessor is deprecated. Use year instead",Qd),je.zone=fa("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Pa);var ke=je,le={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},me={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},ne="Invalid date",oe="%d",pe=/\d{1,2}/,qe={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},re=t.prototype;re._calendar=le,re.calendar=mc,re._longDateFormat=me,re.longDateFormat=nc,re._invalidDate=ne,re.invalidDate=oc,re._ordinal=oe,re.ordinal=pc,re._ordinalParse=pe,re.preparse=qc,re.postformat=qc,re._relativeTime=qe,re.relativeTime=rc,re.pastFuture=sc,re.set=tc,re.months=W,re._months=Fd,re.monthsShort=X,re._monthsShort=Gd,re.monthsParse=Y,re._monthsRegex=Id,re.monthsRegex=ba,re._monthsShortRegex=Hd,re.monthsShortRegex=aa,re.week=Qb,re._week=$d,re.firstDayOfYear=Sb,re.firstDayOfWeek=Rb,re.weekdays=Wb,re._weekdays=ae,re.weekdaysMin=Yb,re._weekdaysMin=ce,re.weekdaysShort=Xb,re._weekdaysShort=be,re.weekdaysParse=Zb,re.isPM=fc,re._meridiemParse=de,re.meridiem=gc,x("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===r(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=fa("moment.lang is deprecated. Use moment.locale instead.",x),a.langData=fa("moment.langData is deprecated. Use moment.localeData instead.",z);var se=Math.abs,te=Lc("ms"),ue=Lc("s"),ve=Lc("m"),we=Lc("h"),xe=Lc("d"),ye=Lc("w"),ze=Lc("M"),Ae=Lc("y"),Be=Nc("milliseconds"),Ce=Nc("seconds"),De=Nc("minutes"),Ee=Nc("hours"),Fe=Nc("days"),Ge=Nc("months"),He=Nc("years"),Ie=Math.round,Je={s:45,m:45,h:22,d:26,M:11},Ke=Math.abs,Le=Ia.prototype;Le.abs=Bc,Le.add=Dc,Le.subtract=Ec,Le.as=Jc,Le.asMilliseconds=te,Le.asSeconds=ue,Le.asMinutes=ve,Le.asHours=we,Le.asDays=xe,Le.asWeeks=ye,Le.asMonths=ze,Le.asYears=Ae,Le.valueOf=Kc,Le._bubble=Gc,Le.get=Mc,Le.milliseconds=Be,Le.seconds=Ce,Le.minutes=De,Le.hours=Ee,Le.days=Fe,Le.weeks=Oc,Le.months=Ge,Le.years=He,Le.humanize=Sc,Le.toISOString=Tc,Le.toString=Tc,Le.toJSON=Tc,Le.locale=ub,Le.localeData=vb,Le.toIsoString=fa("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Tc),Le.lang=Zd,J("X",0,0,"unix"),J("x",0,0,"valueOf"),O("x",od),O("X",rd),S("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),S("x",function(a,b,c){c._d=new Date(r(a))}),a.version="2.11.2",b(Ea),a.fn=ke,a.min=Ga,a.max=Ha,a.now=Td,a.utc=h,a.unix=kc,a.months=wc,a.isDate=d,a.locale=x,a.invalid=l,a.duration=Za,a.isMoment=p,a.weekdays=yc,a.parseZone=lc,a.localeData=z,a.isDuration=Ja,a.monthsShort=xc,a.weekdaysMin=Ac,a.defineLocale=y,a.weekdaysShort=zc,a.normalizeUnits=B,a.relativeTimeThreshold=Rc,a.prototype=ke;var Me=a;return Me}); --------------------------------------------------------------------------------