From b14c266de3eec24418da58c1755da3447c66bfe3 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Tue, 9 Feb 2016 19:17:05 -0600 Subject: [PATCH] client side data models --- client/api/energy_data.js | 22 ++++++ client/api/houses.js | 0 client/api/power_data.js | 1 + client/models/energy_datum.js | 34 +++++++++ client/models/house.js | 125 ++++++++++++++++++++++++++++++++++ client/models/power_datum.js | 35 ++++++++++ client/style.js | 1 + client/style.scss | 16 +++++ server/views/layout.jade | 3 +- shared/models/house.js | 10 +++ shared/utils/array.js | 30 ++++++++ shared/utils/math.js | 22 ++++++ 12 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 client/api/energy_data.js create mode 100644 client/api/houses.js create mode 100644 client/api/power_data.js create mode 100644 client/models/energy_datum.js create mode 100644 client/models/house.js create mode 100644 client/models/power_datum.js create mode 100644 client/style.scss create mode 100644 shared/models/house.js create mode 100644 shared/utils/array.js diff --git a/client/api/energy_data.js b/client/api/energy_data.js new file mode 100644 index 0000000..871d581 --- /dev/null +++ b/client/api/energy_data.js @@ -0,0 +1,22 @@ +const ENDPOINT = '/data/v1/energy'; +import extend from 'extend'; + +class EnergyDataApi { + + static index(params){ + params = extend({ + + }, params); + return jQuery.ajax({ + url: `${ENDPOINT}`, + type: 'GET', + params: params, + dataType: 'json' + }).success((res)=>{ + return res.data; + }); + } + +} + +export default EnergyDataApi; diff --git a/client/api/houses.js b/client/api/houses.js new file mode 100644 index 0000000..e69de29 diff --git a/client/api/power_data.js b/client/api/power_data.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/client/api/power_data.js @@ -0,0 +1 @@ + diff --git a/client/models/energy_datum.js b/client/models/energy_datum.js new file mode 100644 index 0000000..a3d58ec --- /dev/null +++ b/client/models/energy_datum.js @@ -0,0 +1,34 @@ +import extend from 'extend'; + + +class EnergyDatum { + __constructor(data, house){ + var energy_datum = this; + energy_datum.house = house; + energy_datum.data = data; + moment.tz(data.day, house.data.timezone); + EnergyDatum.store[data.id] energy_datum; + } + + get day_to_date(){ + var energy_datum = this, + moment_tz = moment.tz(energy_datum.data.day, energy_datum.house.data.timezone); + // will have to do some additional math here to account for local offset. + return moment(moment_tz.toArray()).toDate(); + } + + update(data){ + var energy_datum = this, + house = power_datum.house; + if (data.day) data.day = moment.tz(data.day, house.data.timezone); + extend(energy_datum.data, data); + } + + static updateOrInitialize(id, data, house){ + var energy_datum = EnergyDatum.store.get(id); + if (energy_datum) energy_datum.update(data); + return energy_datum || new EnergyDatum(data, house) + } +} + +EnergyDatum.store = new Map(); diff --git a/client/models/house.js b/client/models/house.js new file mode 100644 index 0000000..d8a3499 --- /dev/null +++ b/client/models/house.js @@ -0,0 +1,125 @@ +import Api from './../api'; +import Store from './../store'; +import ArrayUtil from './../../shared/util/array' + +class House extends Base { + + __constructor(data, house){ + var energy_datum = this; + House.store.set(data.id, house); + house.data = data; + house.energy_data = new Map(); + house.power_data = new Map(); + } + + ensurePowerData(start_date, end_date){ + var house = this, + date_range = Array.from(house.power_data.keys()), + min_date = Math.min(date_range), + max_date = Math.max(date_range), + query_ranges, cache; + + if (date_range.length === 0) return house.getPowerData({dates: [[start_date, end_date]]}) + + query_ranges = MathUtil.minusRange([start_date, end_date], [min_date, max_date]); + + cache = ArrayUtil.selectMap(date_range, (datum_time)=>{ + return ArrayUtil.all(query_ranges, (query_range)=>{ + !MathUtil.inRange(datum_time, query_range); + })); + }, (datum_time)=>{ + return house.power_data.get(datum_time); + }); + + if (query_ranges.length > 0){ + return house.getPowerData({dates: dates}).then((new_power_data)=>{ + return new_power_data.concat(cache); + }); + } else return Promise.resolve(cache); + } + + getPowerData(params){ + var house = this; + params.house_id = house.data.id; + return Api.PowerData.index(params).then((power_data)=>{ + return power_data.map((power_datum_data)=>{ + var power_datum = Store.PowerDatum.updateOrInitialize(power_datum_data, house); + house.power_data.set(power_datum.data.time, power_datum); + return power_datum; + }); + }); + } + + ensureEnergyData(start_date, end_date){ + var house = this, + date_range = Array.from(house.energy_data.keys()), + min_date = Math.min(date_range), + max_date = Math.max(date_range), + query_ranges, cache; + + if (date_range.length === 0) return house.getEnergyData({dates: [[start_date, end_date]]}) + + query_ranges = MathUtil.minusRange([start_date, end_date], [min_date, max_date]); + + cache = ArrayUtil.selectMap(date_range, (datum_day)=>{ + return ArrayUtil.all(query_ranges, (query_range)=>{ + !MathUtil.inRange(datum_day, query_range); + })); + }, (datum_day)=>{ + return house.energy_data.get(datum_day); + }); + + if (query_ranges.length > 0){ + return house.getEnergyData({dates: dates}).then((new_energy_data)=>{ + return new_energy_data.concat(cache); + }); + } else return Promise.resolve(cache); + } + + getEnergyData(params){ + var house = this; + params.house_id = house.data.id; + return Api.PowerData.index(params).then((energy_data)=>{ + return power_data.map((energy_datum_data)=>{ + var energy_datum = Store.EnergyDatum.updateOrInitialize(energy_datum_data, house); + house.energy_data.set(power_datum.time, energy_datum); + return energy_datum; + }); + }); + } + + update(data){ + var house = this; + extend(house.data, data); + } + + static updateOrInitialize(id, data){ + var house = PowerDatum.store.get(id); + if (house) house.update(data); + return house || new House(data, data) + } + + static ensureHouses(ids){ + var required_ids = ArrayUtil.diff(ids, House.store.keys()), + cached_houses = ArrayUtil.diff(ids, required_ids).map((id)=>{ return House.store.get(id); }); + if (required_ids.length == 0) return Promise.resolve([]); + + return House.getHouses(required_ids).then((new_houses){ + // if these need to be ordered, then concatenation needs to be merged in order. + return new_houses.concat(cached_houses); + }); + } + + static getHouses(ids){ + return Api.HousesApi.index({id: ids}).then((houses_data)=>{ + return houses_data.map((house_data)=>{ + return new House(house_data); + }); + }); + } + +} + +House.store = new Map(); + +export default House; diff --git a/client/models/power_datum.js b/client/models/power_datum.js new file mode 100644 index 0000000..bd4ac26 --- /dev/null +++ b/client/models/power_datum.js @@ -0,0 +1,35 @@ +import extend from 'extend'; + + +class PowerDatum { + __constructor(data, house){ + var power_datum = this; + power_datum.house = house; + power_datum.data = data; + moment.format(data.time); + PowerDatum.store[data.id] power_datum; + } + + get time_to_date(){ + var power_datum = this, + moment_tz = moment.tz(power_datum.data.time, power_datum.house.data.timezone); + // will have to do some additional math here to account for local offset. + return moment(moment_tz.toArray()).toDate(); + } + + update(data){ + var power_datum = this, + house = power_datum.house; + if (data.time) data.time = moment.tz(data.time, house.data.timezone); + extend(power_datum.data, data); + } + + static updateOrInitialize(id, data, house){ + var power_datum = PowerDatum.store.get(id); + if (power_datum) power_datum.update(data); + return power_datum || new PowerDatum(data, house) + } + +} + +PowerDatum.store = new Map(); diff --git a/client/style.js b/client/style.js index bbe7490..8fbe045 100644 --- a/client/style.js +++ b/client/style.js @@ -4,4 +4,5 @@ require('font-awesome/css/font-awesome.min.css'); // Component Stylesheets +require(__dirname + '/style.scss'); require(__dirname + '/dashboard/layout/layout.scss'); diff --git a/client/style.scss b/client/style.scss new file mode 100644 index 0000000..df9f399 --- /dev/null +++ b/client/style.scss @@ -0,0 +1,16 @@ +html, body { + height:100%; +} +#spike_container { + min-height: 100%; + position:relative; + padding-bottom:100px; +} +#spike_footer { + width:100%; + padding:15px; + position:absolute; + bottom:0px; + border-top:2px solid darkgrey; + background-color:#F8F8F8; +} diff --git a/server/views/layout.jade b/server/views/layout.jade index 6c9f795..3dc3b6b 100644 --- a/server/views/layout.jade +++ b/server/views/layout.jade @@ -21,7 +21,8 @@ html ul.nav.navbar-nav.navbar-right li a(href='/') Spike - block content + .container + block content #spike_footer .container Footer script(type='text/javascript'). diff --git a/shared/models/house.js b/shared/models/house.js new file mode 100644 index 0000000..8910627 --- /dev/null +++ b/shared/models/house.js @@ -0,0 +1,10 @@ +import moment from 'moment'; + +class House { + + timeToDateString(timestamp){ + var house = this; + return moment.tz(timestamp, house.timezone).format("YYYY-MM-DD"); + } + +} diff --git a/shared/utils/array.js b/shared/utils/array.js new file mode 100644 index 0000000..24dc1a9 --- /dev/null +++ b/shared/utils/array.js @@ -0,0 +1,30 @@ +class ArrayUtil { + + static diff(a1, a2){ + a1.filter((a1n)=>{ return a2.indexOf(a1n) < 0; }); + } + + static selectMap(a, fnSelect, fnMap){ + var map = []; + for (var elem of a){ + if (fnSelect(elem)){ + map.push(fnMap(elem)); + } + } + return map; + } + + static all(a, fnCondition){ + var all = true; + for (var elem of a){ + if (!fnCondition(elem)){ + all = false; + break; + } + } + return all; + } + +} + +export default ArrayUtil; diff --git a/shared/utils/math.js b/shared/utils/math.js index ad8d608..802ee37 100644 --- a/shared/utils/math.js +++ b/shared/utils/math.js @@ -7,4 +7,26 @@ export default class { static n6(){ return ((Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random()) - 3) / 3; } + + // min_max1 and min_max2 arrays of length two representing mins and maxes of their ranges; + // returns array of array length two, representing mins and maxes not within min_max2. + static minusRange(min_max1, min_max2){ + var minus = []; + if (min_max1[0] >= min_max2[0]){ + if (min_max1[1] > min_max2[1]) minus.push([min_max2[1], min_max1[1]]); + } else if (min_max1[1] <= min_max2[1]){ + if (min_max1[0] < min_max2[0]) minus.push([min_max1[0], min_max2[0]]); + } else if (min_max1[0] < min_max2[0] && min_max1[1] > min_max2[1]){ + minus.push([min_max1[0], min_max2[0]]); + minus.push([min_max2[1], min_max1[1]]); + } else { + minus.push([min_max1[0], min_max1[1]]); + } + return minus; + } + + static inRange(n, min_max){ + return n >= min_max[0] && n =< min_max[1]; + } + }