├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/images/svg/oahu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------