├── Procfile ├── views ├── _modules │ └── _Modules.jade ├── error.jade ├── shared │ └── _google-analytics.jade ├── layouts │ └── _layout.jade └── index.jade ├── src ├── css │ ├── templates │ │ ├── _results.css │ │ ├── _error.css │ │ └── _home.css │ ├── _extends.css │ ├── application.css │ ├── _animations.css │ ├── components │ │ ├── _loaders.css │ │ ├── _buttons.css │ │ ├── _header.css │ │ └── _footer.css │ ├── _settings.css │ └── _structure.css ├── images │ ├── wash-blue.jpg │ ├── wash-green.jpg │ └── svg │ │ ├── icon-toggle.svg │ │ ├── wind-compass.svg │ │ ├── lb.svg │ │ ├── sb.svg │ │ ├── mokes.svg │ │ ├── bb.svg │ │ ├── icon-tide.svg │ │ ├── logo-surfstatus.svg │ │ ├── sup.svg │ │ ├── hawaiian-islands.svg │ │ ├── logo-surfornah.svg │ │ └── oahu.svg └── js │ ├── helpers.js │ ├── application.js │ └── vendor │ └── wave-canvas.js ├── start.js ├── .gitignore ├── lib └── closer.js ├── bower.json ├── data └── surfbreaks.json ├── routes └── index.js ├── package.json ├── bin └── www ├── app.js ├── models ├── surf-data.js ├── weather-data.js └── tide-data.js └── gulpfile.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node start.js -------------------------------------------------------------------------------- /views/_modules/_Modules.jade: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/templates/_results.css: -------------------------------------------------------------------------------- 1 | /**/ -------------------------------------------------------------------------------- /src/images/wash-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorkmho/surfstatus/HEAD/src/images/wash-blue.jpg -------------------------------------------------------------------------------- /src/images/wash-green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorkmho/surfstatus/HEAD/src/images/wash-green.jpg -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | // Ref: https://github.com/remy/nodemon/issues/330#issuecomment-44370399 2 | require('./bin/www') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | data/secrets.json 4 | tmp 5 | public/css/* 6 | public/js/* 7 | public/images/* 8 | .env -------------------------------------------------------------------------------- /src/css/templates/_error.css: -------------------------------------------------------------------------------- 1 | [data-template="error"] { 2 | color: #fff; 3 | .container { 4 | lost-center: 920px; 5 | margin-top: 4rem; 6 | } 7 | } -------------------------------------------------------------------------------- /src/css/_extends.css: -------------------------------------------------------------------------------- 1 | @define-placeholder container--center { 2 | position: relative; 3 | lost-center: $row-width; 4 | top: 50%; 5 | transform: translate3d(0, -50%, 0); 6 | } -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends ./layouts/_layout 2 | 3 | block vars 4 | - var bodyTemplate = "error" 5 | 6 | block content 7 | .container 8 | h1= message 9 | h2= error.status 10 | pre #{error.stack} 11 | -------------------------------------------------------------------------------- /lib/closer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Closer plugin example 3 | * https://gist.github.com/timthez/d1b29ea02cce7a2a59ff 4 | */ 5 | (function ($window, $document, bs) { 6 | var socket = bs.socket; 7 | socket.on("disconnect", function (client) { 8 | window.close(); 9 | }); 10 | })(window, document, ___browserSync___); -------------------------------------------------------------------------------- /src/images/svg/icon-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /views/shared/_google-analytics.jade: -------------------------------------------------------------------------------- 1 | script. 2 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 3 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 4 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 5 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 6 | 7 | ga('create', 'UA-41954759-9', 'auto'); 8 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /src/css/application.css: -------------------------------------------------------------------------------- 1 | @import '../../bower_components/normalize-css/normalize.css'; 2 | 3 | @import './_settings'; 4 | @import './_extends'; 5 | @import './_structure'; 6 | @import './_animations'; 7 | 8 | @import './components/_header'; 9 | @import './components/_footer'; 10 | @import './components/_buttons'; 11 | /* @import './components/_loaders'; */ 12 | 13 | @import './templates/_home'; 14 | @import './templates/_error'; 15 | /* @import './templates/_results'; */ -------------------------------------------------------------------------------- /src/images/svg/wind-compass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/_animations.css: -------------------------------------------------------------------------------- 1 | @keyframes sunset { 2 | 0% { 3 | opacity: 0; 4 | transform: translateY(0rem); 5 | } 6 | 50% { 7 | opacity: 1; 8 | } 9 | 100% { 10 | transform: translateY(2rem); 11 | opacity: 0; 12 | } 13 | } 14 | @keyframes sunrise { 15 | 0% { 16 | opacity: 0; 17 | transform: translateY(0rem); 18 | } 19 | 50% { 20 | opacity: 1; 21 | } 22 | 100% { 23 | transform: translateY(-3rem); 24 | opacity: 0; 25 | } 26 | } -------------------------------------------------------------------------------- /src/images/svg/lb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surfornah", 3 | "description": "A web app to help you decide where to surf.", 4 | "main": "", 5 | "authors": [ 6 | "Taylor Ho " 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/taylorkmho/surfornah", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "test", 18 | "tests" 19 | ], 20 | "dependencies": { 21 | "jaaulde-cookies": "~3.0.6", 22 | "normalize-css": "normalize.css#~3.0.3", 23 | "d3": "~3.5.10", 24 | "fastclick": "~1.0.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/css/components/_loaders.css: -------------------------------------------------------------------------------- 1 | /* .lazyloaded { 2 | $loader-size: 30px; 3 | $loader-thickness: 6px; 4 | 5 | &:before { 6 | content: ''; 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | transform: translate(-50%,-50%); 11 | display: block; 12 | width: $loader-size; 13 | height: $loader-size; 14 | border-radius: $loader-size; 15 | } 16 | &:before { 17 | border: $loader-thickness solid rgba(255,255,255,0.125); 18 | border-top-color: rgba($c-yellow,.5); 19 | animation: spin-loader 1s linear infinite; 20 | } 21 | &.lazyloaded--loaded { 22 | &:before { 23 | width: 0; 24 | height: 0; 25 | border: 0; 26 | } 27 | } 28 | } */ -------------------------------------------------------------------------------- /src/css/components/_buttons.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: inline-block; 3 | padding: $column-gutter calc($column-gutter*4); 4 | text-decoration: none; 5 | text-transform: uppercase; 6 | letter-spacing: .1rem; 7 | border-radius: 4px; 8 | 9 | box-shadow: 2px 2px 5px -2px color(#000 a(80%)); 10 | background-size: 100% 100%; 11 | background-position: center center; 12 | 13 | transition: all $easeOutExpo 250ms; 14 | 15 | &:active { 16 | transform: translate(2px, 2px); 17 | box-shadow: 0px 0px 5px -2px color(#000 a(80%)); 18 | } 19 | 20 | &--green { 21 | color: #fff; 22 | background-image: url('../../images/wash-green.jpg'); 23 | } 24 | &--blue { 25 | color: #fff; 26 | background-image: url('../../images/wash-blue.jpg'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/css/_settings.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Montserrat); 2 | @import url(https://fonts.googleapis.com/css?family=Rock+Salt); 3 | 4 | $c-slate: #1A1D2D; 5 | $c-blue: #014E6E; 6 | $c-red: #EB4329; 7 | 8 | $c-board-fill: #444; 9 | $c-board-detail: #C9CCCF; 10 | 11 | $c-body-bg: #171717; 12 | $c-body-font: #fff; 13 | $c-body-border: #fff; 14 | $c-grid-border: #222; 15 | 16 | $column-gutter: 1rem; 17 | $row-width: 640px; 18 | 19 | $grid-border: calc($column-gutter/2) solid $c-grid-border; 20 | 21 | $padding-section: calc($column-gutter*10); 22 | 23 | $body-font-family: 'Montserrat', 'Helvetica Neue', Helvetica, sans-serif; 24 | $title-font-family: 'Rock Salt', 'Helvetica Neue', Helvetica, sans-serif; 25 | 26 | $easeOutExpo: cubic-bezier(0.19, 1, 0.22, 1); -------------------------------------------------------------------------------- /views/layouts/_layout.jade: -------------------------------------------------------------------------------- 1 | include ../_modules/_Modules 2 | 3 | block vars 4 | - var bodyTemplate = "default" 5 | 6 | doctype html 7 | html 8 | head 9 | meta(charset="utf-8") 10 | meta(name="viewport" content="width=device-width, initial-scale=1.0") 11 | title #{title} 12 | link(rel='stylesheet' href='../css/app.css') 13 | block addToHead 14 | body(data-template="#{bodyTemplate}") 15 | include ../shared/_google-analytics.jade 16 | block content 17 | 18 | footer.footer 19 | h1.footer__message oʻahu’s new favorite surf report 20 | h1.footer__built-by dev&design by tho 21 | 22 | block footerScripts 23 | //- script(src="../js/vendor/jaaulde-cookies.js") 24 | script(src="../js/vendor/fastclick.js") 25 | script(src="../js/app.js") -------------------------------------------------------------------------------- /src/images/svg/sb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/components/_header.css: -------------------------------------------------------------------------------- 1 | /* .waves { 2 | &, &__canvas { 3 | position: absolute; 4 | height: 100%; 5 | width: 100%; 6 | z-index: 100; 7 | } 8 | } */ 9 | 10 | .branding { 11 | position: relative; 12 | padding: 0 calc($column-gutter*2); 13 | lost-utility: clearfix; 14 | 15 | &__logo-container { 16 | width: 100%; 17 | } 18 | .logo-surfstatus { 19 | display: block; 20 | width: 100%; 21 | height: auto; 22 | margin: calc(2*$column-gutter) auto; 23 | path { fill: #3a3a3a; } 24 | 25 | } 26 | } 27 | 28 | @media screen and (min-width: 640px) { 29 | .branding { 30 | padding-top: calc($column-gutter*2); 31 | padding-left: calc($column-gutter*4); 32 | padding-right: calc($column-gutter*4); 33 | &__logo-container { 34 | border-bottom: $c-grid-border calc($column-gutter) solid; 35 | } 36 | .logo-surfstatus { 37 | padding-left: calc(2*$column-gutter); 38 | padding-right: calc(2*$column-gutter); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/images/svg/mokes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/css/_structure.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-size: 90%; 3 | height: 100%; 4 | width: 100%; 5 | } 6 | 7 | @media screen and (min-width: 1200px) { 8 | html, body { 9 | font-size: 100%; 10 | } 11 | } 12 | 13 | body { 14 | font-family: $body-font-family; 15 | background: $c-body-bg; 16 | } 17 | 18 | .branding { 19 | } 20 | 21 | @media screen and (min-width: 640px) { 22 | body { 23 | &:before, &:after { 24 | content: ""; 25 | position: fixed; 26 | top: 0; 27 | height: 100%; 28 | width: calc(2*$column-gutter); 29 | z-index: 300; 30 | background-color: $c-body-border; 31 | } 32 | &:before { 33 | left: 0; 34 | } 35 | &:after { 36 | right: 0; 37 | } 38 | } 39 | .branding { 40 | &:before { 41 | content: ""; 42 | position: fixed; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | z-index: 300; 47 | height: $column-gutter; 48 | background-color: $c-body-border; 49 | height: calc(2*$column-gutter); 50 | } 51 | } 52 | } 53 | 54 | * { 55 | box-sizing: border-box; 56 | } 57 | 58 | svg, img { 59 | max-width: 100%; 60 | height: auto; 61 | display: inline-block; 62 | vertical-align: middle; 63 | } 64 | 65 | img { 66 | -ms-interpolation-mode: bicubic; 67 | } 68 | 69 | ::selection { 70 | background-color: color($c-red tint(50%)); 71 | } -------------------------------------------------------------------------------- /data/surfbreaks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title" : "Ala Moana", 4 | "shore" : "S", 5 | "magicSW": 661 6 | }, 7 | { 8 | "title" : "Waikiki", 9 | "shore" : "S", 10 | "magicSW": 662 11 | }, 12 | { 13 | "title" : "Sandy Beach", 14 | "shore" : "S", 15 | "magicSW": 1099 16 | }, 17 | { 18 | "title" : "Kailua Outer Reefs", 19 | "shore" : "E", 20 | "magicSW": 671 21 | }, 22 | { 23 | "title" : "Makaha Point", 24 | "shore" : "W", 25 | "magicSW": 983 26 | }, 27 | { 28 | "title" : "Makapuu Point", 29 | "shore" : "E", 30 | "magicSW": 984 31 | }, 32 | { 33 | "title" : "Barbers Point", 34 | "shore" : "S", 35 | "magicSW": 3082 36 | }, 37 | { 38 | "title" : "Maili Point", 39 | "shore" : "W", 40 | "magicSW": 3084 41 | }, 42 | { 43 | "title" : "Waimea Bay", 44 | "shore" : "N", 45 | "magicSW": 549 46 | }, 47 | { 48 | "title" : "Pipeline", 49 | "shore" : "N", 50 | "magicSW": 616 51 | }, 52 | { 53 | "title" : "Sunset", 54 | "shore" : "N", 55 | "magicSW": 657 56 | }, 57 | { 58 | "title" : "Rocky Point", 59 | "shore" : "N", 60 | "magicSW": 658 61 | }, 62 | { 63 | "title" : "Haleʻiwa", 64 | "shore" : "N", 65 | "magicSW": 660 66 | }, 67 | { 68 | "title" : "Laniakea", 69 | "shore" : "N", 70 | "magicSW": 3672 71 | } 72 | ] -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var request = require('request'); 3 | var async = require('async'); 4 | var parseXML = require('xml2js').parseString; 5 | 6 | var router = express.Router(); 7 | 8 | var mongoose = require('mongoose'); 9 | var surfDataModel = require('../models/surf-data'); 10 | var weatherDataModel = require('../models/weather-data'); 11 | var tideDataModel = require('../models/tide-data'); 12 | 13 | var recentSurf = new Array(), 14 | recentWeather = new Array(), 15 | recentTide = new Array(); 16 | 17 | router.get('/', function(req, res, next) { 18 | 19 | async.parallel([ 20 | function(callback) { 21 | mongoose.model('SurfData').find().sort('-timestamp').limit(1).exec(function(err, surfData) { 22 | recentSurf = surfData[0]; 23 | callback(); 24 | }) 25 | }, 26 | function(callback) { 27 | mongoose.model('WeatherData').find().sort('-timestamp').limit(1).exec(function(err, weatherData) { 28 | recentWeather = weatherData[0]; 29 | callback(); 30 | }) 31 | }, 32 | function(callback) { 33 | mongoose.model('TideData').find().sort('-timestamp').limit(1).exec(function(err, tideData) { 34 | recentTide = tideData[0]; 35 | callback(); 36 | }) 37 | } 38 | ], 39 | function(err, results) { 40 | res.render('index', { title: 'surfstatus - your new favorite surf report', recentSurf: recentSurf, recentWeather: recentWeather, recentTide: recentTide}); 41 | } 42 | ); 43 | 44 | }); 45 | 46 | module.exports = router; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surfstatus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node start.js", 7 | "postinstall": "bower install && gulp build" 8 | }, 9 | "dependencies": { 10 | "async": "^1.5.0", 11 | "body-parser": "~1.13.2", 12 | "bower": "^1.7.7", 13 | "browser-sync": "^2.10.0", 14 | "coffee-script": "^1.10.0", 15 | "cookie-parser": "~1.3.5", 16 | "cron": "^1.1.0", 17 | "cssnext": "^1.8.4", 18 | "debug": "~2.2.0", 19 | "del": "^2.1.0", 20 | "dotenv": "^2.0.0", 21 | "express": "~4.13.1", 22 | "fs": "0.0.2", 23 | "gulp": "^3.9.0", 24 | "gulp-changed": "^1.3.0", 25 | "gulp-concat": "^2.6.0", 26 | "gulp-cssnano": "^2.0.0", 27 | "gulp-filter": "^3.0.1", 28 | "gulp-gzip": "^1.2.0", 29 | "gulp-imagemin": "^2.4.0", 30 | "gulp-include": "^2.1.0", 31 | "gulp-notify": "^2.2.0", 32 | "gulp-order": "^1.1.1", 33 | "gulp-postcss": "^6.0.1", 34 | "gulp-rsync": "0.0.5", 35 | "gulp-sourcemaps": "^1.6.0", 36 | "gulp-svg-sprite": "^1.2.15", 37 | "gulp-uglify": "^1.5.1", 38 | "gulp-util": "^3.0.7", 39 | "jade": "~1.11.0", 40 | "lost": "^6.6.2", 41 | "main-bower-files": "^2.9.0", 42 | "mongojs": "^2.0.0", 43 | "mongoose": "^4.2.9", 44 | "morgan": "~1.6.1", 45 | "postcss-center": "^1.0.0", 46 | "postcss-focus": "^1.0.0", 47 | "postcss-import": "^7.1.3", 48 | "postcss-nested": "^1.0.0", 49 | "postcss-pxtorem": "^3.1.0", 50 | "postcss-simple-extend": "^1.0.0", 51 | "postcss-simple-vars": "^1.1.0", 52 | "request": "^2.67.0", 53 | "serve-favicon": "~2.3.0", 54 | "xml2js": "^0.4.15" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/css/components/_footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | z-index: 200; 6 | 7 | height: calc($column-gutter*2); 8 | width: 100%; 9 | 10 | padding: 0 calc($column-gutter*2); 11 | background-color: $c-body-border; 12 | 13 | font-size: 0.75rem; 14 | line-height: 1; 15 | text-transform: uppercase; 16 | 17 | &__message, &__built-by { 18 | position: relative; 19 | 20 | display: inline-block; 21 | 22 | font-size: inherit; 23 | } 24 | 25 | &__message { 26 | margin: 0; 27 | padding: calc($column-gutter * .25) 0; 28 | span { 29 | font-family: $title-font-family; 30 | font-size: 200%; 31 | vertical-align: sub; 32 | text-transform: lowercase; 33 | color: $c-red; 34 | } 35 | } 36 | &__built-by { 37 | text-align: right; 38 | float: right; 39 | a { 40 | text-decoration: none; 41 | display: block; 42 | color: #444; 43 | span { 44 | color: $c-red; 45 | display: block; 46 | } 47 | } 48 | } 49 | } 50 | 51 | @media screen and (min-width: 820px) { 52 | .footer { 53 | text-align: center; 54 | &__message, &__built-by { 55 | left: calc(100%/3); 56 | lost-column: 1/3 3 0; 57 | } 58 | &__built-by { 59 | float: initial; 60 | a { 61 | padding: .25rem 0; 62 | &, span { transition: color $easeOutExpo 250ms; } 63 | &, &:active, &:focus { 64 | color: #fff; 65 | span { 66 | color: #ddd; 67 | display: inline-block; 68 | } 69 | } 70 | &:hover { 71 | color: #444; 72 | span { 73 | color: $c-red; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/images/svg/bb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/js/helpers.js: -------------------------------------------------------------------------------- 1 | function addClass(el, className) { 2 | if (el.classList) { 3 | el.classList.add(className); 4 | } else { 5 | el.className += ' ' + className; 6 | } 7 | } 8 | 9 | function removeClass(el, className) { 10 | if (el.classList) { 11 | el.classList.remove(className); 12 | } else { 13 | el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 14 | } 15 | } 16 | 17 | function hasClass(el, className) { 18 | if (el.classList) { 19 | if (el.classList.toString().indexOf(className) > -1) { 20 | return true; 21 | } else { 22 | return false; 23 | } 24 | } else { 25 | new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); 26 | } 27 | } 28 | 29 | // Wrap an HTMLElement around each element in an HTMLElement array. 30 | HTMLElement.prototype.wrap = function(elms) { 31 | // Convert `elms` to an array, if necessary. 32 | if (!elms.length) elms = [elms]; 33 | 34 | // Loops backwards to prevent having to clone the wrapper on the 35 | // first element (see `child` below). 36 | for (var i = elms.length - 1; i >= 0; i--) { 37 | var child = (i > 0) ? this.cloneNode(true) : this; 38 | var el = elms[i]; 39 | 40 | // Cache the current parent and sibling. 41 | var parent = el.parentNode; 42 | var sibling = el.nextSibling; 43 | 44 | // Wrap the element (is automatically removed from its current 45 | // parent). 46 | child.appendChild(el); 47 | 48 | // If the element had a sibling, insert the wrapper before 49 | // the sibling to maintain the HTML structure; otherwise, just 50 | // append it to the parent. 51 | if (sibling) { 52 | parent.insertBefore(child, sibling); 53 | } else { 54 | parent.appendChild(child); 55 | } 56 | } 57 | }; -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('surfornah:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /src/images/svg/icon-tide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tide 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var favicon = require('serve-favicon'); 6 | var logger = require('morgan'); 7 | var cookieParser = require('cookie-parser'); 8 | var bodyParser = require('body-parser'); 9 | 10 | var mongoose = require('mongoose'); 11 | mongoose.connect(process.env.MONGOLAB_URI); 12 | var conn = mongoose.connection; 13 | conn.on('error', console.error.bind(console, 'connection error:')); 14 | conn.once('open', function callback () { 15 | console.log('MONGOLAB/MONGOOSE - open success'); 16 | // models 17 | var weatherDataModel = require('./models/weather-data'); 18 | var tideDataModel = require('./models/tide-data'); 19 | var surfDataModel = require('./models/surf-data'); 20 | }) 21 | 22 | var index = require('./routes/index'); 23 | 24 | var app = express(); 25 | 26 | 27 | // view engine setup 28 | app.set('views', path.join(__dirname, 'views')); 29 | app.set('view engine', 'jade'); 30 | 31 | // uncomment after placing your favicon in /public 32 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 33 | app.use(logger('dev')); 34 | app.use(bodyParser.json()); 35 | app.use(bodyParser.urlencoded({ extended: false })); 36 | app.use(cookieParser()); 37 | app.use(express.static(path.join(__dirname, 'public'))); 38 | 39 | app.use('/', index); 40 | // app.use('/report-data', reportData); 41 | 42 | app.get('/surf-data', function(req,res) { 43 | mongoose.model('SurfData').find(function(err, surfData) { 44 | res.send(surfData); 45 | }) 46 | }); 47 | app.get('/weather-data', function(req,res) { 48 | mongoose.model('WeatherData').find(function(err, WeatherData) { 49 | res.send(WeatherData); 50 | }) 51 | }); 52 | app.get('/tide-data', function(req,res) { 53 | mongoose.model('TideData').find(function(err, TideData) { 54 | res.send(TideData); 55 | }) 56 | }); 57 | 58 | // catch 404 and forward to error handler 59 | app.use(function(req, res, next) { 60 | var err = new Error('Not Found'); 61 | err.status = 404; 62 | next(err); 63 | }); 64 | 65 | // error handlers 66 | 67 | // development error handler 68 | // will print stacktrace 69 | if (app.get('env') === 'development') { 70 | app.use(function(err, req, res, next) { 71 | res.status(err.status || 500); 72 | res.render('error', { 73 | message: err.message, 74 | error: err 75 | }); 76 | }); 77 | } 78 | 79 | // production error handler 80 | // no stacktraces leaked to user 81 | app.use(function(err, req, res, next) { 82 | res.status(err.status || 500); 83 | res.render('error', { 84 | message: err.message, 85 | error: {} 86 | }); 87 | }); 88 | 89 | 90 | module.exports = app; 91 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends ./layouts/_layout.jade 2 | 3 | block vars 4 | - var bodyTemplate = "results" 5 | 6 | block addToHead 7 | 8 | block content 9 | 10 | //- section.waves 11 | //- canvas.waves__canvas 12 | 13 | header.branding 14 | .branding__logo-container 15 | include ../src/images/svg/logo-surfstatus.svg 16 | 17 | section.wave-report 18 | .directions 19 | each direction in [['north','north
shore'], ['west','west
side'], ['east','east
side'], ['south','south
shore']] 20 | - var thisDirection = direction[0]; 21 | - var surfObject = recentSurf[thisDirection] 22 | //- - var tideObject = recentTide[thisDirection] 23 | .directions__direction(data-shore=thisDirection data-height-mean=surfObject.mean) 24 | h1.directions__direction__title 25 | != direction[1] 26 | span.direction__height 27 | .directions__direction__container 28 | .surfbreak 29 | h3.surfbreak__height 30 | = surfObject.min 31 | span - 32 | = surfObject.max 33 | span ’ 34 | .tide 35 | - var tidesTimes = [recentTide[thisDirection][0].time, recentTide[thisDirection][1].time, recentTide[thisDirection][2].time, recentTide[thisDirection][3].time] 36 | - var tidesLabels = [recentTide[thisDirection][0].tideDesc, recentTide[thisDirection][1].tideDesc, recentTide[thisDirection][2].tideDesc, recentTide[thisDirection][3].tideDesc] 37 | - var tidesGraph = [recentTide[thisDirection][0].tideHeight, recentTide[thisDirection][1].tideHeight, recentTide[thisDirection][2].tideHeight, recentTide[thisDirection][3].tideHeight] 38 | svg.tide__graph(data-shore=thisDirection data-tides=tidesGraph data-times=tidesTimes data-time-labels=tidesLabels) 39 | .toggle-mobile 40 | include ../src/images/svg/icon-toggle.svg 41 | section.island-report 42 | .island-report__container 43 | .weather 44 | h2 CURRENT
WEATHER 45 | span #{recentWeather.temperature}° 46 | h4 #{recentWeather.description} 47 | .sunrise-sunset 48 | h2 SUNRISE&SUNSET 49 | .sunrise-sunset__container 50 | include ../src/images/svg/mokes.svg 51 | h4.sunrise-label= recentWeather.sunrise 52 | h4.sunset-label= recentWeather.sunset 53 | .wind(data-wind-direction=recentWeather.windDir) 54 | h2 CURRENT
WIND 55 | include ../src/images/svg/wind-compass.svg 56 | h4 #{recentWeather.windSpeed}MPH from #{recentWeather.windDirComp} 57 | 58 | 59 | block footerScripts 60 | //- script(src="../js/vendor/wave-canvas.js") 61 | script(src="../js/vendor/d3.js") 62 | script. 63 | var windCompass = document.querySelector( '.wind-compass' ); 64 | var windDirectionDegs = #{recentWeather.windDir}; 65 | windCompass.setAttribute('style','transform: rotate(' + windDirectionDegs + 'deg);') -------------------------------------------------------------------------------- /src/images/svg/logo-surfstatus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /models/surf-data.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var async = require('async'); 3 | var parseXML = require('xml2js').parseString; 4 | 5 | var mongoose = require('mongoose'); 6 | var Schema = mongoose.Schema; 7 | var CronJob = require('cron').CronJob; 8 | 9 | var surfSchema = new Schema({ 10 | timestamp: Date, 11 | north: { 12 | full: String, 13 | min: Number, 14 | max: Number, 15 | mean: Number 16 | }, 17 | west: { 18 | full: String, 19 | min: Number, 20 | max: Number, 21 | mean: Number, 22 | }, 23 | east: { 24 | full: String, 25 | min: Number, 26 | max: Number, 27 | mean: Number, 28 | }, 29 | south: { 30 | full: String, 31 | min: Number, 32 | max: Number, 33 | mean: Number 34 | } 35 | }); 36 | 37 | var SurfReport = mongoose.model('SurfData', surfSchema); 38 | 39 | var job = new CronJob({ 40 | cronTime: process.env.CRONTIME_SETAS ? process.env.CRONTIME_DEBUG : '00 00 06 * * *', 41 | onTick: function() { 42 | 43 | var surfHeightRanges = new Array(); 44 | async.parallel([ 45 | function(callback) { 46 | request( 47 | { url: "http://www.prh.noaa.gov/hnl/xml/Surf.xml", method: "GET", timeout: 10000 }, 48 | 49 | function(err, response, body) { 50 | if (err) { 51 | console.log(err); 52 | return; 53 | } 54 | var surfForecast = ""; 55 | 56 | parseXML(body, function(err, result) { 57 | // dear people of NOAA, please don't ever change your RSS format 58 | surfForecast = result['rss']['channel'][0]['item'][1]['description'][0]; 59 | // amen 60 | }); 61 | var hawaiianScale = 2; 62 | // console.log(surfForecast); 63 | var surfForecastDirections = surfForecast.split('\n'); 64 | for (var i = 0; i < surfForecastDirections.length; i++) { 65 | var regExRange = /[0-9]+\s(to)\s[0-9]+/g; 66 | var outputRange = regExRange.exec(surfForecastDirections[i])[0]; 67 | var outputRangeSplit = outputRange.split(' to '); 68 | 69 | outputRangeSplit['min'] = Math.round(outputRangeSplit[0] / hawaiianScale); 70 | outputRangeSplit['max'] = Math.round(outputRangeSplit[1] / hawaiianScale); 71 | outputRangeSplit['mean'] = (outputRangeSplit['min'] + outputRangeSplit['max']) / 2; 72 | 73 | surfHeightRanges.push({ 74 | full: surfForecastDirections[i], 75 | min: outputRangeSplit['min'], 76 | max: outputRangeSplit['max'], 77 | mean: outputRangeSplit['mean'] 78 | }); 79 | } 80 | callback(); 81 | }) 82 | }], 83 | function(err, results) { 84 | if (!err) { 85 | 86 | var newSurfReport = new SurfReport({ 87 | timestamp: new Date(), 88 | north: { 89 | full: surfHeightRanges[0].full, 90 | min: surfHeightRanges[0].min, 91 | max: surfHeightRanges[0].max, 92 | mean: surfHeightRanges[0].mean 93 | }, 94 | west: { 95 | full: surfHeightRanges[1].full, 96 | min: surfHeightRanges[1].min, 97 | max: surfHeightRanges[1].max, 98 | mean: surfHeightRanges[1].mean 99 | }, 100 | east: { 101 | full: surfHeightRanges[2].full, 102 | min: surfHeightRanges[2].min, 103 | max: surfHeightRanges[2].max, 104 | mean: surfHeightRanges[2].mean 105 | }, 106 | south: { 107 | full: surfHeightRanges[3].full, 108 | min: surfHeightRanges[3].min, 109 | max: surfHeightRanges[3].max, 110 | mean: surfHeightRanges[3].mean 111 | } 112 | }); 113 | 114 | newSurfReport.save(function(err){ 115 | if (err) return console.log(err); 116 | console.log('saved new surf height'); 117 | }) 118 | 119 | } else { 120 | console.log('error: '+ err.message); 121 | } 122 | 123 | }); 124 | 125 | }, 126 | start: true, 127 | timeZone: 'Pacific/Honolulu' 128 | }); 129 | job.start(); 130 | 131 | module.exports = surfSchema; -------------------------------------------------------------------------------- /models/weather-data.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var async = require('async'); 3 | 4 | var mongoose = require('mongoose'); 5 | var Schema = mongoose.Schema; 6 | var CronJob = require('cron').CronJob; 7 | 8 | var weatherAPIKey = process.env.KEY_OPENWEATHERMAP; 9 | 10 | var weatherSchema = new Schema({ 11 | timestamp: Date, 12 | temperature: Number, 13 | temperatureMin: Number, 14 | temperatureMax: Number, 15 | description: String, 16 | windSpeed: Number, 17 | windDir: Number, 18 | windDirComp: String, 19 | sunrise: String, 20 | sunset: String 21 | }); 22 | 23 | var WeatherData = mongoose.model('WeatherData', weatherSchema); 24 | 25 | var job = new CronJob({ 26 | cronTime: process.env.CRONTIME_SETAS ? process.env.CRONTIME_DEBUG : '00 00 * * * *', 27 | onTick: function() { 28 | 29 | function toHITime(timestamp) { 30 | var date = new Date(timestamp*1000); 31 | var hours = date.getHours(); 32 | var minutes = (date.getMinutes()<10?'0':'') + date.getMinutes(); 33 | if ( hours > 12 ) { hours = hours - 12 }; 34 | return hours + ':' + minutes; 35 | } 36 | 37 | function toFeet(meter) { 38 | return meter * 3.28084; 39 | } 40 | 41 | function toFarenheit(kelvin) { 42 | return Math.round(kelvin * (9/5) - 459.67); 43 | } 44 | 45 | function toMPH(mps) { 46 | return Math.round( (mps * 2.2369362920544) * 2 ) / 2; 47 | } 48 | 49 | function toCompass(deg) { 50 | while ( deg < 0 ) deg += 360; 51 | while ( deg >= 360 ) deg -= 360; 52 | var val = Math.round( (deg -11.25 ) / 22.5 ); 53 | var arr = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"]; 54 | return arr[ Math.abs(val) ]; 55 | } 56 | 57 | var weather = new Array(); 58 | 59 | async.parallel([ 60 | function(callback) { 61 | // honolulu weather 62 | request( 63 | { url: "http://api.openweathermap.org/data/2.5/weather?id=5856195&appid=" + weatherAPIKey, method: "GET", timeout: 10000 }, 64 | 65 | function(err, response, body) { 66 | 67 | if (err) { 68 | res.render('error', { 69 | message: err.message, 70 | error: err 71 | }); 72 | } 73 | 74 | var jsonResponse = JSON.parse(body); 75 | var weatherSimpleDescription = ""; 76 | if (jsonResponse.weather[0].icon === "02n") { 77 | weatherSimpleDescription = "Light Clouds"; 78 | } else if (jsonResponse.weather[0].icon === "03n" || jsonResponse.weather[0].icon === "04n" ) { 79 | weatherSimpleDescription = "Cloudy"; 80 | } else if (jsonResponse.weather[0].icon === "09n") { 81 | weatherSimpleDescription = "Light Rain"; 82 | } else if (jsonResponse.weather[0].icon === "10n") { 83 | weatherSimpleDescription = "Rain"; 84 | } else if (jsonResponse.weather[0].icon === "11n") { 85 | weatherSimpleDescription = "Thunderstorms"; 86 | } else { 87 | weatherSimpleDescription = "Clear"; 88 | } 89 | 90 | weather = { 91 | "temperature" : toFarenheit(jsonResponse.main.temp), 92 | "temperatureMin" : toFarenheit(jsonResponse.main.temp_min), 93 | "temperatureMax" : toFarenheit(jsonResponse.main.temp_max), 94 | "description" : weatherSimpleDescription, 95 | "windSpeed" : toMPH(jsonResponse.wind.speed), 96 | "windDir" : jsonResponse.wind.deg, 97 | "windDirComp" : toCompass(jsonResponse.wind.deg), 98 | "sunrise" : toHITime(jsonResponse.sys.sunrise)+'am', 99 | "sunset" : toHITime(jsonResponse.sys.sunset)+'pm' 100 | }; 101 | callback(); 102 | 103 | } 104 | ) 105 | }], 106 | function(err, results) { 107 | if (!err) { 108 | 109 | var newWeatherData = new WeatherData({ 110 | timestamp: new Date(), 111 | temperature: weather.temperature, 112 | temperatureMin: weather.temperatureMin, 113 | temperatureMax: weather.temperatureMax, 114 | description: weather.description, 115 | windSpeed: weather.windSpeed, 116 | windDir: weather.windDir, 117 | windDirComp: weather.windDirComp, 118 | sunrise: weather.sunrise, 119 | sunset: weather.sunset 120 | }); 121 | newWeatherData.save(function(err){ 122 | if (err) return handleError(err); 123 | console.log('saved new weather data'); 124 | }) 125 | 126 | } else { 127 | console.log('error: '+ err.message); 128 | } 129 | 130 | }); 131 | 132 | }, 133 | start: true, 134 | timeZone: 'Pacific/Honolulu' 135 | }); 136 | job.start(); 137 | 138 | module.exports = weatherSchema; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var del = require('del'); 2 | var fs = require('fs'); 3 | var browsersync = require('browser-sync'); 4 | var gulp = require('gulp'); 5 | var filter = require('gulp-filter'); 6 | var uglify = require('gulp-uglify'); 7 | var notify = require('gulp-notify'); 8 | var rsync = require('gulp-rsync'); 9 | var gzip = require('gulp-gzip'); 10 | var gutil = require('gulp-util'); 11 | var changed = require('gulp-changed'); 12 | var imagemin = require('gulp-imagemin'); 13 | var bower = require('main-bower-files'); 14 | var order = require('gulp-order'); 15 | var include = require('gulp-include'); 16 | var cssnano = require('gulp-cssnano'); 17 | var concat = require('gulp-concat'); 18 | var sourcemaps = require('gulp-sourcemaps'); 19 | var lost = require('lost'); 20 | var cssnext = require('cssnext'); 21 | var postcss = require('gulp-postcss'); 22 | var postcssimport = require('postcss-import'); 23 | var postcssnested = require('postcss-nested'); 24 | var postcssfocus = require('postcss-focus'); 25 | var postcsspxtorem = require('postcss-pxtorem'); 26 | var postcsscolorfunction = require('postcss-center'); 27 | var postcsssimplevars = require('postcss-simple-vars'); 28 | var postcsssimpleextend = require('postcss-simple-extend'); 29 | 30 | var paths = { 31 | base: { 32 | root: '', 33 | src: './src', 34 | dist: './public', 35 | tmp: './tmp' 36 | } 37 | }; 38 | 39 | paths.src = { 40 | css: paths.base.src + '/css', 41 | js: paths.base.src + '/js', 42 | images: paths.base.src + '/images' 43 | }; 44 | 45 | paths.dist = { 46 | css: paths.base.dist + '/css', 47 | js: paths.base.dist + '/js', 48 | vendorjs: paths.base.dist + '/js/vendor', 49 | images: paths.base.dist + '/images' 50 | }; 51 | 52 | var watching = false; 53 | 54 | var ERROR_LEVELS = ['error', 'warning']; 55 | 56 | var isFatal = function(level) { 57 | return ERROR_LEVELS.indexOf(level) <= ERROR_LEVELS.indexOf(fatalLevel || 'error'); 58 | }; 59 | 60 | var handleError = function(level, error) { 61 | gutil.log(error.message); 62 | if (watching) { 63 | return this.emit('end'); 64 | } else { 65 | return process.exit(1); 66 | } 67 | }; 68 | 69 | var onError = function(error) { 70 | return handleError.call(this, 'error', error); 71 | }; 72 | 73 | var onWarning = function(error) { 74 | return handleError.call(this, 'warning', error); 75 | }; 76 | 77 | var deleteFolderRecursive = function(path) { 78 | if (fs.existsSync(path)) { 79 | fs.readdirSync(path).forEach(function(file, index) { 80 | var curPath; 81 | curPath = path + "/" + file; 82 | if (fs.lstatSync(curPath).isDirectory()) { 83 | return deleteFolderRecursive(curPath); 84 | } else { 85 | return fs.unlinkSync(curPath); 86 | } 87 | }); 88 | return fs.rmdirSync(path); 89 | } 90 | }; 91 | 92 | gulp.task('clean', function() { 93 | return deleteFolderRecursive(paths.base.dist); 94 | }); 95 | 96 | gulp.task('bower', function() { 97 | return gulp.src(bower()).pipe(filter('*.js')).pipe(uglify()).pipe(gulp.dest(paths.dist.vendorjs)).on('error', onError); 98 | }); 99 | 100 | gulp.task('js', function() { 101 | gulp.src(paths.src.js + "/*.js").pipe(order(["helpers.js", "application.js"])).pipe(include().on('error', onError)).pipe(concat('app.js')).pipe(sourcemaps.init()).pipe(uglify().on('error', onError)).pipe(sourcemaps.write('maps')).pipe(gulp.dest(paths.dist.js)).on('error', onError); 102 | return gulp.src(paths.src.js + "/vendor/*.js").pipe(gulp.dest(paths.dist.vendorjs)); 103 | }); 104 | 105 | gulp.task('css', function() { 106 | var postCSSProcessors; 107 | postCSSProcessors = [ 108 | postcssimport({ 109 | from: paths.src.css + "/app.css" 110 | }), postcssnested, postcssfocus, postcsscolorfunction, postcsspxtorem, postcsssimplevars, postcsssimpleextend, lost, cssnext({ 111 | compress: false, 112 | autoprefixer: { 113 | browsers: ['last 1 version'] 114 | } 115 | }) 116 | ]; 117 | return gulp.src(paths.src.css + "/**/[^_]*.{css,scss}").pipe(concat('app.css')).pipe(sourcemaps.init()).pipe(postcss(postCSSProcessors).on('error', onError)).pipe(cssnano({ 118 | browsers: ['last 1 version'] 119 | })).pipe(sourcemaps.write('maps')).pipe(gulp.dest(paths.dist.css)).on('error', onError); 120 | }); 121 | 122 | gulp.task('images', function() { 123 | return gulp.src(paths.src.images + "/**/*.{gif,jpg,png}").pipe(changed(paths.dist.images)).pipe(imagemin({ 124 | optimizationLevel: 3 125 | })).pipe(gulp.dest(paths.dist.images)); 126 | }); 127 | 128 | gulp.task('browsersync', function() { 129 | browsersync.use({ 130 | plugin: function() {}, 131 | hooks: { 132 | 'client:js': fs.readFileSync("./lib/closer.js", "utf-8") 133 | } 134 | }); 135 | return browsersync.init([paths.dist.css, paths.dist.js]); 136 | }); 137 | 138 | gulp.task('watch', ['browsersync'], function() { 139 | watching = true; 140 | gulp.watch([paths.base.src + "/*.*", paths.base.src + "/data/**/*"], ['static-files']); 141 | gulp.watch(paths.src.css + "/**/*", ['css']); 142 | gulp.watch(paths.src.js + "/**/*.{js,coffee}", ['js']); 143 | return gulp.watch(paths.src.images + "/**/*.{gif,jpg,png}", ['images']); 144 | }); 145 | 146 | gulp.task('refresh', ['clean', 'build']); 147 | 148 | gulp.task('build', ['bower', 'js', 'css', 'images']); 149 | 150 | gulp.task('default', ['bower', 'refresh', 'watch']); -------------------------------------------------------------------------------- /src/images/svg/sup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/js/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if ('addEventListener' in document) { 4 | document.addEventListener('DOMContentLoaded', function() { 5 | FastClick.attach(document.body); 6 | }, false); 7 | } 8 | 9 | /* 10 | Surf height description 11 | */ 12 | 13 | var directions = document.querySelectorAll('.directions__direction'); 14 | 15 | if (directions) { 16 | for (var i = 0; i < directions.length; i ++) { 17 | var thisBreaks = directions[i].querySelectorAll('.surfbreak'); 18 | 19 | // SET DESCRIPTIVE SURF CONDITIONS 20 | var thisAverage = directions[i].getAttribute('data-height-mean'); 21 | var setSurfConditions = function(surfConditions) { 22 | directions[i].querySelector('.direction__height').innerHTML = surfConditions; 23 | }; 24 | if (thisAverage < 1) { 25 | setSurfConditions('flat'); 26 | } else if (thisAverage >= 1 && thisAverage < 2) { 27 | setSurfConditions('small'); 28 | } else if (thisAverage >= 2 && thisAverage < 3) { 29 | setSurfConditions('not bad'); 30 | } else if (thisAverage >= 3 && thisAverage < 5) { 31 | setSurfConditions('good'); 32 | } else if (thisAverage >= 5 && thisAverage < 7) { 33 | setSurfConditions('firing'); 34 | } else if (thisAverage >= 7) { 35 | setSurfConditions('massive'); 36 | } 37 | } 38 | 39 | // 40 | // var wind = document.querySelector('.wind'); 41 | // var needle = document.querySelector('.wind-compass__needle path'); 42 | // var windDirection = wind.getAttribute('data-wind-direction'); 43 | // // wind-compass__needle 44 | // needle.setAttribute('style', 'transform: rotate('+ windDirection +'deg);') 45 | // console.log(windDirection); 46 | 47 | /* 48 | CREATE CAROUSELS 49 | */ 50 | 51 | var width = 500, 52 | widthSegment = (width/5), 53 | height = 300; 54 | 55 | var shoreDirections = ['north','west','east','south']; 56 | var tideGraphs = document.querySelectorAll('.tide__graph'); 57 | for (var tideIndex = 0; tideIndex < tideGraphs.length; tideIndex++) { 58 | var tidesData = tideGraphs[tideIndex].getAttribute('data-tides').replace('["','').replace('"]','').split('","'); 59 | var timesData = tideGraphs[tideIndex].getAttribute('data-times').replace('["','').replace('"]','').split('","'); 60 | var timeLabelData = tideGraphs[tideIndex].getAttribute('data-time-labels').replace('["','').replace('"]','').split('","'); 61 | console.log('tidesData', tidesData) 62 | console.log('timesData', timesData) 63 | console.log('timeLabelData', timeLabelData) 64 | 65 | var lineData = [ { "x": 0, "y": height}, 66 | { "x": 0, "y": height/2}, 67 | { "x": widthSegment, "y": (height/2) - (height/4)*tidesData[0]}, 68 | { "x": widthSegment*2, "y": (height/2) - (height/4)*tidesData[1]}, 69 | { "x": widthSegment*3, "y": (height/2) - (height/4)*tidesData[2]}, 70 | { "x": widthSegment*4, "y": (height/2) - (height/4)*tidesData[3]}, 71 | { "x": widthSegment*5, "y": height/2}, 72 | { "x": widthSegment*5, "y": height} 73 | ]; 74 | 75 | var svgContainer = d3.select(".tide__graph[data-shore="+ shoreDirections[tideIndex] +"]").attr('viewBox', '0 0 '+ width +' '+height+''); 76 | 77 | var line = d3.svg.line(); 78 | 79 | var tideFunction = d3.svg.line() 80 | .x(function(d) { 81 | return d.x; 82 | }) 83 | .y(function(d) { 84 | return d.y; 85 | }).interpolate("monotone"); 86 | 87 | var tideAttributes = svgContainer.append("path").attr('d', tideFunction(lineData)); 88 | 89 | var graphMarks = svgContainer.append("line").attr('x1', widthSegment).attr('y1', 0).attr('x2', widthSegment).attr('y2', height).attr('stroke-width', '.0025'); 90 | var graphMarks1 = svgContainer.append("line").attr('x1', widthSegment*2).attr('y1', 0).attr('x2', widthSegment*2).attr('y2', height).attr('stroke-width', '.0025'); 91 | var graphMarks2 = svgContainer.append("line").attr('x1', widthSegment*3).attr('y1', 0).attr('x2', widthSegment*3).attr('y2', height).attr('stroke-width', '.0025'); 92 | var graphMarks3 = svgContainer.append("line").attr('x1', widthSegment*4).attr('y1', 0).attr('x2', widthSegment*4).attr('y2', height).attr('stroke-width', '.0025'); 93 | 94 | var graphHeight = svgContainer.append("text").attr('x', widthSegment ).attr('y', 60).text(Math.round(tidesData[0] * 100) / 100 + 'ft'); 95 | var graphHeight1 = svgContainer.append("text").attr('x', widthSegment*2).attr('y', 60).text(Math.round(tidesData[1] * 100) / 100 + 'ft'); 96 | var graphHeight2 = svgContainer.append("text").attr('x', widthSegment*3).attr('y', 60).text(Math.round(tidesData[2] * 100) / 100 + 'ft'); 97 | var graphHeight3 = svgContainer.append("text").attr('x', widthSegment*4).attr('y', 60).text(Math.round(tidesData[3] * 100) / 100 + 'ft'); 98 | 99 | var graphTime = svgContainer.append("text").attr('x', widthSegment ).attr('y', 280).text(timesData[0]); 100 | var graphTime1 = svgContainer.append("text").attr('x', widthSegment*2).attr('y', 280).text(timesData[1]); 101 | var graphTime2 = svgContainer.append("text").attr('x', widthSegment*3).attr('y', 280).text(timesData[2]); 102 | var graphTime3 = svgContainer.append("text").attr('x', widthSegment*4).attr('y', 280).text(timesData[3]); 103 | 104 | var graphLabel = svgContainer.append("text").attr('x', widthSegment).attr('y', 40).text(timeLabelData[0]); 105 | var graphLabel1 = svgContainer.append("text").attr('x', widthSegment*2).attr('y', 40).text(timeLabelData[1]); 106 | var graphLabel2 = svgContainer.append("text").attr('x', widthSegment*3).attr('y', 40).text(timeLabelData[2]); 107 | var graphLabel3 = svgContainer.append("text").attr('x', widthSegment*4).attr('y', 40).text(timeLabelData[3]); 108 | } 109 | 110 | } 111 | 112 | 113 | 114 | // media query event handler 115 | if (matchMedia) { 116 | var mq = window.matchMedia("(max-width: 639px)"); 117 | mq.addListener(mobileListener); 118 | mobileListener(mq); 119 | } 120 | 121 | // media query change 122 | function mobileListener(mq) { 123 | 124 | if (mq.matches) { 125 | for (var i = 0; i < directions.length; i++) { 126 | var thisDirection = directions[i]; 127 | addClass(thisDirection, 'directions__direction--dropdown'); 128 | 129 | var toggleMobile = thisDirection.querySelector('.toggle-mobile'); 130 | toggleMobile.addEventListener('click', function() { 131 | if ( hasClass(this.parentNode, 'directions__direction--dropdown-active') ) { 132 | removeClass(this.parentNode, 'directions__direction--dropdown-active'); 133 | } else { 134 | addClass(this.parentNode, 'directions__direction--dropdown-active'); 135 | } 136 | }) 137 | } 138 | } 139 | else { 140 | for (var i = 0; i < directions.length; i++) { 141 | removeClass(directions[i], 'directions__direction--dropdown'); 142 | } 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /src/css/templates/_home.css: -------------------------------------------------------------------------------- 1 | .wave-report { 2 | lost-utility: clearfix; 3 | transition: all $easeOutExpo 400ms; 4 | } 5 | @media screen and (min-width: 640px) { 6 | .wave-report { 7 | padding: calc($column-gutter*2) calc($column-gutter*2); 8 | } 9 | } 10 | 11 | .directions { 12 | display: table; 13 | width: 100%; 14 | text-align: center; 15 | cursor: default; 16 | lost-utility: clearfix; 17 | transition: all $easeOutExpo 400ms; 18 | background-color: $c-grid-border; 19 | *::selection { 20 | background: transparent; 21 | } 22 | 23 | &__direction { 24 | position: relative; 25 | float: left; 26 | width: 100%; 27 | background-color: $c-body-bg; 28 | padding-top: 0; 29 | padding-bottom: 0; 30 | 31 | display: table-cell; 32 | 33 | &__title { 34 | position: absolute; 35 | top: 1rem; 36 | left: 1rem; 37 | font-size: 2rem; 38 | margin: 0; 39 | display: inline-block; 40 | line-height: .85; 41 | color: #fff; 42 | text-transform: uppercase; 43 | text-align: left; 44 | span { 45 | position: absolute; 46 | bottom: -.5vw; 47 | right: -.5vw; 48 | 49 | font-family: $title-font-family; 50 | font-size: 4vw; 51 | text-transform: lowercase; 52 | color: $c-red; 53 | text-shadow: 0 0 .5rem color(#000 a(90%)); 54 | } 55 | } 56 | 57 | &__container { 58 | width: 100%; 59 | } 60 | } 61 | } 62 | 63 | .toggle-mobile { 64 | display: none; 65 | } 66 | 67 | .directions__direction--dropdown { 68 | height: 6rem; 69 | overflow: hidden; 70 | 71 | padding-bottom: 0; 72 | transition: padding 500ms $easeOutExpo; 73 | 74 | border-top: 4px solid $c-grid-border; 75 | &:last-of-type { 76 | border-bottom: 4px solid $c-grid-border; 77 | } 78 | 79 | &-active { 80 | padding-bottom: calc(75% + 4rem); 81 | } 82 | 83 | .surfbreak { 84 | h3 { 85 | padding-right: 3rem; 86 | } 87 | } 88 | .tide { 89 | svg { 90 | margin-top: 0; 91 | } 92 | } 93 | .toggle-mobile { 94 | display: block; 95 | position: absolute; 96 | top: 0; 97 | right: .5rem; 98 | height: 6rem; 99 | cursor: pointer; 100 | svg { 101 | position: relative; 102 | top: 50%; 103 | transform: translateY(-50%); 104 | width: 2rem; 105 | fill: $c-grid-border; 106 | transition: fill 250ms $easeOutExpo; 107 | } 108 | &:hover svg { 109 | fill: color($c-grid-border blend(#fff 5%)); 110 | } 111 | } 112 | } 113 | 114 | @media screen and (min-width: 640px) { 115 | .directions { 116 | &__direction { 117 | lost-column: 1/2 2 $column-gutter; 118 | padding-top: calc($column-gutter*2); 119 | padding-bottom: calc($column-gutter*2); 120 | &__title { 121 | position: relative; 122 | top: 0; 123 | left: 0; 124 | font-size: 8vw; 125 | span { 126 | font-size: 3vw; 127 | text-shadow: 0 0 .5rem color(#000 a(50%)); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | @media screen and (min-width: 1200px) { 135 | .directions { 136 | &__direction { 137 | lost-column: 1/4 4 $column-gutter; 138 | /* border: 0; 139 | border-right: $column-gutter $c-grid-border solid; */ 140 | /* &:nth-child(1), &:nth-child(2) { 141 | border-bottom: 0; 142 | } 143 | &:nth-child(even) { border-right: $column-gutter $c-grid-border solid; } 144 | &:last-of-type { border-right-color: transparent; } */ 145 | &__title { 146 | font-size: 4vw; 147 | span { 148 | font-size: 2vw; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .surfbreak { 156 | padding: $column-gutter 0; 157 | &__height { 158 | font-size: 3rem; 159 | padding-right: $column-gutter; 160 | width: 50%; 161 | left: 50%; 162 | position: relative; 163 | text-align: right; 164 | 165 | margin: 0; 166 | color: #fff; 167 | transition: font $easeOutExpo 400ms; 168 | } 169 | } 170 | @media screen and (min-width: 640px) { 171 | .surfbreak { 172 | padding: 4vw 0; 173 | &__height { 174 | width: 100%; 175 | left: 0; 176 | padding-right: 0; 177 | text-align: center; 178 | font-size: 10vw; 179 | } 180 | } 181 | } 182 | 183 | @media screen and (min-width: 1200px) { 184 | .surfbreak { 185 | &__height { 186 | font-size: 6vw; 187 | } 188 | } 189 | } 190 | .tide { 191 | padding: 0 calc($column-gutter*2); 192 | svg { 193 | border-radius: $column-gutter; 194 | box-shadow: 0 0 3rem 0 color(#000 a(.5)); 195 | margin-top: $column-gutter; 196 | path { 197 | fill: #fff; 198 | } 199 | line { 200 | stroke: color($c-body-bg a(.2)); 201 | stroke-width: calc($column-gutter/4); 202 | transform: translate3d(-4px, 0, 0); 203 | } 204 | text { 205 | font-size: 1rem; 206 | letter-spacing: .01rem; 207 | fill: $c-body-bg; 208 | text-anchor: middle; 209 | &[y="40"] { 210 | fill: $c-red; 211 | text-transform: uppercase; 212 | text-anchor: middle; 213 | font-size: 1.25rem; 214 | letter-spacing: .05rem; 215 | } 216 | &[y="60"] { 217 | fill: #fff; 218 | } 219 | &[y="280"] { 220 | /* fill: #fff; */ 221 | font-size: 1rem; 222 | } 223 | } 224 | } 225 | } 226 | 227 | .island-report { 228 | padding: 0 calc($column-gutter*2); 229 | margin: calc($column-gutter*2) 0; 230 | lost-utility: clearfix; 231 | transition: all $easeOutExpo 400ms; 232 | &__container { 233 | text-align: center; 234 | > div { 235 | margin-bottom: calc($column-gutter * 2); 236 | } 237 | } 238 | h2 { 239 | margin: 0; 240 | line-height: 1; 241 | color: #fff; 242 | font-size: 2rem; 243 | span { 244 | position: relative; 245 | z-index: 1000; 246 | display: block; 247 | line-height: 0; 248 | color: $c-red; 249 | text-shadow: 0 0 .75rem #000; 250 | } 251 | } 252 | h4 { 253 | margin: 0; 254 | color: #fff; 255 | font-size: 1.25rem; 256 | } 257 | } 258 | 259 | @media screen and (min-width: 640px) { 260 | .island-report { 261 | padding: 0 calc($column-gutter*4) calc($column-gutter*2); 262 | } 263 | } 264 | 265 | @media screen and (min-width: 768px) { 266 | .island-report { 267 | &__container { 268 | > div { 269 | lost-column: 1/3 3; 270 | margin-bottom: 0; 271 | } 272 | } 273 | } 274 | } 275 | 276 | .weather { 277 | span { 278 | color: $c-red; 279 | font-weight: 600; 280 | font-size: 6.5rem; 281 | } 282 | } 283 | 284 | .sunrise-sunset { 285 | padding-bottom: calc($column-gutter*2); 286 | display: inline-block; 287 | transition: all $easeOutExpo 400ms; 288 | &__container { 289 | position: relative; 290 | display: inline-block; 291 | } 292 | svg { 293 | width: 100%; 294 | max-width: 20rem; 295 | height: auto; 296 | padding: 1rem 0; 297 | } 298 | h4 { 299 | position: absolute; 300 | bottom: calc(-$column-gutter); 301 | text-transform: uppercase; 302 | &.sunrise-label { 303 | left: 0; 304 | } 305 | &.sunset-label { 306 | right: 0; 307 | } 308 | } 309 | } 310 | 311 | .mokes { 312 | use { 313 | fill: $c-red; 314 | opacity: 0; 315 | &:nth-of-type(1) { 316 | animation: sunset 12s $easeOutExpo 6s infinite; 317 | } 318 | &:nth-of-type(2) { 319 | animation: sunrise 12s $easeOutExpo 0s infinite; 320 | } 321 | } 322 | &__island { 323 | &:nth-of-type(1) { 324 | fill: $c-grid-border; 325 | } 326 | &:nth-of-type(2) { 327 | fill: color($c-grid-border tint(3%)); 328 | } 329 | } 330 | } 331 | 332 | .wind { 333 | svg { 334 | margin: 0.25rem 0 1rem; 335 | height: 6.5rem; 336 | path { 337 | fill: $c-red; 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /src/images/svg/hawaiian-islands.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/js/vendor/wave-canvas.js: -------------------------------------------------------------------------------- 1 | var size = { 2 | width: window.innerWidth, 3 | height: window.innerHeight 4 | }; 5 | 6 | /* 7 | * UTILITIES 8 | */ 9 | 10 | function times( amount, closure ) { 11 | for ( var i = 0; i < amount; i++ ) { 12 | closure( i ); 13 | } 14 | } 15 | 16 | function func( name ) { 17 | return function( obj ) { 18 | return obj[ name ](); 19 | }; 20 | } 21 | 22 | 23 | function rand( min, max ) { 24 | return min + ( max - min ) * Math.random(); 25 | } 26 | 27 | function bezier( points, context ) { 28 | 29 | var a, b, x, y; 30 | 31 | for ( var i = 1, length = points.length - 2; i < length; i++ ) { 32 | 33 | a = points[ i ]; 34 | b = points[ i + 1 ]; 35 | 36 | x = ( a.x + b.x ) * 0.5; 37 | y = ( a.y + b.y ) * 0.5; 38 | 39 | context.quadraticCurveTo( a.x, a.y, x, y ); 40 | } 41 | 42 | a = points[ i ]; 43 | b = points[ i + 1 ]; 44 | 45 | context.quadraticCurveTo( a.x, a.y, b.x, b.y ); 46 | } 47 | 48 | function distance( a, b ) { 49 | var x = b.x - a.x; 50 | var y = b.y - a.y; 51 | 52 | return Math.sqrt( x * x + y * y ); 53 | } 54 | 55 | function clamp( val, min, max ) { 56 | return val < min ? min : ( val > max ? max : val ); 57 | } 58 | 59 | /* 60 | * GLOBAL CLASSES 61 | */ 62 | 63 | var Mouse = ( function() { 64 | 65 | var exports = { 66 | x: 0, 67 | y: 0, 68 | bind: function( canvas ) { 69 | canvas.addEventListener( "mousemove", onMouseMove ); 70 | canvas.addEventListener( "touchmove", onTouchMove ); 71 | }, 72 | unbind: function( canvas ) { 73 | canvas.removeEventListener( "mousemove", onMouseMove ); 74 | canvas.removeEventListener( "touchmove", onTouchMove ); 75 | } 76 | }; 77 | 78 | function onMouseMove( event ) { 79 | exports.x = event.pageX; 80 | exports.y = event.pageY; 81 | } 82 | 83 | function onTouchMove( event ) { 84 | event.preventDefault(); 85 | 86 | exports.x = event.touches[ 0 ].pageX; 87 | exports.y = event.touches[ 0 ].pageY; 88 | } 89 | 90 | return exports; 91 | 92 | } )(); 93 | 94 | var Stage = { 95 | width: 1, 96 | height: 1, 97 | set: function( values ) { 98 | Stage.width = values.width; 99 | Stage.height = values.height; 100 | } 101 | }; 102 | 103 | /* 104 | * ARCHITECTURE CLASSES 105 | */ 106 | 107 | var Water = function( context ) { 108 | 109 | var waves; 110 | 111 | function init() { 112 | options.waveComeUp = this.start.bind( this ); 113 | } 114 | 115 | this.render = function() { 116 | context.strokeStyle = options.color; 117 | context.lineWidth = options.lineWidth; 118 | context.lineCap = "round"; 119 | context.beginPath(); 120 | 121 | waves.forEach( func( "render" ) ); 122 | 123 | context.stroke(); 124 | }; 125 | 126 | this.setSize = function( width, height ) { 127 | 128 | createWaves( height ); 129 | 130 | waves.forEach( function( wave ) { 131 | wave.setSize( width, height ); 132 | } ); 133 | 134 | }; 135 | 136 | this.start = function() { 137 | waves.forEach( func( "start" ) ); 138 | }; 139 | 140 | function createWaves( height ) { 141 | 142 | waves = []; 143 | var distance = options.distance; 144 | 145 | times( height / distance, function( index ) { 146 | waves.push( new Wave( 0, index * distance + 10, context, rand( 0.08, 0.12 ) * index ) ); 147 | } ); 148 | 149 | } 150 | 151 | init.call( this ); 152 | 153 | }; 154 | 155 | var Wave = function( originalX, originalY, context, offset ) { 156 | 157 | var anchors; 158 | var width; 159 | var height; 160 | var mouseDirection; 161 | var oldMouse; 162 | var x; 163 | var y; 164 | 165 | function init() { 166 | x = originalX; 167 | y = originalY; 168 | 169 | anchors = []; 170 | mouseDirection = { x: 0, y: 0 }; 171 | 172 | var anchor; 173 | var current = 0; 174 | var start = - options.waveAmplitude; 175 | var target = options.waveAmplitude; 176 | var delta = offset; 177 | var step = 0.4; 178 | 179 | times( window.innerWidth / options.waveLength, function() { 180 | anchor = new Anchor( current, 0, start, target, delta ); 181 | anchor.setOrigin( current + x, y ); 182 | 183 | anchors.push( anchor ); 184 | 185 | current += 90; 186 | delta += step; 187 | 188 | if ( delta > 1 ) { 189 | times( Math.floor( delta ), function() { 190 | delta--; 191 | start *= -1; 192 | target *= -1; 193 | } ); 194 | } 195 | 196 | } ); 197 | } 198 | 199 | this.render = function() { 200 | 201 | update(); 202 | 203 | context.save(); 204 | context.translate( x, y ); 205 | 206 | context.moveTo( anchors[ 0 ].x, anchors[ 0 ].y ); 207 | bezier( anchors, context ); 208 | 209 | context.restore(); 210 | }; 211 | 212 | this.setSize = function( _width, _height ) { 213 | width = _width; 214 | height = _height; 215 | 216 | var step = _width / ( anchors.length - 1 ); 217 | 218 | anchors.forEach( function( anchor, i ) { 219 | anchor.x = step * i; 220 | anchor.setOrigin( anchor.x, y ); 221 | } ); 222 | }; 223 | 224 | this.onAmpChange = function() { 225 | anchors.forEach( func( "onAmpChange" ) ); 226 | }; 227 | 228 | this.start = function() { 229 | y = height + 300 + originalY * 0.4; 230 | }; 231 | 232 | function update() { 233 | var targetY = Math.min( y, Mouse.y + originalY ); 234 | y += ( targetY - y ) / options.waveRiseSpeed; 235 | 236 | updateMouse(); 237 | 238 | anchors.forEach( function( anchor ) { 239 | anchor.update( mouseDirection, y ); 240 | } ); 241 | } 242 | 243 | function updateMouse() { 244 | if ( ! oldMouse ) { 245 | oldMouse = { x: Mouse.x, y: Mouse.y }; 246 | return; 247 | } 248 | 249 | mouseDirection.x = Mouse.x - oldMouse.x; 250 | mouseDirection.y = Mouse.y - oldMouse.y; 251 | 252 | oldMouse = { x: Mouse.x, y: Mouse.y }; 253 | } 254 | 255 | init.call( this ); 256 | 257 | }; 258 | 259 | var Anchor = function( x, y, start, target, delta ) { 260 | 261 | var spring; 262 | var motion; 263 | var origin; 264 | 265 | function init() { 266 | spring = new Spring(); 267 | motion = new Motion( start, target, delta ); 268 | origin = {}; 269 | this.x = x; 270 | this.y = y; 271 | } 272 | 273 | this.update = function( mouseDirection, currentY ) { 274 | origin.y = currentY; 275 | 276 | var factor = getMultiplier(); 277 | var vector = { 278 | x: mouseDirection.x * factor * options.waveMouse, 279 | y: mouseDirection.y * factor * options.waveMouse 280 | }; 281 | 282 | if ( factor > 0 ) { 283 | spring.shoot( vector ); 284 | } 285 | 286 | spring.update(); 287 | motion.update(); 288 | 289 | this.y = motion.get() + spring.y; 290 | }; 291 | 292 | this.onAmpChange = function() { 293 | motion.onAmpChange(); 294 | }; 295 | 296 | this.setOrigin = function( x, y ) { 297 | origin.x = x; 298 | origin.y = y; 299 | }; 300 | 301 | 302 | function getMultiplier() { 303 | var lang = distance( Mouse, origin ); 304 | var radius = options.waveRadius; 305 | 306 | return lang < radius ? 1 - lang / radius : 0; 307 | } 308 | 309 | init.call( this ); 310 | 311 | }; 312 | 313 | var Motion = function( start, target, delta ) { 314 | 315 | var SPEED = 0.02; 316 | var half; 317 | var upper; 318 | var lower; 319 | var min; 320 | var max; 321 | 322 | function init() { 323 | this.onAmpChange(); 324 | } 325 | 326 | 327 | this.setRange = function( a, b ) { 328 | min = a; 329 | max = b; 330 | }; 331 | 332 | this.update = function() { 333 | delta += SPEED; 334 | 335 | if ( delta > 1 ) { 336 | delta = 0; 337 | start = target; 338 | target = target < half ? rand( upper, max ) : rand( min, lower ); 339 | } 340 | }; 341 | 342 | this.get = function() { 343 | var factor = ( Math.cos( ( 1 + delta ) * Math.PI ) + 1 ) / 2; 344 | return start + factor * ( target - start ); 345 | }; 346 | 347 | this.onAmpChange = function() { 348 | min = - options.waveAmplitude; 349 | max = options.waveAmplitude; 350 | half = min + ( max - min ) / 2; 351 | upper = min + ( max - min ) * 0.75; 352 | lower = min + ( max - min ) * 0.25; 353 | }; 354 | 355 | 356 | init.call( this ); 357 | 358 | }; 359 | 360 | var Spring = function() { 361 | 362 | var px = 0; 363 | var py = 0; 364 | var vx = 0; 365 | var vy = 0; 366 | var targetX = 0; 367 | var targetY = 0; 368 | var timeout; 369 | 370 | function init() { 371 | this.x = 0; 372 | this.y = 0; 373 | } 374 | 375 | this.update = function() { 376 | vx = targetX - this.x; 377 | vy = targetY - this.y; 378 | px = px * options.waveElasticity + vx * options.waveStrength; 379 | py = py * options.waveElasticity + vy * options.waveStrength; 380 | this.x += px; 381 | this.y += py; 382 | }; 383 | 384 | this.shoot = function( vector ) { 385 | targetX = clamp( vector.x, -options.waveMax, options.waveMax ); 386 | targetY = clamp( vector.y, -options.waveMax, options.waveMax ); 387 | 388 | clearTimeout( timeout ); 389 | timeout = setTimeout( cancelOffset, 100 ); 390 | }; 391 | 392 | function cancelOffset() { 393 | targetX = 0; 394 | targetY = 0; 395 | } 396 | 397 | init.call( this ); 398 | }; 399 | 400 | var Canvas = function( canvas, size ) { 401 | 402 | var context; 403 | var width, height; 404 | var animation; 405 | 406 | function init() { 407 | 408 | context = canvas.getContext( "2d" ); 409 | 410 | setTimeout( function() { 411 | Mouse.bind( canvas ); 412 | }, 1000 ); 413 | 414 | Stage.set( size ); 415 | 416 | animation = new Water( context ); 417 | 418 | this.setSize( size.width, size.height ); 419 | 420 | animation.start(); 421 | 422 | requestAnimationFrame( render ); 423 | } 424 | 425 | function render() { 426 | context.setTransform( 1, 0, 0, 1, 0, 0 ); 427 | context.clearRect( 0, 0, width, height ); 428 | 429 | context.save(); 430 | animation.render(); 431 | context.restore(); 432 | 433 | requestAnimationFrame( render ); 434 | } 435 | 436 | this.setSize = function( _width, _height ) { 437 | 438 | canvas.width = Stage.width = width = _width; 439 | canvas.height = Stage.height = height = _height; 440 | 441 | animation.setSize( _width, _height ); 442 | }; 443 | 444 | init.call( this ); 445 | }; -------------------------------------------------------------------------------- /models/tide-data.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var async = require('async'); 3 | var parseXML = require('xml2js').parseString; 4 | 5 | var mongoose = require('mongoose'); 6 | var Schema = mongoose.Schema; 7 | var CronJob = require('cron').CronJob; 8 | 9 | var tideAPIKey = process.env.KEY_WORLDTIDES; 10 | 11 | var tideSchema = new Schema({ 12 | timestamp: Date, 13 | north: { 14 | 0: { 15 | date: String, 16 | time: String, 17 | tideHeight: String, 18 | tideDesc: String 19 | }, 20 | 1: { 21 | date: String, 22 | time: String, 23 | tideHeight: String, 24 | tideDesc: String 25 | }, 26 | 2: { 27 | date: String, 28 | time: String, 29 | tideHeight: String, 30 | tideDesc: String 31 | }, 32 | 3: { 33 | date: String, 34 | time: String, 35 | tideHeight: String, 36 | tideDesc: String 37 | } 38 | }, 39 | west: { 40 | 0: { 41 | date: String, 42 | time: String, 43 | tideHeight: String, 44 | tideDesc: String 45 | }, 46 | 1: { 47 | date: String, 48 | time: String, 49 | tideHeight: String, 50 | tideDesc: String 51 | }, 52 | 2: { 53 | date: String, 54 | time: String, 55 | tideHeight: String, 56 | tideDesc: String 57 | }, 58 | 3: { 59 | date: String, 60 | time: String, 61 | tideHeight: String, 62 | tideDesc: String 63 | } 64 | }, 65 | east: { 66 | 0: { 67 | date: String, 68 | time: String, 69 | tideHeight: String, 70 | tideDesc: String 71 | }, 72 | 1: { 73 | date: String, 74 | time: String, 75 | tideHeight: String, 76 | tideDesc: String 77 | }, 78 | 2: { 79 | date: String, 80 | time: String, 81 | tideHeight: String, 82 | tideDesc: String 83 | }, 84 | 3: { 85 | date: String, 86 | time: String, 87 | tideHeight: String, 88 | tideDesc: String 89 | } 90 | }, 91 | south: { 92 | 0: { 93 | date: String, 94 | time: String, 95 | tideHeight: String, 96 | tideDesc: String 97 | }, 98 | 1: { 99 | date: String, 100 | time: String, 101 | tideHeight: String, 102 | tideDesc: String 103 | }, 104 | 2: { 105 | date: String, 106 | time: String, 107 | tideHeight: String, 108 | tideDesc: String 109 | }, 110 | 3: { 111 | date: String, 112 | time: String, 113 | tideHeight: String, 114 | tideDesc: String 115 | } 116 | } 117 | }); 118 | 119 | var TideData = mongoose.model('TideData', tideSchema); 120 | 121 | var job = new CronJob({ 122 | cronTime: process.env.CRONTIME_SETAS ? process.env.CRONTIME_DEBUG : '00 00 00 * * *', 123 | onTick: function() { 124 | 125 | function toFeet(meter) { 126 | return meter * 3.28084; 127 | } 128 | 129 | function getTime(timestamp) { 130 | var date = new Date(timestamp*1000); 131 | var hours = ((date.getHours() + 11) % 12) + 1; 132 | var minutes = (date.getMinutes()<10?'0':'') + date.getMinutes(); 133 | var amPm = date.getHours() > 11 ? 'pm' : 'am'; 134 | return hours + ':' + minutes + amPm; 135 | } 136 | function getDate(timestamp) { 137 | var date = new Date(timestamp*1000); 138 | var month = date.getMonth() + 1; 139 | var day = date.getDate(); 140 | return month + '/' + day; 141 | } 142 | 143 | var now = new Date(); 144 | var startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 145 | var timestamp = startOfDay / 1000; 146 | 147 | var tideTimeParams = "&start=" + timestamp + "&length=172800"; 148 | 149 | var tides = new Array(); 150 | 151 | async.parallel([ 152 | function(callback) { 153 | // north 154 | request( 155 | { url: "https://www.worldtides.info/api?extremes&lat=21.690596&lon=-158.092333" + tideTimeParams + "&key=" + tideAPIKey, method: "GET", timeout: 10000 }, 156 | 157 | function(err, response, body) { 158 | 159 | if (err) { 160 | res.render('error', { 161 | message: err.message, 162 | error: err 163 | }); 164 | } 165 | 166 | var jsonResponse = JSON.parse(body); 167 | 168 | tides.north = { 169 | 0 : { 170 | date: getDate(jsonResponse.extremes[0].dt), 171 | time: getTime(jsonResponse.extremes[0].dt), 172 | tideHeight: toFeet(jsonResponse.extremes[0].height), 173 | tideDesc: jsonResponse.extremes[0].type 174 | }, 175 | 1 : { 176 | date: getDate(jsonResponse.extremes[1].dt), 177 | time: getTime(jsonResponse.extremes[1].dt), 178 | tideHeight: toFeet(jsonResponse.extremes[1].height), 179 | tideDesc: jsonResponse.extremes[1].type 180 | }, 181 | 2 : { 182 | date: getDate(jsonResponse.extremes[2].dt), 183 | time: getTime(jsonResponse.extremes[2].dt), 184 | tideHeight: toFeet(jsonResponse.extremes[2].height), 185 | tideDesc: jsonResponse.extremes[2].type 186 | }, 187 | 3 : { 188 | date: getDate(jsonResponse.extremes[3].dt), 189 | time: getTime(jsonResponse.extremes[3].dt), 190 | tideHeight: toFeet(jsonResponse.extremes[3].height), 191 | tideDesc: jsonResponse.extremes[3].type 192 | } 193 | }; 194 | 195 | // console.log('tides.north - \n', tides.north); 196 | callback(); 197 | 198 | } 199 | ) 200 | // } 201 | }, 202 | function(callback) { 203 | // west 204 | request( 205 | { url: "https://www.worldtides.info/api?extremes&lat=21.412162&lon=-158.269043" + tideTimeParams + "&key=" + tideAPIKey, method: "GET", timeout: 10000 }, 206 | 207 | function(err, response, body) { 208 | 209 | if (err) { 210 | res.render('error', { 211 | message: err.message, 212 | error: err 213 | }); 214 | } 215 | 216 | var jsonResponse = JSON.parse(body); 217 | 218 | tides.west = { 219 | 0 : { 220 | date: getDate(jsonResponse.extremes[0].dt), 221 | time: getTime(jsonResponse.extremes[0].dt), 222 | tideHeight: toFeet(jsonResponse.extremes[0].height), 223 | tideDesc: jsonResponse.extremes[0].type 224 | }, 225 | 1 : { 226 | date: getDate(jsonResponse.extremes[1].dt), 227 | time: getTime(jsonResponse.extremes[1].dt), 228 | tideHeight: toFeet(jsonResponse.extremes[1].height), 229 | tideDesc: jsonResponse.extremes[1].type 230 | }, 231 | 2 : { 232 | date: getDate(jsonResponse.extremes[2].dt), 233 | time: getTime(jsonResponse.extremes[2].dt), 234 | tideHeight: toFeet(jsonResponse.extremes[2].height), 235 | tideDesc: jsonResponse.extremes[2].type 236 | }, 237 | 3 : { 238 | date: getDate(jsonResponse.extremes[3].dt), 239 | time: getTime(jsonResponse.extremes[3].dt), 240 | tideHeight: toFeet(jsonResponse.extremes[3].height), 241 | tideDesc: jsonResponse.extremes[3].type 242 | } 243 | }; 244 | 245 | // console.log('tides.west - \n', tides.west); 246 | callback(); 247 | 248 | } 249 | ) 250 | } 251 | // }, 252 | // function(callback) { 253 | // // east 254 | // request( 255 | // { url: "https://www.worldtides.info/api?extremes&lat=21.477751&lon=-157.789996" + tideTimeParams + "&key=" + tideAPIKey, method: "GET", timeout: 10000 }, 256 | 257 | // function(err, response, body) { 258 | 259 | // if (err) { 260 | // res.render('error', { 261 | // message: err.message, 262 | // error: err 263 | // }); 264 | // } 265 | 266 | // var jsonResponse = JSON.parse(body); 267 | 268 | // tides.east = { 269 | // 0 : { 270 | // date: getDate(jsonResponse.extremes[0].dt), 271 | // time: getTime(jsonResponse.extremes[0].dt), 272 | // tideHeight: toFeet(jsonResponse.extremes[0].height), 273 | // tideDesc: jsonResponse.extremes[0].type 274 | // }, 275 | // 1 : { 276 | // date: getDate(jsonResponse.extremes[1].dt), 277 | // time: getTime(jsonResponse.extremes[1].dt), 278 | // tideHeight: toFeet(jsonResponse.extremes[1].height), 279 | // tideDesc: jsonResponse.extremes[1].type 280 | // }, 281 | // 2 : { 282 | // date: getDate(jsonResponse.extremes[2].dt), 283 | // time: getTime(jsonResponse.extremes[2].dt), 284 | // tideHeight: toFeet(jsonResponse.extremes[2].height), 285 | // tideDesc: jsonResponse.extremes[2].type 286 | // }, 287 | // 3 : { 288 | // date: getDate(jsonResponse.extremes[3].dt), 289 | // time: getTime(jsonResponse.extremes[3].dt), 290 | // tideHeight: toFeet(jsonResponse.extremes[3].height), 291 | // tideDesc: jsonResponse.extremes[3].type 292 | // } 293 | // }; 294 | 295 | // // console.log('tides.east - \n', tides.east); 296 | // callback(); 297 | 298 | // } 299 | // ) 300 | // }, 301 | // function(callback) { 302 | // // south 303 | // request( 304 | // { url: "https://www.worldtides.info/api?extremes&lat=21.2749739&lon=-157.8491944" + tideTimeParams + "&key=" + tideAPIKey, method: "GET", timeout: 10000 }, 305 | 306 | // function(err, response, body) { 307 | 308 | // if (err) { 309 | // res.render('error', { 310 | // message: err.message, 311 | // error: err 312 | // }); 313 | // } 314 | 315 | // var jsonResponse = JSON.parse(body); 316 | 317 | // tides.south = { 318 | // 0 : { 319 | // date: getDate(jsonResponse.extremes[0].dt), 320 | // time: getTime(jsonResponse.extremes[0].dt), 321 | // tideHeight: toFeet(jsonResponse.extremes[0].height), 322 | // tideDesc: jsonResponse.extremes[0].type 323 | // }, 324 | // 1 : { 325 | // date: getDate(jsonResponse.extremes[1].dt), 326 | // time: getTime(jsonResponse.extremes[1].dt), 327 | // tideHeight: toFeet(jsonResponse.extremes[1].height), 328 | // tideDesc: jsonResponse.extremes[1].type 329 | // }, 330 | // 2 : { 331 | // date: getDate(jsonResponse.extremes[2].dt), 332 | // time: getTime(jsonResponse.extremes[2].dt), 333 | // tideHeight: toFeet(jsonResponse.extremes[2].height), 334 | // tideDesc: jsonResponse.extremes[2].type 335 | // }, 336 | // 3 : { 337 | // date: getDate(jsonResponse.extremes[3].dt), 338 | // time: getTime(jsonResponse.extremes[3].dt), 339 | // tideHeight: toFeet(jsonResponse.extremes[3].height), 340 | // tideDesc: jsonResponse.extremes[3].type 341 | // } 342 | // }; 343 | 344 | // // console.log('tides.south - \n', tides.south); 345 | // callback(); 346 | 347 | // } 348 | // ) 349 | // } 350 | 351 | ], 352 | function(err, results) { 353 | if (!err) { 354 | // TODO - fix tide so each location gets unique 355 | var newTideData = new TideData({ 356 | timestamp: new Date(), 357 | north: tides.north, 358 | west: tides.west, 359 | east: tides.north, 360 | south: tides.west 361 | }); 362 | newTideData.save(function(err){ 363 | if (err) return console.log(err); 364 | console.log('saved new tide data'); 365 | }) 366 | 367 | } else { 368 | console.log('error: '+ err.message); 369 | } 370 | } 371 | ); 372 | 373 | }, 374 | start: true, 375 | timeZone: 'Pacific/Honolulu' 376 | }); 377 | job.start(); 378 | 379 | module.exports = tideSchema; -------------------------------------------------------------------------------- /src/images/svg/logo-surfornah.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/svg/oahu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------