Skip to content

Instantly share code, notes, and snippets.

@stilist
Last active August 29, 2015 14:24
Show Gist options
  • Save stilist/963000a9cc891189cfe3 to your computer and use it in GitHub Desktop.
Save stilist/963000a9cc891189cfe3 to your computer and use it in GitHub Desktop.
US wind speed
/**
* Copyright (c) 2010, Jason Davies.
*
* All rights reserved. This code is based on Bradley White's Java version,
* which is in turn based on Nicholas Yue's C++ version, which in turn is based
* on Paul D. Bourke's original Fortran version. See below for the respective
* copyright notices.
*
* See http://local.wasp.uwa.edu.au/~pbourke/papers/conrec/ for the original
* paper by Paul D. Bourke.
*
* The vector conversion code is based on http://apptree.net/conrec.htm by
* Graham Cox.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the <organization> nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Copyright (c) 1996-1997 Nicholas Yue
*
* This software is copyrighted by Nicholas Yue. This code is based on Paul D.
* Bourke's CONREC.F routine.
*
* The authors hereby grant permission to use, copy, and distribute this
* software and its documentation for any purpose, provided that existing
* copyright notices are retained in all copies and that this notice is
* included verbatim in any distributions. Additionally, the authors grant
* permission to modify this software and its documentation for any purpose,
* provided that such modifications are not distributed without the explicit
* consent of the authors and that existing copyright notices are retained in
* all copies. Some of the algorithms implemented by this software are
* patented, observe all applicable patent law.
*
* IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT
* OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF,
* EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS
* PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO
* OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
* MODIFICATIONS.
*/
(function(exports) {
exports.Conrec = Conrec;
var EPSILON = 1e-10;
function pointsEqual(a, b) {
var x = a.x - b.x, y = a.y - b.y;
return x * x + y * y < EPSILON;
}
function reverseList(list) {
var pp = list.head;
while (pp) {
// swap prev/next pointers
var temp = pp.next;
pp.next = pp.prev;
pp.prev = temp;
// continue through the list
pp = temp;
}
// swap head/tail pointers
var temp = list.head;
list.head = list.tail;
list.tail = temp;
}
function ContourBuilder(level) {
this.level = level;
this.s = null;
this.count = 0;
}
ContourBuilder.prototype.remove_seq = function(list) {
// if list is the first item, static ptr s is updated
if (list.prev) {
list.prev.next = list.next;
} else {
this.s = list.next;
}
if (list.next) {
list.next.prev = list.prev;
}
--this.count;
}
ContourBuilder.prototype.addSegment = function(a, b) {
var ss = this.s;
var ma = null;
var mb = null;
var prependA = false;
var prependB = false;
while (ss) {
if (ma == null) {
// no match for a yet
if (pointsEqual(a, ss.head.p)) {
ma = ss;
prependA = true;
} else if (pointsEqual(a, ss.tail.p)) {
ma = ss;
}
}
if (mb == null) {
// no match for b yet
if (pointsEqual(b, ss.head.p)) {
mb = ss;
prependB = true;
} else if (pointsEqual(b, ss.tail.p)) {
mb = ss;
}
}
// if we matched both no need to continue searching
if (mb != null && ma != null) {
break;
} else {
ss = ss.next;
}
}
// c is the case selector based on which of ma and/or mb are set
var c = ((ma != null) ? 1 : 0) | ((mb != null) ? 2 : 0);
switch(c) {
case 0: // both unmatched, add as new sequence
var aa = {p: a, prev: null};
var bb = {p: b, next: null};
aa.next = bb;
bb.prev = aa;
// create sequence element and push onto head of main list. The order
// of items in this list is unimportant
ma = {head: aa, tail: bb, next: this.s, prev: null, closed: false};
if (this.s) {
this.s.prev = ma;
}
this.s = ma;
++this.count; // not essential - tracks number of unmerged sequences
break;
case 1: // a matched, b did not - thus b extends sequence ma
var pp = {p: b};
if (prependA) {
pp.next = ma.head;
pp.prev = null;
ma.head.prev = pp;
ma.head = pp;
} else {
pp.next = null;
pp.prev = ma.tail;
ma.tail.next = pp;
ma.tail = pp;
}
break;
case 2: // b matched, a did not - thus a extends sequence mb
var pp = {p: a};
if (prependB) {
pp.next = mb.head;
pp.prev = null;
mb.head.prev = pp;
mb.head = pp;
} else {
pp.next = null;
pp.prev = mb.tail;
mb.tail.next = pp;
mb.tail = pp;
}
break;
case 3: // both matched, can merge sequences
// if the sequences are the same, do nothing, as we are simply closing this path (could set a flag)
if (ma === mb) {
var pp = {p: ma.tail.p, next: ma.head, prev: null};
ma.head.prev = pp;
ma.head = pp;
ma.closed = true;
break;
}
// there are 4 ways the sequence pair can be joined. The current setting of prependA and
// prependB will tell us which type of join is needed. For head/head and tail/tail joins
// one sequence needs to be reversed
switch((prependA ? 1 : 0) | (prependB ? 2 : 0)) {
case 0: // tail-tail
// reverse ma and append to mb
reverseList(ma);
// fall through to head/tail case
case 1: // head-tail
// ma is appended to mb and ma discarded
mb.tail.next = ma.head;
ma.head.prev = mb.tail;
mb.tail = ma.tail;
//discard ma sequence record
this.remove_seq(ma);
break;
case 3: // head-head
// reverse ma and append mb to it
reverseList(ma);
// fall through to tail/head case
case 2: // tail-head
// mb is appended to ma and mb is discarded
ma.tail.next = mb.head;
mb.head.prev = ma.tail;
ma.tail = mb.tail;
//discard mb sequence record
this.remove_seq(mb);
break;
}
}
}
/**
* Implements CONREC.
*
* @param {function} drawContour function for drawing contour. Defaults to a
* custom "contour builder", which populates the
* contours property.
*/
function Conrec(drawContour) {
if (!drawContour) {
var c = this;
c.contours = {};
/**
* drawContour - interface for implementing the user supplied method to
* render the countours.
*
* Draws a line between the start and end coordinates.
*
* @param startX - start coordinate for X
* @param startY - start coordinate for Y
* @param endX - end coordinate for X
* @param endY - end coordinate for Y
* @param contourLevel - Contour level for line.
*/
this.drawContour = function(startX, startY, endX, endY, contourLevel, k) {
var cb = c.contours[k];
if (!cb) {
cb = c.contours[k] = new ContourBuilder(contourLevel);
}
cb.addSegment({x: startX, y: startY}, {x: endX, y: endY});
}
this.contourList = function() {
var l = [];
var a = c.contours;
for (var k in a) {
var s = a[k].s;
var level = a[k].level;
while (s) {
var h = s.head;
var l2 = [];
l2.level = level;
l2.k = k;
while (h && h.p) {
l2.push(h.p);
h = h.next;
}
l.push(l2);
s = s.next;
}
}
l.sort(function(a, b) { return a.k - b.k });
return l;
}
} else {
this.drawContour = drawContour;
}
this.h = new Array(5);
this.sh = new Array(5);
this.xh = new Array(5);
this.yh = new Array(5);
}
/**
* contour is a contouring subroutine for rectangularily spaced data
*
* It emits calls to a line drawing subroutine supplied by the user which
* draws a contour map corresponding to real*4data on a randomly spaced
* rectangular grid. The coordinates emitted are in the same units given in
* the x() and y() arrays.
*
* Any number of contour levels may be specified but they must be in order of
* increasing value.
*
*
* @param {number[][]} d - matrix of data to contour
* @param {number} ilb,iub,jlb,jub - index bounds of data matrix
*
* The following two, one dimensional arrays (x and y) contain
* the horizontal and vertical coordinates of each sample points.
* @param {number[]} x - data matrix column coordinates
* @param {number[]} y - data matrix row coordinates
* @param {number} nc - number of contour levels
* @param {number[]} z - contour levels in increasing order.
*/
Conrec.prototype.contour = function(d, ilb, iub, jlb, jub, x, y, nc, z) {
var h = this.h, sh = this.sh, xh = this.xh, yh = this.yh;
var drawContour = this.drawContour;
this.contours = {};
/** private */
var xsect = function(p1, p2){
return (h[p2]*xh[p1]-h[p1]*xh[p2])/(h[p2]-h[p1]);
}
var ysect = function(p1, p2){
return (h[p2]*yh[p1]-h[p1]*yh[p2])/(h[p2]-h[p1]);
}
var m1;
var m2;
var m3;
var case_value;
var dmin;
var dmax;
var x1 = 0.0;
var x2 = 0.0;
var y1 = 0.0;
var y2 = 0.0;
// The indexing of im and jm should be noted as it has to start from zero
// unlike the fortran counter part
var im = [0, 1, 1, 0];
var jm = [0, 0, 1, 1];
// Note that castab is arranged differently from the FORTRAN code because
// Fortran and C/C++ arrays are transposed of each other, in this case
// it is more tricky as castab is in 3 dimensions
var castab = [
[
[0, 0, 8], [0, 2, 5], [7, 6, 9]
],
[
[0, 3, 4], [1, 3, 1], [4, 3, 0]
],
[
[9, 6, 7], [5, 2, 0], [8, 0, 0]
]
];
for (var j=(jub-1);j>=jlb;j--) {
for (var i=ilb;i<=iub-1;i++) {
var temp1, temp2;
temp1 = Math.min(d[i][j],d[i][j+1]);
temp2 = Math.min(d[i+1][j],d[i+1][j+1]);
dmin = Math.min(temp1,temp2);
temp1 = Math.max(d[i][j],d[i][j+1]);
temp2 = Math.max(d[i+1][j],d[i+1][j+1]);
dmax = Math.max(temp1,temp2);
if (dmax>=z[0]&&dmin<=z[nc-1]) {
for (var k=0;k<nc;k++) {
if (z[k]>=dmin&&z[k]<=dmax) {
for (var m=4;m>=0;m--) {
if (m>0) {
// The indexing of im and jm should be noted as it has to
// start from zero
h[m] = d[i+im[m-1]][j+jm[m-1]]-z[k];
xh[m] = x[i+im[m-1]];
yh[m] = y[j+jm[m-1]];
} else {
h[0] = 0.25*(h[1]+h[2]+h[3]+h[4]);
xh[0]=0.5*(x[i]+x[i+1]);
yh[0]=0.5*(y[j]+y[j+1]);
}
if (h[m]>EPSILON) {
sh[m] = 1;
} else if (h[m]<-EPSILON) {
sh[m] = -1;
} else
sh[m] = 0;
}
//
// Note: at this stage the relative heights of the corners and the
// centre are in the h array, and the corresponding coordinates are
// in the xh and yh arrays. The centre of the box is indexed by 0
// and the 4 corners by 1 to 4 as shown below.
// Each triangle is then indexed by the parameter m, and the 3
// vertices of each triangle are indexed by parameters m1,m2,and
// m3.
// It is assumed that the centre of the box is always vertex 2
// though this isimportant only when all 3 vertices lie exactly on
// the same contour level, in which case only the side of the box
// is drawn.
//
//
// vertex 4 +-------------------+ vertex 3
// | \ / |
// | \ m-3 / |
// | \ / |
// | \ / |
// | m=2 X m=2 | the centre is vertex 0
// | / \ |
// | / \ |
// | / m=1 \ |
// | / \ |
// vertex 1 +-------------------+ vertex 2
//
//
//
// Scan each triangle in the box
//
for (m=1;m<=4;m++) {
m1 = m;
m2 = 0;
if (m!=4) {
m3 = m+1;
} else {
m3 = 1;
}
case_value = castab[sh[m1]+1][sh[m2]+1][sh[m3]+1];
if (case_value!=0) {
switch (case_value) {
case 1: // Line between vertices 1 and 2
x1=xh[m1];
y1=yh[m1];
x2=xh[m2];
y2=yh[m2];
break;
case 2: // Line between vertices 2 and 3
x1=xh[m2];
y1=yh[m2];
x2=xh[m3];
y2=yh[m3];
break;
case 3: // Line between vertices 3 and 1
x1=xh[m3];
y1=yh[m3];
x2=xh[m1];
y2=yh[m1];
break;
case 4: // Line between vertex 1 and side 2-3
x1=xh[m1];
y1=yh[m1];
x2=xsect(m2,m3);
y2=ysect(m2,m3);
break;
case 5: // Line between vertex 2 and side 3-1
x1=xh[m2];
y1=yh[m2];
x2=xsect(m3,m1);
y2=ysect(m3,m1);
break;
case 6: // Line between vertex 3 and side 1-2
x1=xh[m3];
y1=yh[m3];
x2=xsect(m1,m2);
y2=ysect(m1,m2);
break;
case 7: // Line between sides 1-2 and 2-3
x1=xsect(m1,m2);
y1=ysect(m1,m2);
x2=xsect(m2,m3);
y2=ysect(m2,m3);
break;
case 8: // Line between sides 2-3 and 3-1
x1=xsect(m2,m3);
y1=ysect(m2,m3);
x2=xsect(m3,m1);
y2=ysect(m3,m1);
break;
case 9: // Line between sides 3-1 and 1-2
x1=xsect(m3,m1);
y1=ysect(m3,m1);
x2=xsect(m1,m2);
y2=ysect(m1,m2);
break;
default:
break;
}
// Put your processing code here and comment out the printf
//printf("%f %f %f %f %f\n",x1,y1,x2,y2,z[k]);
drawContour(x1,y1,x2,y2,z[k],k);
}
}
}
}
}
}
}
}
})(typeof exports !== "undefined" ? exports : window);
<!doctype html>
<meta charset='utf-8'>
<style>
.beaufort--n0 {display:none;}
.beaufort--n1 {background-color:#cff;}
.beaufort--n2 {background-color:#9fc;}
.beaufort--n3 {background-color:#9f9;}
.beaufort--n4 {background-color:#9f6;}
.beaufort--n5 {background-color:#9f0;}
.beaufort--n6 {background-color:#cf0;}
.beaufort--n7 {background-color:#ff0;}
.beaufort--n8 {background-color:#fc0;}
.beaufort--n9 {background-color:#f90;}
.beaufort--n10 {background-color:#f60;}
.beaufort--n11 {background-color:#f30;}
.beaufort--n12 {background-color:#f00;}
</style>
<body>
<script src='http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js'></script>
<script src='conrec.js'></script>
<script>
var height = 600
var map_url = '/mbostock/raw/4090846/us.json'
var width = 960
// ****************************************************************************
function getDataUrl() {
var ts_begin = new Date()
ts_begin.setMinutes(ts_begin.getMinutes() - 1)
var ts_end = new Date()
var base = 'http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdXMLclient.php'
// per http://qr.ae/7Zohpm
var bounding_box = [-124.848974, 24.396308, -66.885444, 49.384358]
// http://graphical.weather.gov/xml/rest.php
var params = {
begin: ts_begin.toISOString().split('.')[0],
end: ts_end.toISOString().split('.')[0],
lat1: bounding_box[1],
lat2: bounding_box[3],
lon1: bounding_box[0],
lon2: bounding_box[2],
product: 'time-series',
// km
resolutionSub: 100.0,
wspd: 'wspd'
}
var out_params = _.map(params, function (v, k) { return k + '=' + v }).
join('&')
var url = base + '?' + out_params
// The AirNow servers don’t set CORS headers, so the XHR fails. Work around
// it by proxying through YQL: http://stackoverflow.com/a/8579158/672403
var yql_query = 'select * from xml where url="' + url + '"'
yql_query = encodeURIComponent(yql_query)
var proxied = 'http://query.yahooapis.com/v1/public/yql?q=' + yql_query +
'&format=json'
return proxied
}
// ****************************************************************************
function getContours(data) {
c = new Conrec()
var conrec_data = _.map(data, function (p) { return [p.lat, p.lng, p.value] })
// ###
console.log(conrec_data, conrec_data.length)
return data
}
function processData(data) {
var points = {}
var processed = {}
var root = data.dwml.data
_.each(root.location, function (location) {
points[location['location-key']] = {
lat: parseFloat(location.point.latitude),
lng: parseFloat(location.point.longitude)
}
})
_.each(root.parameters, function (parameter) {
var speed = parameter['wind-speed']
if (_.isUndefined(speed)) value = 0
else value = parseInt(speed.value, 10)
points[parameter['applicable-location']].value = value
})
return points
}
// ****************************************************************************
var svg = d3.select('body').append('svg').
attr('height', height).
attr('width', width)
var project = d3.geo.albersUsa().
scale(1000).
translate([width / 2, height / 2])
var path = d3.geo.path().
projection(project)
var quantize = d3.scale.quantize().
domain([0, 0.3, 3, 6.4, 10.6, 15.5, 21, 26.9, 33.4, 40.3, 47.6, 55.3, 63.4, 10000]).
range(d3.range(14))
function renderData(data) {
var contours = getContours(data)
var g = svg.append('g')
// ###
console.log('rendering:', contours)
g.selectAll('circle').
data(contours).
enter().
append('circle').
attr('cx', function (d) { return project([d.lng, d.lat])[0] }).
attr('cy', function (d) { return project([d.lng, d.lat])[1] }).
attr('r', 1).
attr('class', function (d) { return 'beaufort beaufort--n' + quantize(d.value) + ' raw--' + d.value })
}
function renderMap() {
var map = svg.append('path')
d3.json(map_url, function (error, data) {
if (error) throw error;
map.datum(topojson.feature(data, data.objects.land)).
attr('class', 'land').
attr('d', path)
})
}
function render(data) {
renderMap()
renderData(data)
d3.select(self.frameElement).style('height', height + 'px')
}
// ****************************************************************************
var data_url = getDataUrl()
var max_requests = 5
// First request usually passes YQL’s 5 second timeout, so allow re-requests.
function request(n) {
if (!n) n = 0
else if (n === max_requests) throw new Error('unable to get data')
console.log('attempt ' + (n + 1))
d3.json(data_url, function (response) {
var data = response.query.results
if (!data) {
request(n + 1)
return
}
var processed = processData(data)
render(processed)
})
}
request()
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment