diff --git a/css/dashboard.css b/css/dashboard.css index bf505d4..52e938a 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -417,7 +417,9 @@ input[type=radio] { .icon.alarm_snooze:before { content: "\F068E"; } .icon.alpha_b:before { content: "\F0AEF"; } .icon.alpha_b_box:before { content: "\F0B09"; } +.icon.arrow_expand_horizontal:before { content: "\F084E"; } .icon.arrow_expand_vertical:before { content: "\F084F"; } +.icon.arrow_horizontal_lock:before { content: "\F115B"; } .icon.arrow_left:before { content: "\F004D"; } .icon.basket_fill:before { content: "\F0077"; } .icon.basket_unfill:before { content: "\F0078"; } @@ -429,7 +431,8 @@ input[type=radio] { .icon.calendar:before { content: "\F00ED"; } .icon.calendar_alert:before { content: "\F0A31"; } .icon.calendar_edit:before { content: "\F08A7"; } -.icon.calendar_range:before { content: "\F0679"; } +.icon.calendar_month:before { content: "\F0E17"; } +.icon.calendar_week:before { content: "\F0A33"; } .icon.cancel:before { content: "\F073A"; } .icon.card_plus_outline:before { content: "\F1200"; } .icon.card_remove_outline:before { content: "\F1605"; } @@ -453,6 +456,7 @@ input[type=radio] { .icon.eye_circle:before { content: "\F0B94"; } .icon.eye_off:before { content: "\F0209"; } .icon.fan:before { content: "\F0210"; } +.icon.fast_forward:before { content: "\F0211"; } .icon.fire:before { content: "\F0238"; } .icon.floor_plan:before { content: "\F0821"; } .icon.google_play:before { content: "\F02BC"; } @@ -501,7 +505,9 @@ input[type=radio] { .icon.numeric_9_box:before { content: "\F03BC"; } .icon.open_in_new:before { content: "\F03CC"; } .icon.patreon:before { content: "\F0882"; } +.icon.pause:before { content: "\F03E4"; } .icon.pencil:before { content: "\F03EB"; } +.icon.play:before { content: "\F040A"; } .icon.plus:before { content: "\F0415"; } .icon.pound:before { content: "\F0423"; } .icon.redo:before { content: "\F044E"; } @@ -509,6 +515,7 @@ input[type=radio] { .icon.resistor:before { content: "\F0B44"; } .icon.snowflake:before { content: "\F0717"; } .icon.swap_horizontal:before { content: "\F04E1"; } +.icon.thermometer:before { content: "\F050F"; } .icon.thermostat:before { content: "\F0393"; } .icon.thumb_up:before { content: "\F0513"; } .icon.tune:before { content: "\F062E"; } diff --git a/js/beestat.js b/js/beestat.js index 79d2b80..e820e2d 100644 --- a/js/beestat.js +++ b/js/beestat.js @@ -87,6 +87,7 @@ window.addEventListener('resize', rocket.throttle(100, function() { // First run var $ = rocket.extend(rocket.$, rocket); $.ready(function() { + moment.suppressDeprecationWarnings = true; if (window.environment === 'live') { Sentry.init({ 'dsn': 'https://af9fd2cf6cda49dcb93dcaf02fe39fc6@sentry.io/3736982', diff --git a/js/beestat/floor_plan.js b/js/beestat/floor_plan.js index bf533d7..9338e84 100644 --- a/js/beestat/floor_plan.js +++ b/js/beestat/floor_plan.js @@ -57,3 +57,84 @@ beestat.floor_plan.get_area_room = function(room, round = true) { return area; }; + +beestat.floor_plan.get_bounding_box = function(floor_plan_id) { + const floor_plan = beestat.cache.floor_plan[floor_plan_id]; + + let min_x = Infinity; + let max_x = -Infinity; + let min_y = Infinity; + let max_y = -Infinity; + + floor_plan.data.groups.forEach(function(group) { + const bounding_box = beestat.floor_plan.get_bounding_box_group(group); + + min_x = Math.min(bounding_box.left, min_x); + max_x = Math.max(bounding_box.right, max_x); + min_y = Math.min(bounding_box.top, min_y); + max_y = Math.max(bounding_box.bottom, max_y); + }); + + return { + 'width': max_x - min_x, + 'height': max_y - min_y, + 'left': min_x, + 'top': min_y, + 'right': max_x, + 'bottom': max_y, + 'x': min_x, + 'y': min_y + }; +}; + +beestat.floor_plan.get_bounding_box_group = function(group) { + let min_x = Infinity; + let max_x = -Infinity; + let min_y = Infinity; + let max_y = -Infinity; + + group.rooms.forEach(function(room) { + const bounding_box = beestat.floor_plan.get_bounding_box_room(room); + + min_x = Math.min(bounding_box.left, min_x); + max_x = Math.max(bounding_box.right, max_x); + min_y = Math.min(bounding_box.top, min_y); + max_y = Math.max(bounding_box.bottom, max_y); + }); + + return { + 'width': max_x - min_x, + 'height': max_y - min_y, + 'left': min_x, + 'top': min_y, + 'right': max_x, + 'bottom': max_y, + 'x': min_x, + 'y': min_y + }; +}; + +beestat.floor_plan.get_bounding_box_room = function(room) { + let min_x = Infinity; + let max_x = -Infinity; + let min_y = Infinity; + let max_y = -Infinity; + + room.points.forEach(function(point) { + min_x = Math.min(room.x + point.x, min_x); + max_x = Math.max(room.x + point.x, max_x); + min_y = Math.min(room.y + point.y, min_y); + max_y = Math.max(room.y + point.y, max_y); + }); + + return { + 'width': max_x - min_x, + 'height': max_y - min_y, + 'left': min_x, + 'top': min_y, + 'right': max_x, + 'bottom': max_y, + 'x': min_x, + 'y': min_y + }; +}; diff --git a/js/beestat/runtime_sensor.js b/js/beestat/runtime_sensor.js index e2cc9f6..4ab9101 100644 --- a/js/beestat/runtime_sensor.js +++ b/js/beestat/runtime_sensor.js @@ -4,7 +4,7 @@ beestat.runtime_sensor = {}; * Get a bunch of data for the current runtime_sensor rows. Includes basically * everything you need to make a cool chart. * - * @param {number} thermostat_id The thermostat_id to get data for. + * @param {number} sensor_ids The sensor_ids to get data for. * @param {object} range Range settings. * @param {string} key The key to pull the data from inside * beestat.cache.data. This exists because runtime_sensor data exists in @@ -12,7 +12,7 @@ beestat.runtime_sensor = {}; * * @return {object} The data. */ -beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { +beestat.runtime_sensor.get_data = function(sensor_ids, range, key) { var data = { 'x': [], 'series': {}, @@ -51,30 +51,28 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { beestat.style.color.lightblue.dark ]; - // Duration objects. These are passed by reference into the metadata. - var durations = {}; - var series_codes = []; // Get and sort all the sensors. - var sensors = beestat.sensor.get_sorted(); - data.metadata.sensors = sensors; + data.metadata.sensors = []; // Set up the series_codes. const sensor_series_colors = {}; - sensors.forEach(function(sensor, i) { - if (sensor.thermostat_id === thermostat_id) { - series_codes.push('temperature_' + sensor.sensor_id); - series_codes.push('occupancy_' + sensor.sensor_id); + sensor_ids.forEach(function(sensor_id, i) { + const sensor = beestat.cache.sensor[sensor_id]; - sensor_series_colors[sensor.sensor_id] = colors[i]; + series_codes.push('temperature_' + sensor.sensor_id); + series_codes.push('occupancy_' + sensor.sensor_id); - if (sensor.type === 'thermostat') { - series_codes.push('air_quality_' + sensor.sensor_id); - series_codes.push('voc_concentration_' + sensor.sensor_id); - series_codes.push('co2_concentration_' + sensor.sensor_id); - } + sensor_series_colors[sensor.sensor_id] = colors[i]; + + if (sensor.type === 'thermostat') { + series_codes.push('air_quality_' + sensor.sensor_id); + series_codes.push('voc_concentration_' + sensor.sensor_id); + series_codes.push('co2_concentration_' + sensor.sensor_id); } + + data.metadata.sensors.push(sensor); }); series_codes.push('dummy'); @@ -87,7 +85,6 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { data.series[series_code] = []; data.metadata.series[series_code] = { 'active': false, - 'durations': {}, 'data': {} }; if (series_code === 'dummy') { @@ -103,8 +100,6 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { data.metadata.series[series_code].name = beestat.cache.sensor[sensor_id].name; data.metadata.series[series_code].color = sensor_series_colors[sensor_id]; } - - durations[series_code] = {'seconds': 0}; }); var begin_m; @@ -141,7 +136,7 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { data.metadata.series.dummy.active = true; if (runtime_sensors[current_m.valueOf()] !== undefined) { - sensors.forEach(function(sensor, j) { + data.metadata.sensors.forEach(function(sensor, j) { var runtime_sensor = runtime_sensors[current_m.valueOf()][sensor.sensor_id]; if (runtime_sensor === undefined) { data.series['temperature_' + sensor.sensor_id].push(null); @@ -177,7 +172,7 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { if (runtime_sensor.occupancy === true) { let swimlane_properties = beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties( - sensors.length, + data.metadata.sensors.length, j ); @@ -188,7 +183,7 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { if (sequential['occupancy_' + runtime_sensor.sensor_id] > 0) { let swimlane_properties = beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties( - sensors.length, + data.metadata.sensors.length, j ); @@ -201,16 +196,14 @@ beestat.runtime_sensor.get_data = function(thermostat_id, range, key) { } }); } else { - sensors.forEach(function(sensor) { - if (sensor.thermostat_id === thermostat_id) { - data.series['temperature_' + sensor.sensor_id].push(null); - data.series['occupancy_' + sensor.sensor_id].push(null); + data.metadata.sensors.forEach(function(sensor) { + data.series['temperature_' + sensor.sensor_id].push(null); + data.series['occupancy_' + sensor.sensor_id].push(null); - if (sensor.type === 'thermostat') { - data.series['air_quality_' + sensor.sensor_id].push(null); - data.series['voc_concentration_' + sensor.sensor_id].push(null); - data.series['co2_concentration_' + sensor.sensor_id].push(null); - } + if (sensor.type === 'thermostat') { + data.series['air_quality_' + sensor.sensor_id].push(null); + data.series['voc_concentration_' + sensor.sensor_id].push(null); + data.series['co2_concentration_' + sensor.sensor_id].push(null); } }); } diff --git a/js/beestat/sensor.js b/js/beestat/sensor.js deleted file mode 100644 index cc247f9..0000000 --- a/js/beestat/sensor.js +++ /dev/null @@ -1,24 +0,0 @@ -beestat.sensor = {}; - -/** - * Get a sorted list of all sensors attached to the current thermostat. - * - * @param {number} thermostat_id Thermostat to get this list for. - * - * @return {array} The sensors. - */ -beestat.sensor.get_sorted = function(thermostat_id) { - // Get and sort all the sensors. - const sensors = []; - Object.values(beestat.cache.sensor).forEach(function(sensor) { - if (sensor.thermostat_id === (thermostat_id || beestat.setting('thermostat_id'))) { - sensors.push(sensor); - } - }); - - sensors.sort(function(a, b) { - return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'}); - }); - - return sensors; -}; diff --git a/js/beestat/setting.js b/js/beestat/setting.js index c0ea89d..e9fd467 100644 --- a/js/beestat/setting.js +++ b/js/beestat/setting.js @@ -68,7 +68,21 @@ beestat.setting = function(argument_1, opt_value, opt_callback) { 'thermostat.#.profile.ignore_solar_gain': false, - 'floor_plan_id': null + 'visualize.data_type': 'temperature', + 'visualize.range_type': 'dynamic', + 'visualize.range_dynamic': 7, + 'visualize.range_static.begin': moment() + .subtract(3, 'day') + .format('MM/DD/YYYY'), + 'visualize.range_static.end': moment() + .format('MM/DD/YYYY'), + 'visualize.floor_plan_id': null, + 'visualize.heat_map_type': 'relative', + 'visualize.heat_map_absolute.temperature.min': 70, + 'visualize.heat_map_absolute.temperature.max': 80, + 'visualize.heat_map_absolute.occupancy.min': 0, + 'visualize.heat_map_absolute.occupancy.max': 100 + }; // Figure out what we're trying to do. diff --git a/js/beestat/style.js b/js/beestat/style.js index 7d9165b..c5dd717 100644 --- a/js/beestat/style.js +++ b/js/beestat/style.js @@ -339,3 +339,61 @@ beestat.series.co2_concentration = { 'name': 'CO₂', 'color': beestat.style.color.blue.base }; + +/** + * Generate a number of colors between two points. + * + * @param {Object} colors Array of colors in RGB. + * @param {number} steps Number of colors to generate. + * + * @see http://forums.codeguru.com/showthread.php?259953-Code-to-create-Color-Gradient-programatically&s=4710043a327ee6059da1f8433ad1e5d2&p=795289#post795289 + * + * @return {Array.} RGB color array + */ +beestat.style.generate_gradient = function(colors, steps) { + const gradient_count = colors.length - 1; + + let gradient = []; + for (let j = 0; j < gradient_count; j++) { + gradient = gradient.concat(this.generate_gradient_( + colors[j], + colors[j + 1], + steps + )); + } + + return gradient; +}; + +beestat.style.generate_gradient_ = function(begin, end, steps) { + var gradient = []; + for (var i = 0; i < steps; i++) { + var n = i / (steps - 1); + gradient.push({ + 'r': Math.round(begin.r * (1 - n) + end.r * n), + 'g': Math.round(begin.g * (1 - n) + end.g * n), + 'b': Math.round(begin.b * (1 - n) + end.b * n) + }); + } + return gradient; +}; + +/** + * Convert a hex string to RGB components. + * + * @param {string} hex + * + * @return {object} RGB + */ +beestat.style.hex_to_rgb = function(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + 'r': parseInt(result[1], 16), + 'g': parseInt(result[2], 16), + 'b': parseInt(result[3], 16) + } : null; +}; + +beestat.style.rgb_to_hex = function(rgb) { + return "#" + ((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1); +} diff --git a/js/component/card.js b/js/component/card.js index 44ce2e9..6bb37d5 100644 --- a/js/component/card.js +++ b/js/component/card.js @@ -9,33 +9,10 @@ beestat.extend(beestat.component.card, beestat.component); beestat.component.card.prototype.box_shadow_ = true; /** - * [get_class_name_recursive_ description] + * Decorate * - * @param {[type]} parent [description] - * @param {[type]} opt_prefix [description] - * - * @return {[type]} [description] + * @param {rocket.Elements} parent */ -beestat.component.card.prototype.get_class_name_recursive_ = function(parent, opt_prefix) { - for (var i in parent) { - if ( - (parent[i]) && - (parent[i].prototype) && - (this instanceof parent[i]) - ) { - var name = opt_prefix ? rocket.clone(opt_prefix) : []; - name.push(i); - if (parent[i] === this.constructor) { - return name; - } - name = this.get_class_name_recursive_(parent[i], name); - if (name) { - return name; - } - } - } -}; - beestat.component.card.prototype.decorate_ = function(parent) { this.hide_loading_(); diff --git a/js/component/card/air_quality_detail.js b/js/component/card/air_quality_detail.js index 752d5fb..36acbea 100644 --- a/js/component/card/air_quality_detail.js +++ b/js/component/card/air_quality_detail.js @@ -25,8 +25,6 @@ beestat.component.card.air_quality_detail = function(thermostat_id) { beestat.dispatcher.addEventListener( [ - 'setting.air_quality_detail_range_type', - 'setting.air_quality_detail_range_dynamic', 'cache.data.air_quality_detail__runtime_thermostat', 'cache.data.air_quality_detail__runtime_sensor' ], @@ -79,8 +77,6 @@ beestat.component.card.air_quality_detail.prototype.decorate_contents_ = functio chart_container.appendChild($.createElement('p').innerText('CO₂ Concentration (ppm)')); this.charts_.co2_concentration.render(chart_container); - // this.charts_.x_axis.render(chart_container); - // Sync extremes and crosshair. Object.values(this.charts_).forEach(function(source_chart) { Object.values(self.charts_).forEach(function(target_chart) { @@ -149,7 +145,7 @@ beestat.component.card.air_quality_detail.prototype.decorate_contents_ = functio } var api_call = new beestat.api(); - beestat.sensor.get_sorted().forEach(function(sensor) { + Object.values(beestat.cache.sensor).forEach(function(sensor) { if (sensor.thermostat_id === self.thermostat_id_) { api_call.add_call( 'runtime_sensor', @@ -347,6 +343,8 @@ beestat.component.card.air_quality_detail.prototype.has_data_ = function() { * @return {object} The data. */ beestat.component.card.air_quality_detail.prototype.get_data_ = function(force) { + const self = this; + if (this.data_ === undefined || force === true) { var range = { 'type': beestat.setting('air_quality_detail_range_type'), @@ -356,7 +354,10 @@ beestat.component.card.air_quality_detail.prototype.get_data_ = function(force) }; var sensor_data = beestat.runtime_sensor.get_data( - this.thermostat_id_, + Object.values(beestat.cache.sensor).filter(function(sensor) { + return sensor.thermostat_id === self.thermostat_id_; + }) + .map(sensor => sensor.sensor_id), range, 'air_quality_detail__runtime_sensor' ); diff --git a/js/component/card/air_quality_summary.js b/js/component/card/air_quality_summary.js index 88a8262..56532d2 100644 --- a/js/component/card/air_quality_summary.js +++ b/js/component/card/air_quality_summary.js @@ -115,7 +115,7 @@ beestat.component.card.air_quality_summary.prototype.decorate_contents_ = functi } var api_call = new beestat.api(); - beestat.sensor.get_sorted().forEach(function(sensor) { + Object.values(beestat.cache.sensor).forEach(function(sensor) { if ( sensor.thermostat_id === self.thermostat_id_ && sensor.type === 'thermostat' diff --git a/js/component/card/comparison_settings.js b/js/component/card/comparison_settings.js index 4b698c0..65b68c3 100644 --- a/js/component/card/comparison_settings.js +++ b/js/component/card/comparison_settings.js @@ -151,7 +151,7 @@ beestat.component.card.comparison_settings.prototype.decorate_region_ = function var color = beestat.style.color.green.base; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); regions.forEach(function(region) { var button = new beestat.component.tile() .set_background_hover_color(color) @@ -192,9 +192,9 @@ beestat.component.card.comparison_settings.prototype.decorate_region_ = function }); } - button_group.add_button(button); + tile_group.add_tile(button); }); - button_group.render(parent); + tile_group.render(parent); }; /** @@ -233,7 +233,7 @@ beestat.component.card.comparison_settings.prototype.decorate_property_ = functi var color = beestat.style.color.purple.base; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); property_types.forEach(function(property_type) { var button = new beestat.component.tile() .set_background_hover_color(color) @@ -274,9 +274,9 @@ beestat.component.card.comparison_settings.prototype.decorate_property_ = functi }); } - button_group.add_button(button); + tile_group.add_tile(button); }); - button_group.render(parent); + tile_group.render(parent); }; beestat.component.card.comparison_settings.prototype.decorate_detail_ = function(parent) { diff --git a/js/component/card/floor_plan_editor.js b/js/component/card/floor_plan_editor.js index f5668f4..fd12c22 100644 --- a/js/component/card/floor_plan_editor.js +++ b/js/component/card/floor_plan_editor.js @@ -8,7 +8,7 @@ beestat.component.card.floor_plan_editor = function(thermostat_id) { this.thermostat_id_ = thermostat_id; - var change_function = beestat.debounce(function() { + const change_function = beestat.debounce(function() { // todo replace these with (if entity set active false?) delete self.state_.active_group; @@ -21,10 +21,7 @@ beestat.component.card.floor_plan_editor = function(thermostat_id) { }, 10); beestat.dispatcher.addEventListener( - [ - 'setting.floor_plan_id', - 'cache.floor_plan' - ], + 'setting.visualize.floor_plan_id', change_function ); @@ -71,7 +68,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function }); center_container.appendChild(get_started_button); } else { - const floor_plan = beestat.cache.floor_plan[beestat.setting('floor_plan_id')]; + const floor_plan = beestat.cache.floor_plan[beestat.setting('visualize.floor_plan_id')]; // Set group ids if they are not set. floor_plan.data.groups.forEach(function(group) { @@ -138,7 +135,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func // Create and render a new SVG component. this.floor_plan_ = new beestat.component.floor_plan( - beestat.setting('floor_plan_id'), + beestat.setting('visualize.floor_plan_id'), this.state_ ); @@ -467,9 +464,13 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu const sensors = {}; Object.values(beestat.cache.thermostat).forEach(function(thermostat) { - const thermostat_sensors = beestat.sensor.get_sorted( - thermostat.thermostat_id - ); + const thermostat_sensors = Object.values(beestat.cache.sensor).filter(function(sensor) { + return sensor.thermostat_id === self.thermostat_id_; + }) + .sort(function(a, b) { + return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'}); + }); + sensors[thermostat.thermostat_id] = thermostat_sensors; }); @@ -526,20 +527,6 @@ beestat.component.card.floor_plan_editor.prototype.get_title_ = function() { return 'Floor Plan'; }; -/** - * Get the subtitle of the card. - * - * @return {string} Subtitle - */ -beestat.component.card.floor_plan_editor.prototype.get_subtitle_ = function() { - if (beestat.setting('floor_plan_id') !== null) { - const floor_plan = beestat.cache.floor_plan[beestat.setting('floor_plan_id')]; - return floor_plan.name; - } - - return null; -}; - /** * Update the floor plan in the database. This is throttled so the update can * only run so fast. @@ -547,6 +534,9 @@ beestat.component.card.floor_plan_editor.prototype.get_subtitle_ = function() { beestat.component.card.floor_plan_editor.prototype.update_floor_plan_ = function() { const self = this; + // Fake this event since the cache is being directly modified. + beestat.dispatcher.dispatchEvent('cache.floor_plan'); + window.clearTimeout(this.update_timeout_); this.update_timeout_ = window.setTimeout(function() { new beestat.api() @@ -555,8 +545,8 @@ beestat.component.card.floor_plan_editor.prototype.update_floor_plan_ = function 'update', { 'attributes': { - 'floor_plan_id': beestat.setting('floor_plan_id'), - 'data': self.get_floor_plan_data_(beestat.setting('floor_plan_id')) + 'floor_plan_id': beestat.setting('visualize.floor_plan_id'), + 'data': self.get_floor_plan_data_(beestat.setting('visualize.floor_plan_id')) } }, 'update_floor_plan' @@ -566,22 +556,14 @@ beestat.component.card.floor_plan_editor.prototype.update_floor_plan_ = function }; /** - * Get floor plan data with UUIDs stripped. + * Get cloned floor plan data. * * @param {number} floor_plan_id Floor plan ID * * @return {object} The modified floor plan data. */ beestat.component.card.floor_plan_editor.prototype.get_floor_plan_data_ = function(floor_plan_id) { - const floor_plan = beestat.cache.floor_plan[floor_plan_id]; - const data = beestat.clone(floor_plan.data); - data.groups.forEach(function(group) { - delete group.group_id; - group.rooms.forEach(function(room) { - delete room.room_id; - }); - }); - return data; + return beestat.clone(beestat.cache.floor_plan[floor_plan_id].data); }; /** @@ -592,7 +574,7 @@ beestat.component.card.floor_plan_editor.prototype.get_floor_plan_data_ = functi beestat.component.card.floor_plan_editor.prototype.decorate_top_right_ = function(parent) { const self = this; - var menu = (new beestat.component.menu()).render(parent); + const menu = (new beestat.component.menu()).render(parent); if (window.is_demo === false) { menu.add_menu_item(new beestat.component.menu_item() @@ -604,33 +586,24 @@ beestat.component.card.floor_plan_editor.prototype.decorate_top_right_ = functio ).render(); })); - if (Object.keys(beestat.cache.floor_plan).length > 1) { - menu.add_menu_item(new beestat.component.menu_item() - .set_text('Switch') - .set_icon('swap_horizontal') - .set_callback(function() { - (new beestat.component.modal.change_floor_plan()).render(); - })); - } - - if (beestat.setting('floor_plan_id') !== null) { + if (beestat.setting('visualize.floor_plan_id') !== null) { menu.add_menu_item(new beestat.component.menu_item() .set_text('Edit') .set_icon('pencil') .set_callback(function() { new beestat.component.modal.update_floor_plan( - beestat.setting('floor_plan_id') + beestat.setting('visualize.floor_plan_id') ).render(); })); } - if (beestat.setting('floor_plan_id') !== null) { + if (beestat.setting('visualize.floor_plan_id') !== null) { menu.add_menu_item(new beestat.component.menu_item() .set_text('Delete') .set_icon('delete') .set_callback(function() { new beestat.component.modal.delete_floor_plan( - beestat.setting('floor_plan_id') + beestat.setting('visualize.floor_plan_id') ).render(); })); } diff --git a/js/component/card/my_home.js b/js/component/card/my_home.js index 3da7b63..b877ad3 100644 --- a/js/component/card/my_home.js +++ b/js/component/card/my_home.js @@ -67,27 +67,27 @@ beestat.component.card.my_home.prototype.decorate_system_type_ = function(parent ); const cool_stages_string = cool_stages > 1 ? ' (2 Stage)' : ''; - var button_group = new beestat.component.tile_group(); - button_group.add_button(new beestat.component.tile() + var tile_group = new beestat.component.tile_group(); + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.series.compressor_heat_1.color) .set_text_color('#fff') .set_icon('fire') .set_text(heat.charAt(0).toUpperCase() + heat.slice(1) + heat_stages_string)); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.series.auxiliary_heat_1.color) .set_text_color('#fff') .set_icon('fire') .set_text(auxiliary_heat.charAt(0).toUpperCase() + auxiliary_heat.slice(1))); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.series.compressor_cool_1.color) .set_text_color('#fff') .set_icon('snowflake') .set_text(cool.charAt(0).toUpperCase() + cool.slice(1) + cool_stages_string)); - button_group.render(parent); + tile_group.render(parent); }; /** @@ -117,7 +117,7 @@ beestat.component.card.my_home.prototype.decorate_region_ = function(parent) { region = null; } - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); if (region !== null) { var button = new beestat.component.tile() .set_type('pill') @@ -125,16 +125,16 @@ beestat.component.card.my_home.prototype.decorate_region_ = function(parent) { .set_text_color('#fff') .set_icon('map_marker') .set_text(region); - button_group.add_button(button); + tile_group.add_tile(button); } else { - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.gray.dark) .set_text_color('#fff') .set_icon('border_none_variant') .set_text('No Data')); } - button_group.render(parent); + tile_group.render(parent); }; /** @@ -147,10 +147,12 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { (new beestat.component.title('Property')).render(parent); - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); + let has_data = false; if (thermostat.property.structure_type !== null) { - button_group.add_button(new beestat.component.tile() + has_data = true; + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.purple.base) .set_text_color('#fff') @@ -167,7 +169,8 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { thermostat.property.structure_type === 'semi-detached' ) ) { - button_group.add_button(new beestat.component.tile() + has_data = true; + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.purple.base) .set_text_color('#fff') @@ -177,7 +180,8 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { } if (thermostat.property.square_feet !== null) { - button_group.add_button(new beestat.component.tile() + has_data = true; + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.purple.base) .set_text_color('#fff') @@ -186,7 +190,8 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { } if (thermostat.property.age !== null) { - button_group.add_button(new beestat.component.tile() + has_data = true; + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.purple.base) .set_text_color('#fff') @@ -194,8 +199,8 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { .set_text(thermostat.property.age + ' Years')); } - if (button_group.get_buttons().length === 0) { - button_group.add_button(new beestat.component.tile() + if (has_data === false) { + tile_group.add_tile(new beestat.component.tile() .set_type('pill') .set_background_color(beestat.style.color.gray.dark) .set_text_color('#fff') @@ -203,7 +208,7 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { .set_text('No Data')); } - button_group.render(parent); + tile_group.render(parent); }; /** diff --git a/js/component/card/patreon.js b/js/component/card/patreon.js index a127912..c4219fd 100644 --- a/js/component/card/patreon.js +++ b/js/component/card/patreon.js @@ -60,6 +60,7 @@ beestat.component.card.patreon.prototype.get_title_ = function() { beestat.component.card.patreon.prototype.decorate_top_right_ = function(parent) { new beestat.component.tile() .set_type('pill') + .set_shadow(false) .set_icon('close') .set_text_color('#fff') .set_background_hover_color(beestat.style.color.green.light) diff --git a/js/component/card/runtime_sensor_detail.js b/js/component/card/runtime_sensor_detail.js index 941c388..311a014 100644 --- a/js/component/card/runtime_sensor_detail.js +++ b/js/component/card/runtime_sensor_detail.js @@ -25,8 +25,6 @@ beestat.component.card.runtime_sensor_detail = function(thermostat_id) { beestat.dispatcher.addEventListener( [ - 'setting.runtime_sensor_detail_range_type', - 'setting.runtime_sensor_detail_range_dynamic', 'cache.data.runtime_sensor_detail__runtime_thermostat', 'cache.data.runtime_sensor_detail__runtime_sensor' ], @@ -149,7 +147,7 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = func } var api_call = new beestat.api(); - beestat.sensor.get_sorted().forEach(function(sensor) { + Object.values(beestat.cache.sensor).forEach(function(sensor) { if (sensor.thermostat_id === self.thermostat_id_) { api_call.add_call( 'runtime_sensor', @@ -354,6 +352,8 @@ beestat.component.card.runtime_sensor_detail.prototype.has_data_ = function() { * @return {object} The data. */ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function(force) { + const self = this; + if (this.data_ === undefined || force === true) { var range = { 'type': beestat.setting('runtime_sensor_detail_range_type'), @@ -363,7 +363,10 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function(forc }; var sensor_data = beestat.runtime_sensor.get_data( - this.thermostat_id_, + Object.values(beestat.cache.sensor).filter(function(sensor) { + return sensor.thermostat_id === self.thermostat_id_; + }) + .map(sensor => sensor.sensor_id), range, 'runtime_sensor_detail__runtime_sensor' ); diff --git a/js/component/card/runtime_thermostat_detail.js b/js/component/card/runtime_thermostat_detail.js index 2c083d4..3abda4e 100644 --- a/js/component/card/runtime_thermostat_detail.js +++ b/js/component/card/runtime_thermostat_detail.js @@ -25,8 +25,6 @@ beestat.component.card.runtime_thermostat_detail = function(thermostat_id) { beestat.dispatcher.addEventListener( [ - 'setting.runtime_thermostat_detail_range_type', - 'setting.runtime_thermostat_detail_range_dynamic', 'cache.data.runtime_thermostat_detail__runtime_thermostat', 'cache.thermostat' ], diff --git a/js/component/card/runtime_thermostat_summary.js b/js/component/card/runtime_thermostat_summary.js index 837be77..4498251 100755 --- a/js/component/card/runtime_thermostat_summary.js +++ b/js/component/card/runtime_thermostat_summary.js @@ -509,7 +509,7 @@ beestat.component.card.runtime_thermostat_summary.prototype.decorate_top_right_ if (beestat.thermostat.get_sync_progress(this.thermostat_id_) !== null) { menu.add_menu_item(new beestat.component.menu_item() .set_text('Past 3 Months') - .set_icon('calendar_range') + .set_icon('calendar_month') .set_callback(function() { if ( beestat.setting('runtime_thermostat_summary_time_count') !== 3 || @@ -526,7 +526,7 @@ beestat.component.card.runtime_thermostat_summary.prototype.decorate_top_right_ menu.add_menu_item(new beestat.component.menu_item() .set_text('Past 12 Months') - .set_icon('calendar_range') + .set_icon('calendar_month') .set_callback(function() { if ( beestat.setting('runtime_thermostat_summary_time_count') !== 12 || @@ -543,7 +543,7 @@ beestat.component.card.runtime_thermostat_summary.prototype.decorate_top_right_ menu.add_menu_item(new beestat.component.menu_item() .set_text('All Time') - .set_icon('calendar_range') + .set_icon('calendar_month') .set_callback(function() { if ( beestat.setting('runtime_thermostat_summary_time_count') !== 0 || diff --git a/js/component/card/sensors.js b/js/component/card/sensors.js index 558e86a..9a11140 100644 --- a/js/component/card/sensors.js +++ b/js/component/card/sensors.js @@ -25,13 +25,19 @@ beestat.component.card.sensors.prototype.decorate_contents_ = function(parent) { var sensors = []; var internal_sensor; - beestat.sensor.get_sorted().forEach(function(sensor) { - if (sensor.thermostat_id === beestat.setting('thermostat_id')) { - if (sensor.type === 'thermostat') { - internal_sensor = sensor; - } else { - sensors.push(sensor); - } + + const thermostat_sensors = Object.values(beestat.cache.sensor).filter(function(sensor) { + return sensor.thermostat_id === beestat.setting('thermostat_id'); + }) + .sort(function(a, b) { + return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'}); + }); + + thermostat_sensors.forEach(function(sensor) { + if (sensor.type === 'thermostat') { + internal_sensor = sensor; + } else { + sensors.push(sensor); } }); diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js new file mode 100644 index 0000000..432721b --- /dev/null +++ b/js/component/card/three_d.js @@ -0,0 +1,595 @@ +/** + * 3D View + */ +beestat.component.card.three_d = function() { + const self = this; + + // Things that update the scene that don't require a rerender. + // TODO these probably need moved to the layer instead of here + beestat.dispatcher.addEventListener( + [ + 'setting.visualize.data_type', + 'setting.visualize.heat_map_type', + 'setting.visualize.heat_map_absolute.temperature.min', + 'setting.visualize.heat_map_absolute.temperature.max', + 'setting.visualize.heat_map_absolute.occupancy.min', + 'setting.visualize.heat_map_absolute.occupancy.max' + ], self.update_scene_.bind(this)); + + beestat.dispatcher.addEventListener('cache.floor_plan', function() { + self.scene_.rerender(); + }); + + /* + * When a setting is changed clear all of the data. Then rerender which will + * trigger the loading state. Also do this when the cache changes. + * + * Debounce so that multiple setting changes don't re-trigger the same + * event. This fires on the trailing edge so that all changes are accounted + * for when rerendering. + */ + const change_function = beestat.debounce(function() { + self.get_data_(true); + self.rerender(); + }, 10); + + beestat.dispatcher.addEventListener( + [ + 'cache.data.three_d__runtime_sensor' + ], + change_function + ); + + beestat.component.card.apply(this, arguments); +}; +beestat.extend(beestat.component.card.three_d, beestat.component.card); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.card.three_d.prototype.decorate_ = function(parent) { + this.hide_loading_(); + + this.parent_ = parent; + + /* + * Unfortunate but necessary to get the card to fill the height of the flex + * container. Everything leading up to the card has to be 100% height. + */ + parent.style('height', '100%'); + + this.contents_ = $.createElement('div') + .style({ + 'height': '100%', + 'background': beestat.style.color.bluegray.base, + 'border-radius': beestat.style.size.border_radius + }); + + if (this.box_shadow_ === true) { + this.contents_.style('box-shadow', '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)'); + } + + parent.appendChild(this.contents_); + + this.decorate_contents_(this.contents_); +}; + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.card.three_d.prototype.decorate_contents_ = function(parent) { + const drawing_pane_container = document.createElement('div'); + + parent.appendChild(drawing_pane_container); + this.decorate_drawing_pane_(drawing_pane_container); + + // Decorate everything. + const controls_container = document.createElement('div'); + Object.assign(controls_container.style, { + 'position': 'absolute', + // 'margin': 'auto', + 'top': `${beestat.style.size.gutter}px`, + 'left': '50%', + 'width': '300px', + 'margin-left': '-150px', + 'background': beestat.style.color.bluegray.base, + 'padding': `${beestat.style.size.gutter / 2}px`, + 'border-radius': `${beestat.style.size.border_radius}px` + }); + parent.appendChild(controls_container); + this.decorate_controls_(controls_container); + + + // var thermostat = beestat.cache.thermostat[this.thermostat_id_]; + + let required_begin; + let required_end; + if (beestat.setting('visualize.range_type') === 'dynamic') { + required_begin = moment() + .subtract( + beestat.setting('visualize.range_dynamic'), + 'day' + ) + .hour(0) + .minute(0) + .second(0); + + required_end = required_begin + .clone() + .hour(23) + .minute(59) + .second(59); + } else { + required_begin = moment( + beestat.setting('visualize.range_static_begin') + ' 00:00:00' + ); + required_end = moment( + beestat.setting('visualize.range_static_end') + ' 23:59:59' + ); + } + + // Don't go before there's data. +/* required_begin = moment.max( + required_begin, + moment.utc(thermostat.data_begin) + );*/ + + // Don't go after now. +/* required_end = moment.min( + required_end, + moment().subtract(1, 'hour') + );*/ + + /** + * If the needed data exists in the database and the runtime_sensor + * cache is empty, then query the data. If the needed data does not exist in + * the database, check every 2 seconds until it does. + */ + // TODO somewhat problematic because I need to check if data is synced from multiple thermostats now + // if (beestat.thermostat.data_synced(this.thermostat_id_, required_begin, required_end) === true) { + const sensor_ids = Object.keys(this.get_sensor_ids_map_()); + if (sensor_ids.length > 0) { + if (true) { + if (beestat.cache.data.three_d__runtime_sensor === undefined) { + // console.log('data is undefined need to load it'); + this.show_loading_('Fetching'); + + var value; + var operator; + // var value = [ + // required_begin.format(), + // required_end.format() + // ]; + // var operator = 'between'; + + if (beestat.setting('visualize.range_type') === 'dynamic') { + value = required_begin.format(); + operator = '>='; + } else { + value = [ + required_begin.format(), + required_end.format() + ]; + operator = 'between'; + } + + const sensor_ids = Object.keys(this.get_sensor_ids_map_()); + // if (sensor_ids.length > 0) { + const api_call = new beestat.api(); + sensor_ids.forEach(function(sensor_id) { + api_call.add_call( + 'runtime_sensor', + 'read', + { + 'attributes': { + 'sensor_id': sensor_id, + 'timestamp': { + 'value': value, + 'operator': operator + } + } + }, + 'runtime_sensor_' + sensor_id + ); + }); + + api_call.set_callback(function(response) { + var runtime_sensors = []; + for (var alias in response) { + var r = response[alias]; + runtime_sensors = runtime_sensors.concat(r); + } + beestat.cache.set('data.three_d__runtime_sensor', runtime_sensors); + }); + + api_call.send(); + + // } + } else if (this.has_data_() === false) { + console.info('has data false'); + /*chart_container.style('filter', 'blur(3px)'); + var no_data = $.createElement('div'); + no_data.style({ + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'width': '100%', + 'height': '100%', + 'display': 'flex', + 'flex-direction': 'column', + 'justify-content': 'center', + 'text-align': 'center' + }); + no_data.innerText('No data to display'); + container.appendChild(no_data);*/ + } + } else { + this.show_loading_('Syncing'); + window.setTimeout(function() { + new beestat.api() + .add_call( + 'thermostat', + 'read_id', + { + 'attributes': { + 'inactive': 0 + } + }, + 'thermostat' + ) + .set_callback(function(response) { + beestat.cache.set('thermostat', response); + self.rerender(); + }) + .send(); + }, 2000); + } + } +}; + +/** + * Decorate the drawing pane. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(parent) { + const self = this; + + // Create the scene + this.scene_ = new beestat.component.scene( + beestat.setting('visualize.floor_plan_id'), + this.get_data_() + ); + + // Set the initial date. + if (this.has_data_() === true) { + this.update_scene_(); + this.scene_.render($(parent)); + + if (beestat.setting('visualize.range_type') === 'dynamic') { + this.date_m_ = moment() + .subtract( + beestat.setting('visualize.range_dynamic'), + 'day' + ) + .hour(0) + .minute(0) + .second(0); + } else { + this.date_m_ = moment( + beestat.setting('visualize.range_static_begin') + ' 00:00:00' + ); + } + + this.scene_.set_date(this.date_m_); + } else { + this.scene_.render($(parent)); + } + + // Manage width of the scene. + setTimeout(function() { + if (parent.getBoundingClientRect().width > 0) { + self.scene_.set_width(parent.getBoundingClientRect().width); + } + }, 0); + + beestat.dispatcher.removeEventListener('resize.three_d'); + beestat.dispatcher.addEventListener('resize.three_d', function() { + self.scene_.set_width(parent.getBoundingClientRect().width); + }); +}; + +/** + * Decorate the playback controls. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.three_d.prototype.decorate_controls_ = function(parent) { + const self = this; + + window.clearInterval(self.interval_); + + // Hoisting + const range = new beestat.component.input.range(); + const right_container = document.createElement('div'); + + const container = document.createElement('div'); + Object.assign(container.style, { + 'display': 'flex', + 'align-items': 'center' + }); + parent.appendChild(container); + + const left_container = document.createElement('div'); + container.appendChild(left_container); + + const play_tile = new beestat.component.tile() + .set_icon('play') + .set_shadow(false) + .set_text_hover_color(beestat.style.color.green.base) + .render($(left_container)); + play_tile.addEventListener('click', function() { + if (self.interval_ === undefined) { + play_tile + .set_icon('pause') + .set_text_hover_color(beestat.style.color.red.base); + + self.interval_ = window.setInterval(function() { + self.date_m_.add(5, 'minutes'); + self.scene_.set_date(self.date_m_); + range.set_value( + ((self.date_m_.hours() * 60) + self.date_m_.minutes()) / 1440 * 288 + ); + right_container.innerText = self.date_m_.format('h:mm a'); + }, 100); + } else { + play_tile + .set_icon('play') + .set_text_hover_color(beestat.style.color.green.base); + window.clearInterval(self.interval_); + delete self.interval_; + } + }); + + const center_container = document.createElement('div'); + Object.assign(center_container.style, { + 'flex-grow': '1' + }); + container.appendChild(center_container); + + range + .set_min(0) + .set_max(287) + .set_value(0) + .render($(center_container)); + + right_container.innerText = '12:00 am'; + Object.assign(right_container.style, { + 'width': '70px', + 'text-align': 'right' + }); + container.appendChild(right_container); + + range.addEventListener('input', function() { + play_tile + .set_icon('play') + .set_text_hover_color(beestat.style.color.green.base); + window.clearInterval(self.interval_); + delete self.interval_; + + const minute_of_day = range.get_value() * 5; + self.date_m_.hours(Math.floor(minute_of_day / 60)); + self.date_m_.minutes(Math.floor(minute_of_day % 60)); + right_container.innerText = self.date_m_.format('h:mm a'); + self.scene_.set_date(self.date_m_); + }); +}; + +/** + * Get data. This doesn't directly or indirectly make any API calls, but it + * caches the data so it doesn't have to loop over everything more than once. + * + * @param {boolean} force Force get the data? + * + * @return {object} The data. + */ +beestat.component.card.three_d.prototype.get_data_ = function(force) { + const self = this; + if (this.data_ === undefined || force === true) { + const sensor_ids_map = this.get_sensor_ids_map_(); + + this.data_ = { + 'metadata': { + 'series': { + 'temperature': { + 'min': Infinity, + 'max': -Infinity + }, + 'occupancy': { + 'min': Infinity, + 'max': -Infinity + } + } + }, + 'series': { + 'temperature': {}, + 'occupancy': {} + } + }; + + if (beestat.cache.data.three_d__runtime_sensor !== undefined) { + // Add to data + beestat.cache.data.three_d__runtime_sensor.forEach(function(runtime_sensor) { + if ( + sensor_ids_map[runtime_sensor.sensor_id] !== undefined && + runtime_sensor.temperature !== null && + runtime_sensor.occupancy !== null + ) { + const timestamp_m = moment(runtime_sensor.timestamp); + const time = timestamp_m.format('HH:mm'); + + // Temperature + if (self.data_.series.temperature[runtime_sensor.sensor_id] === undefined) { + self.data_.series.temperature[runtime_sensor.sensor_id] = {}; + } + if (self.data_.series.temperature[runtime_sensor.sensor_id][time] === undefined) { + self.data_.series.temperature[runtime_sensor.sensor_id][time] = []; + } + self.data_.series.temperature[runtime_sensor.sensor_id][time].push(runtime_sensor.temperature); + + // Occupancy + if (self.data_.series.occupancy[runtime_sensor.sensor_id] === undefined) { + self.data_.series.occupancy[runtime_sensor.sensor_id] = {}; + } + if (self.data_.series.occupancy[runtime_sensor.sensor_id][time] === undefined) { + self.data_.series.occupancy[runtime_sensor.sensor_id][time] = []; + } + self.data_.series.occupancy[runtime_sensor.sensor_id][time].push(runtime_sensor.occupancy === true ? 1 : 0); + } + }); + + // Average data + for (let key in this.data_.series) { + for (let sensor_id in this.data_.series[key]) { + for (let time in this.data_.series[key][sensor_id]) { + this.data_.series[key][sensor_id][time] = this.data_.series[key][sensor_id][time].reduce(function(a, b) { + return a + b; + }) / this.data_.series[key][sensor_id][time].length; + + // Set min/max + this.data_.metadata.series[key].min = Math.min( + this.data_.series[key][sensor_id][time], + this.data_.metadata.series[key].min + ); + this.data_.metadata.series[key].max = Math.max( + this.data_.series[key][sensor_id][time], + this.data_.metadata.series[key].max + ); + } + } + } + } + } + // console.log(this.data_); + return this.data_; +}; + +/** + * Whether or not there is data to display on the chart. + * + * @return {boolean} Whether or not there is data to display on the chart. + */ +beestat.component.card.three_d.prototype.has_data_ = function() { + const data = this.get_data_(); + for (let key in data.series) { + for (let sensor_id in data.series[key]) { + if (Object.keys(data.series[key][sensor_id]).length > 0) { + return true; + } + } + } + + return false; +}; + +/** + * Get the title of the card. + * + * @return {string} The title. + */ +beestat.component.card.three_d.prototype.get_title_ = function() { + return '3D View'; +}; + +/** + * Decorate the menu. + * + * @param {rocket.Elements} parent + */ +beestat.component.card.three_d.prototype.decorate_top_right_ = function(parent) { + const menu = (new beestat.component.menu()).render(parent); + + menu.add_menu_item(new beestat.component.menu_item() + .set_text('Help') + .set_icon('help_circle') + .set_callback(function() { + // TODO + // window.open('https://doc.beestat.io/???'); + })); +}; + +/** + * Update the scene with current settings. Anything that doesn't require + * re-rendering can go here. + */ +beestat.component.card.three_d.prototype.update_scene_ = function() { + this.scene_.set_data_type(beestat.setting('visualize.data_type')); + + switch (beestat.setting('visualize.heat_map_type')) { + case 'relative': + this.scene_.set_heat_map_min( + this.data_.metadata.series[beestat.setting('visualize.data_type')].min + ); + this.scene_.set_heat_map_max( + this.data_.metadata.series[beestat.setting('visualize.data_type')].max + ); + break; + case 'absolute': + this.scene_.set_heat_map_min( + beestat.setting( + 'visualize.heat_map_absolute.' + + beestat.setting('visualize.data_type') + + '.min' + ) / (beestat.setting('visualize.data_type') === 'occupancy' ? 100 : 1) + ); + this.scene_.set_heat_map_max( + beestat.setting( + 'visualize.heat_map_absolute.' + + beestat.setting('visualize.data_type') + + '.max' + ) / (beestat.setting('visualize.data_type') === 'occupancy' ? 100 : 1) + ); + break; + } +}; + +/** + * Set the floor_plan_id. + * + * @param {number} floor_plan_id + * + * @return {beestat.component.card.three_d} + */ +beestat.component.card.three_d.prototype.set_floor_plan_id = function(floor_plan_id) { + this.floor_plan_id_ = floor_plan_id; + + if (this.rendered_ === true) { + this.rerender(); + } + + return this; +}; + +/** + * Get an object of all the sensor_ids included in the current floor plan. Key + * is sensor_id, value is true. + * + * @return {object} + */ +beestat.component.card.three_d.prototype.get_sensor_ids_map_ = function() { + const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; + const sensor_ids_map = []; + floor_plan.data.groups.forEach(function(group) { + group.rooms.forEach(function(room) { + if (room.sensor_id !== undefined) { + sensor_ids_map[room.sensor_id] = true; + } + }); + }); + + return sensor_ids_map; +}; diff --git a/js/component/card/visualize.js b/js/component/card/visualize.js deleted file mode 100644 index f9554b4..0000000 --- a/js/component/card/visualize.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Visualize - * - * @param {number} thermostat_id The thermostat_id this card is displaying - * data for. - */ -beestat.component.card.visualize = function(thermostat_id) { - this.thermostat_id_ = thermostat_id; - - beestat.component.card.apply(this, arguments); -}; -beestat.extend(beestat.component.card.visualize, beestat.component.card); - -beestat.component.card.visualize.prototype.decorate_contents_ = function(parent) { - const scene = new beestat.component.scene(this.thermostat_id_); - scene.render(parent); - - let date = new Date('2022-06-27 11:30:00'); - scene.set_date(date); - // let date = new Date('2024-04-08 08:00:00'); // Total solar eclipse - // scene.set_date(date); - // setInterval(function() { - // date = new Date(date.getTime() + (60000 * 1)); - // scene.set_date(date); - // }, 100); - // - - beestat.dispatcher.addEventListener('cache.floor_plan', function() { - scene.rerender(); - // todo get scene to remember date, camera position, etc on rerender - scene.set_date(date); - }); -}; - -/** - * Get the title of the card. - * - * @return {string} The title. - */ -beestat.component.card.visualize.prototype.get_title_ = function() { - return 'Visualize'; -}; diff --git a/js/component/card/visualize_settings.js b/js/component/card/visualize_settings.js new file mode 100644 index 0000000..8557bba --- /dev/null +++ b/js/component/card/visualize_settings.js @@ -0,0 +1,359 @@ +/** + * Visualize settings. + */ +beestat.component.card.visualize_settings = function() { + const self = this; + beestat.dispatcher.addEventListener('cache.floor_plan', function() { + self.rerender(); + }); + + beestat.component.card.apply(this, arguments); +}; +beestat.extend(beestat.component.card.visualize_settings, beestat.component.card); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_contents_ = function(parent) { + const grid_1 = document.createElement('div'); + Object.assign(grid_1.style, { + 'display': 'grid', + 'grid-template-columns': 'repeat(auto-fit, minmax(min(350px, 100%), 1fr))', + 'grid-gap': `${beestat.style.size.gutter}px`, + 'margin-bottom': `${beestat.style.size.gutter}px` + }); + parent.appendChild(grid_1); + + const type_container = document.createElement('div'); + this.decorate_data_type_(type_container); + grid_1.appendChild(type_container); + + const time_period_container = document.createElement('div'); + this.decorate_time_period_(time_period_container); + grid_1.appendChild(time_period_container); + + const grid_2 = document.createElement('div'); + Object.assign(grid_2.style, { + 'display': 'grid', + 'grid-template-columns': 'repeat(auto-fit, minmax(min(350px, 100%), 1fr))', + 'grid-gap': `${beestat.style.size.gutter}px` + }); + parent.appendChild(grid_2); + + const heat_map_type_container = document.createElement('div'); + this.decorate_heat_map_type_(heat_map_type_container); + grid_2.appendChild(heat_map_type_container); + + const floor_plan_container = document.createElement('div'); + this.decorate_floor_plan_(floor_plan_container); + grid_2.appendChild(floor_plan_container); +}; + +/** + * Decorate the type options. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_data_type_ = function(parent) { + const self = this; + + (new beestat.component.title('Data Type')).render($(parent)); + + const types = [ + { + 'code': 'temperature', + 'name': 'Temperature', + 'icon': 'thermometer' + }, + { + 'code': 'occupancy', + 'name': 'Occupancy', + 'icon': 'eye' + } + ]; + + const color = beestat.style.color.green.base; + const tile_group = new beestat.component.tile_group(); + types.forEach(function(type) { + const tile = new beestat.component.tile() + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon(type.icon) + .set_text(type.name); + + if (beestat.setting('visualize.data_type') === type.code) { + tile.set_background_color(color); + } else { + tile + .set_background_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + beestat.setting('visualize.data_type', type.code); + self.rerender(); + }); + } + tile_group.add_tile(tile); + }); + + tile_group.render($(parent)); +}; +/** + * Decorate the type options. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_heat_map_type_ = function(parent) { + const self = this; + + (new beestat.component.title('Heat Map Type')).render($(parent)); + + const types = [ + { + 'code': 'relative', + 'name': 'Relative', + 'icon': 'arrow_expand_horizontal' + }, + { + 'code': 'absolute', + 'name': 'Absolute', + 'icon': 'arrow_horizontal_lock' + } + ]; + + const color = beestat.style.color.orange.base; + const tile_group = new beestat.component.tile_group(); + types.forEach(function(type) { + const tile = new beestat.component.tile() + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon(type.icon) + .set_text(type.name); + + if (beestat.setting('visualize.heat_map_type') === type.code) { + tile.set_background_color(color); + } else { + tile + .set_background_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + beestat.setting('visualize.heat_map_type', type.code); + self.rerender(); + }); + } + tile_group.add_tile(tile); + }); + tile_group.render($(parent)); + + if (beestat.setting('visualize.heat_map_type') === 'absolute') { + const min_max_container = document.createElement('div'); + min_max_container.style.marginTop = `${beestat.style.size.gutter}px`; + parent.appendChild(min_max_container); + + const min = new beestat.component.input.text() + .set_value(beestat.setting( + 'visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.min') + ) + .set_width(50); + min.addEventListener('change', function() { + beestat.setting('visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.min', min.get_value()); + }); + + const max = new beestat.component.input.text() + .set_value(beestat.setting( + 'visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.max') + ) + .set_width(50); + max.addEventListener('change', function() { + beestat.setting('visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.max', max.get_value()); + }); + + let span; + + span = document.createElement('span'); + span.style.display = 'inline-block'; + min.render($(span)); + parent.appendChild(span); + + span = document.createElement('span'); + span.innerText = 'to'; + Object.assign(span.style, { + 'display': 'inline-block', + 'margin-left': `${beestat.style.size.gutter}px`, + 'margin-right': `${beestat.style.size.gutter}px` + }); + parent.appendChild(span); + + span = document.createElement('span'); + span.style.display = 'inline-block'; + max.render($(span)); + parent.appendChild(span); + + span = document.createElement('span'); + switch (beestat.setting('visualize.data_type')) { + case 'temperature': + span.innerText = beestat.setting('temperature_unit'); + break; + case 'occupancy': + span.innerText = '%'; + break; + } + + Object.assign(span.style, { + 'display': 'inline-block', + 'margin-left': `${beestat.style.size.gutter}px` + }); + parent.appendChild(span); + } +}; + +/** + * Decorate the type options. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_time_period_ = function(parent) { + const self = this; + + (new beestat.component.title('Time Period')).render($(parent)); + + const color = beestat.style.color.purple.base; + + const tile_group = new beestat.component.tile_group(); + + // Current Day + const day_tile = new beestat.component.tile() + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon('calendar') + .set_text('Today'); + + if ( + beestat.setting('visualize.range_type') === 'dynamic' && + beestat.setting('visualize.range_dynamic') === 0 + ) { + day_tile.set_background_color(color); + } else { + day_tile + .set_background_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + beestat.cache.delete('data.three_d__runtime_sensor'); + beestat.setting('visualize.range_type', 'dynamic'); + beestat.setting('visualize.range_dynamic', 0); + self.rerender(); + }); + } + tile_group.add_tile(day_tile); + + // Current Week + const week_tile = new beestat.component.tile() + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon('calendar_week') + .set_text('Last 7 Days'); + + if ( + beestat.setting('visualize.range_type') === 'dynamic' && + beestat.setting('visualize.range_dynamic') === 7 + ) { + week_tile.set_background_color(color); + } else { + week_tile + .set_background_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + beestat.cache.delete('data.three_d__runtime_sensor'); + beestat.setting('visualize.range_type', 'dynamic'); + beestat.setting('visualize.range_dynamic', 7); + self.rerender(); + }); + } + tile_group.add_tile(week_tile); + + // Custom +/* const custom_tile = new beestat.component.tile() + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon('calendar_edit') + .set_text('Custom'); + + if ( + beestat.setting('visualize.range_type') === 'static' + ) { + custom_tile.set_background_color(color); + } else { + custom_tile + .set_background_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + // TODO MODAL + beestat.setting('visualize.range_type', 'static'); + self.rerender(); + }); + } + tile_group.add_tile(custom_tile);*/ + + tile_group.render($(parent)); +}; + +/** + * Decorate the floor plan options. + * + * @param {HTMLDivElement} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_floor_plan_ = function(parent) { + const self = this; + + (new beestat.component.title('Floor Plan')).render($(parent)); + + var sorted_floor_plans = $.values(beestat.cache.floor_plan) + .sort(function(a, b) { + return a.name > b.name; + }); + + const tile_group = new beestat.component.tile_group(); + sorted_floor_plans.forEach(function(floor_plan) { + const tile = new beestat.component.tile.floor_plan(floor_plan.floor_plan_id) + .set_text_color('#fff') + .set_display('block'); + + if (floor_plan.floor_plan_id === beestat.setting('visualize.floor_plan_id')) { + tile.set_background_color(beestat.style.color.lightblue.base); + } else { + tile + .set_background_color(beestat.style.color.bluegray.light) + .set_background_hover_color(beestat.style.color.lightblue.base) + .addEventListener('click', function() { + beestat.setting('visualize.floor_plan_id', floor_plan.floor_plan_id); + self.rerender(); + }); + } + + tile_group.add_tile(tile); + }); + + tile_group.render($(parent)); +}; + +/** + * Get the title of the card. + * + * @return {string} The title of the card. + */ +beestat.component.card.visualize_settings.prototype.get_title_ = function() { + return 'Visualize Settings'; +}; + +/** + * Decorate the menu. + * + * @param {rocket.Elements} parent + */ +beestat.component.card.visualize_settings.prototype.decorate_top_right_ = function(parent) { + var menu = (new beestat.component.menu()).render(parent); + + menu.add_menu_item(new beestat.component.menu_item() + .set_text('Help') + .set_icon('help_circle') + .set_callback(function() { + // TODO + // window.open('https://doc.beestat.io/596040eadd014928830b4d1d54692761'); + })); +}; diff --git a/js/component/floor_plan.js b/js/component/floor_plan.js index 57518d2..d9fa56b 100644 --- a/js/component/floor_plan.js +++ b/js/component/floor_plan.js @@ -404,24 +404,25 @@ beestat.component.floor_plan.prototype.dispose = function() { beestat.component.floor_plan.prototype.update_toolbar = function() { const self = this; - if (this.button_group_ !== undefined) { - this.button_group_.dispose(); + if (this.tile_group_ !== undefined) { + this.tile_group_.dispose(); } - if (this.button_group_floors_ !== undefined) { - this.button_group_floors_.dispose(); + if (this.tile_group_floors_ !== undefined) { + this.tile_group_floors_.dispose(); } - this.button_group_ = new beestat.component.tile_group(); + this.tile_group_ = new beestat.component.tile_group(); // Add floor - this.button_group_.add_button(new beestat.component.tile() + this.tile_group_.add_tile(new beestat.component.tile() .set_icon('layers') + .set_shadow(false) .set_text_color(beestat.style.color.lightblue.base) ); // Add room - this.button_group_.add_button(new beestat.component.tile() + this.tile_group_.add_tile(new beestat.component.tile() .set_icon('card_plus_outline') .set_title('Add Room [R]') .set_text_color(beestat.style.color.gray.light) @@ -437,7 +438,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('card_remove_outline') .set_title('Remove Room [Delete]') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(remove_room_button); + this.tile_group_.add_tile(remove_room_button); if (this.state_.active_room_entity !== undefined) { remove_room_button @@ -454,7 +455,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('vector_square_plus') .set_title('Add Point [Double click]') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(add_point_button); + this.tile_group_.add_tile(add_point_button); if (this.state_.active_wall_entity !== undefined) { add_point_button @@ -471,7 +472,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_background_color(beestat.style.color.bluegray.base) .set_title('Remove Point [Delete]') .set_icon('vector_square_remove'); - this.button_group_.add_button(remove_point_button); + this.tile_group_.add_tile(remove_point_button); if ( this.state_.active_point_entity !== undefined && @@ -497,7 +498,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { snapping_title = 'Enable Snapping [S]'; } - this.button_group_.add_button(new beestat.component.tile() + this.tile_group_.add_tile(new beestat.component.tile() .set_icon(snapping_icon) .set_title(snapping_title) .set_text_color(beestat.style.color.gray.light) @@ -511,7 +512,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('undo') .set_title('Undo [Ctrl+Z]') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(undo_button); + this.tile_group_.add_tile(undo_button); if ( this.can_undo_() === true @@ -532,7 +533,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('redo') .set_title('redo [Ctrl+Y]') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(redo_button); + this.tile_group_.add_tile(redo_button); if ( this.can_redo_() === true @@ -553,7 +554,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('magnify_plus_outline') .set_title('Zoom In') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(zoom_in_button); + this.tile_group_.add_tile(zoom_in_button); if ( this.can_zoom_in_() === true @@ -574,7 +575,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { .set_icon('magnify_minus_outline') .set_title('Zoom out') .set_background_color(beestat.style.color.bluegray.base); - this.button_group_.add_button(zoom_out_button); + this.tile_group_.add_tile(zoom_out_button); if ( this.can_zoom_out_() === true @@ -591,10 +592,10 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { } // Render - this.button_group_.render(this.toolbar_container_); + this.tile_group_.render(this.toolbar_container_); // FLOORS - this.button_group_floors_ = new beestat.component.tile_group(); + this.tile_group_floors_ = new beestat.component.tile_group(); const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; @@ -607,6 +608,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { sorted_groups.forEach(function(group) { const button = new beestat.component.tile() .set_title(group.name) + .set_shadow(false) .set_text_hover_color(beestat.style.color.lightblue.light) .set_text_color(beestat.style.color.lightblue.base); @@ -639,10 +641,10 @@ beestat.component.floor_plan.prototype.update_toolbar = function() { }); } - self.button_group_floors_.add_button(button); + self.tile_group_floors_.add_tile(button); }); - this.button_group_floors_.render(this.floors_container_); + this.tile_group_floors_.render(this.floors_container_); }; /** @@ -952,30 +954,33 @@ beestat.component.floor_plan.prototype.get_group_below = function(group) { * Center the view box on the content. Sets zoom and pan. */ beestat.component.floor_plan.prototype.center_content = function() { - const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; + // const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; - let min_x = Infinity; - let max_x = -Infinity; - let min_y = Infinity; - let max_y = -Infinity; + // let min_x = Infinity; + // let max_x = -Infinity; + // let min_y = Infinity; + // let max_y = -Infinity; - let has_content = false; - floor_plan.data.groups.forEach(function(group) { - group.rooms.forEach(function(room) { - room.points.forEach(function(point) { - has_content = true; - min_x = Math.min(room.x + point.x, min_x); - max_x = Math.max(room.x + point.x, max_x); - min_y = Math.min(room.y + point.y, min_y); - max_y = Math.max(room.y + point.y, max_y); - }); - }); - }); + // let has_content = false; + // floor_plan.data.groups.forEach(function(group) { + // group.rooms.forEach(function(room) { + // room.points.forEach(function(point) { + // has_content = true; + // min_x = Math.min(room.x + point.x, min_x); + // max_x = Math.max(room.x + point.x, max_x); + // min_y = Math.min(room.y + point.y, min_y); + // max_y = Math.max(room.y + point.y, max_y); + // }); + // }); + // }); + + const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_); this.reset_view_box_(); - if (has_content === true) { - const width = (max_x - min_x) + 50; - const height = (max_y - min_y) + 50; + // TODO ADD THIS BACK IN + // if (has_content === true) { + const width = (bounding_box.width) + 50; + const height = (bounding_box.height) + 50; while ( ( this.view_box_.width < width || @@ -986,14 +991,14 @@ beestat.component.floor_plan.prototype.center_content = function() { this.zoom_out_(); } - const center_x = (max_x + min_x) / 2; - const center_y = (max_y + min_y) / 2; + const center_x = (bounding_box.right + bounding_box.left) / 2; + const center_y = (bounding_box.bottom + bounding_box.top) / 2; this.view_box_.x = center_x - (this.view_box_.width / 2); this.view_box_.y = center_y - (this.view_box_.height / 2); this.update_view_box_(); - } + // } }; /** @@ -1019,7 +1024,7 @@ beestat.component.floor_plan.prototype.save_buffer = function(clear = true) { } this.state_.buffer.push({ - 'floor_plan': beestat.clone(beestat.cache.floor_plan[beestat.setting('floor_plan_id')]), + 'floor_plan': beestat.clone(beestat.cache.floor_plan[beestat.setting('visualize.floor_plan_id')]), 'active_room_entity': this.state_.active_room_entity, 'active_group_id': this.state_.active_group.group_id }); diff --git a/js/component/header.js b/js/component/header.js index 8096f6d..b9a6892 100644 --- a/js/component/header.js +++ b/js/component/header.js @@ -90,10 +90,11 @@ beestat.component.header.prototype.decorate_ = function(parent) { }); row.appendChild(column_navigation); - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); pages.forEach(function(page) { var button = new beestat.component.tile() .set_icon(page.icon) + .set_shadow(false) .set_text_color(beestat.style.color.bluegray.dark); if (beestat.width > 800) { @@ -115,10 +116,10 @@ beestat.component.header.prototype.decorate_ = function(parent) { }); } - button_group.add_button(button); + tile_group.add_tile(button); }); - button_group.render(column_navigation); + tile_group.render(column_navigation); // Menu diff --git a/js/component/input/range.js b/js/component/input/range.js new file mode 100644 index 0000000..42ef90e --- /dev/null +++ b/js/component/input/range.js @@ -0,0 +1,81 @@ +/** + * Range input. + */ +beestat.component.input.range = function() { + const self = this; + + this.input_ = document.createElement('input'); + this.input_.setAttribute('type', 'range'); + + this.input_.addEventListener('change', function() { + self.dispatchEvent('change'); + }); + + this.input_.addEventListener('input', function() { + self.dispatchEvent('input'); + }); + + beestat.component.input.apply(this, arguments); +}; +beestat.extend(beestat.component.input.range, beestat.component.input); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.input.range.prototype.decorate_ = function(parent) { + this.input_.style.width = '100%'; + + parent.appendChild(this.input_); +}; + +/** + * Set the value in the range field. Do not rerender; it's unnecessary. + * + * @param {string} value + * + * @return {beestat.component.input.range} This. + */ +beestat.component.input.range.prototype.set_value = function(value) { + this.input_.value = value; + + this.dispatchEvent('change'); + + return this; +}; + +/** + * Get the value of the input. + * + * @return {string} + */ +beestat.component.input.range.prototype.get_value = function() { + return this.input_.value; +}; + +/** + * Set the min value of the range input. + * + * @param {string} min + * + * @return {beestat.component.input.range} This. + */ +beestat.component.input.range.prototype.set_min = function(min) { + this.input_.setAttribute('min', min); + + return this; +}; + +/** + * Set the max value of the range input. + * + * @param {string} max + * + * @return {beestat.component.input.range} This. + */ +beestat.component.input.range.prototype.set_max = function(max) { + this.input_.setAttribute('max', max); + + return this; +}; diff --git a/js/component/menu.js b/js/component/menu.js index 5dc7701..41b9642 100644 --- a/js/component/menu.js +++ b/js/component/menu.js @@ -13,6 +13,7 @@ beestat.component.menu.prototype.decorate_ = function(parent) { this.icon_ = new beestat.component.tile() .set_type('pill') + .set_shadow(false) .set_icon('dots_vertical') .set_bubble_text(this.bubble_text_) .set_bubble_color(this.bubble_color_) diff --git a/js/component/modal.js b/js/component/modal.js index d2ee41b..c2338e0 100644 --- a/js/component/modal.js +++ b/js/component/modal.js @@ -208,6 +208,7 @@ beestat.component.modal.prototype.decorate_close_ = function(parent) { var close = new beestat.component.tile() .set_type('pill') + .set_shadow(false) .set_icon('close') .set_text_color(beestat.style.color.gray.dark) .set_background_hover_color(beestat.style.color.gray.light) @@ -248,11 +249,11 @@ beestat.component.modal.prototype.decorate_buttons_ = function(parent) { }); parent.appendChild(container); - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); buttons.forEach(function(button) { - button_group.add_button(button); + tile_group.add_tile(button); }); - button_group.render(container); + tile_group.render(container); } }; diff --git a/js/component/modal/air_quality_detail_custom.js b/js/component/modal/air_quality_detail_custom.js index f86af9c..3d496f5 100644 --- a/js/component/modal/air_quality_detail_custom.js +++ b/js/component/modal/air_quality_detail_custom.js @@ -43,9 +43,9 @@ beestat.component.modal.air_quality_detail_custom.prototype.decorate_contents_ = beestat.component.modal.air_quality_detail_custom.prototype.decorate_range_type_ = function(parent) { var self = this; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -59,7 +59,7 @@ beestat.component.modal.air_quality_detail_custom.prototype.decorate_range_type_ self.rerender(); })); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -78,7 +78,7 @@ beestat.component.modal.air_quality_detail_custom.prototype.decorate_range_type_ parent.appendChild(row); var column = $.createElement('div').addClass(['column column_12']); row.appendChild(column); - button_group.render(column); + tile_group.render(column); }; /** @@ -298,6 +298,7 @@ beestat.component.modal.air_quality_detail_custom.prototype.get_buttons_ = funct .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); @@ -332,8 +333,8 @@ beestat.component.modal.air_quality_detail_custom.prototype.get_buttons_ = funct self.state_.air_quality_detail_range_static_end = temp; } - beestat.cache.delete('runtime_thermostat'); - beestat.cache.delete('runtime_sensor'); + beestat.cache.delete('data.air_quality_detail__runtime_thermostat'); + beestat.cache.delete('data.air_quality_detail__runtime_sensor'); beestat.setting( { 'air_quality_detail_range_type': self.state_.air_quality_detail_range_type, diff --git a/js/component/modal/change_floor_plan.js b/js/component/modal/change_floor_plan.js deleted file mode 100644 index b88a92b..0000000 --- a/js/component/modal/change_floor_plan.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Change floor plan - */ -beestat.component.modal.change_floor_plan = function() { - beestat.component.modal.apply(this, arguments); -}; -beestat.extend(beestat.component.modal.change_floor_plan, beestat.component.modal); - -/** - * Decorate - * - * @param {rocket.Elements} parent - */ -beestat.component.modal.change_floor_plan.prototype.decorate_contents_ = function(parent) { - const self = this; - - const p = document.createElement('p'); - p.innerText = 'You have multiple floor plans; which one would you like to view?'; - parent.appendChild(p); - - const grid = document.createElement('div'); - grid.style.display = 'grid'; - grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))'; - grid.style.columnGap = beestat.style.size.gutter + 'px'; - grid.style.rowGap = beestat.style.size.gutter + 'px'; - parent.appendChild(grid); - - var sorted_floor_plans = $.values(beestat.cache.floor_plan) - .sort(function(a, b) { - return a.name > b.name; - }); - - let div; - sorted_floor_plans.forEach(function(floor_plan) { - div = document.createElement('div'); - grid.appendChild(div); - - const tile = new beestat.component.tile.floor_plan(floor_plan.floor_plan_id) - .set_text_color('#fff') - .set_display('block'); - - if (floor_plan.floor_plan_id === beestat.setting('floor_plan_id')) { - tile.set_background_color(beestat.style.color.lightblue.base); - } else { - tile - .set_background_color(beestat.style.color.bluegray.base) - .set_background_hover_color(beestat.style.color.lightblue.base) - .addEventListener('click', function() { - beestat.setting('floor_plan_id', floor_plan.floor_plan_id); - self.dispose(); - }); - } - - tile.render($(div)); - }); -}; - -/** - * Get title. - * - * @return {string} Title. - */ -beestat.component.modal.change_floor_plan.prototype.get_title_ = function() { - return 'Change Floor Plan'; -}; - diff --git a/js/component/modal/change_system_type.js b/js/component/modal/change_system_type.js index e32cd79..91ac187 100644 --- a/js/component/modal/change_system_type.js +++ b/js/component/modal/change_system_type.js @@ -62,7 +62,7 @@ beestat.component.modal.change_system_type.prototype.decorate_contents_ = functi key ); - let button_group = new beestat.component.tile_group(); + let tile_group = new beestat.component.tile_group(); options[key].forEach(function(system_type) { let text = system_type.charAt(0).toUpperCase() + system_type.slice(1); if (thermostat.system_type.detected[key].equipment === system_type) { @@ -93,9 +93,9 @@ beestat.component.modal.change_system_type.prototype.decorate_contents_ = functi button.set_background_color(beestat.style.color.bluegray.base); } - button_group.add_button(button); + tile_group.add_tile(button); }); - button_group.render(parent); + tile_group.render(parent); } }; @@ -117,6 +117,7 @@ beestat.component.modal.change_system_type.prototype.get_buttons_ = function() { .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); diff --git a/js/component/modal/create_floor_plan.js b/js/component/modal/create_floor_plan.js index 17b8414..9a35b60 100644 --- a/js/component/modal/create_floor_plan.js +++ b/js/component/modal/create_floor_plan.js @@ -176,6 +176,7 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); @@ -248,7 +249,7 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { 'floor_plan' ) .set_callback(function(response) { - beestat.setting('floor_plan_id', response.new_floor_plan.floor_plan_id); + beestat.setting('visualize.floor_plan_id', response.new_floor_plan.floor_plan_id); beestat.cache.set('floor_plan', response.floor_plan); }) .send(); diff --git a/js/component/modal/delete_floor_plan.js b/js/component/modal/delete_floor_plan.js index e14932a..9fb4625 100644 --- a/js/component/modal/delete_floor_plan.js +++ b/js/component/modal/delete_floor_plan.js @@ -47,6 +47,7 @@ beestat.component.modal.delete_floor_plan.prototype.get_buttons_ = function() { .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); @@ -78,9 +79,9 @@ beestat.component.modal.delete_floor_plan.prototype.get_buttons_ = function() { console.log('deleted fp'); console.log(response); if (Object.keys(response.floor_plan).length > 0) { - beestat.setting('floor_plan_id', Object.values(response.floor_plan)[0].floor_plan_id); + beestat.setting('visualize.floor_plan_id', Object.values(response.floor_plan)[0].floor_plan_id); } else { - beestat.setting('floor_plan_id', null); + beestat.setting('visualize.floor_plan_id', null); } beestat.cache.set('floor_plan', response.floor_plan); }) diff --git a/js/component/modal/download_data.js b/js/component/modal/download_data.js index 06f6b53..051d5e9 100644 --- a/js/component/modal/download_data.js +++ b/js/component/modal/download_data.js @@ -169,7 +169,7 @@ beestat.component.modal.download_data.prototype.decorate_presets_ = function(par } ]; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); presets.forEach(function(preset) { preset.button .set_background_color(beestat.style.color.bluegray.base) @@ -181,7 +181,7 @@ beestat.component.modal.download_data.prototype.decorate_presets_ = function(par self.state_.range_end = preset.range_end; self.dispatchEvent('range_change'); }); - button_group.add_button(preset.button); + tile_group.add_tile(preset.button); }); // Highlight the preset if the current date range matches. @@ -198,7 +198,7 @@ beestat.component.modal.download_data.prototype.decorate_presets_ = function(par }); }); - button_group.render(column); + tile_group.render(column); }; /** @@ -246,6 +246,7 @@ beestat.component.modal.download_data.prototype.get_buttons_ = function() { .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); diff --git a/js/component/modal/enjoy_beestat.js b/js/component/modal/enjoy_beestat.js index 4925a65..e4dbd37 100644 --- a/js/component/modal/enjoy_beestat.js +++ b/js/component/modal/enjoy_beestat.js @@ -57,6 +57,7 @@ beestat.component.modal.enjoy_beestat.prototype.get_buttons_ = function() { var hide = new beestat.component.tile() .set_background_color('#fff') + .set_shadow(false) .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.bluegray.base) .set_text('Hide for one month') diff --git a/js/component/modal/runtime_sensor_detail_custom.js b/js/component/modal/runtime_sensor_detail_custom.js index ca3da0d..9b4f269 100644 --- a/js/component/modal/runtime_sensor_detail_custom.js +++ b/js/component/modal/runtime_sensor_detail_custom.js @@ -43,9 +43,9 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_contents beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_range_type_ = function(parent) { var self = this; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -59,7 +59,7 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_range_ty self.rerender(); })); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -78,7 +78,7 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_range_ty parent.appendChild(row); var column = $.createElement('div').addClass(['column column_12']); row.appendChild(column); - button_group.render(column); + tile_group.render(column); }; /** @@ -298,6 +298,7 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.get_buttons_ = fu .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); @@ -332,8 +333,8 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.get_buttons_ = fu self.state_.runtime_sensor_detail_range_static_end = temp; } - beestat.cache.delete('runtime_thermostat'); - beestat.cache.delete('runtime_sensor'); + beestat.cache.delete('data.runtime_sensor_detail__runtime_thermostat'); + beestat.cache.delete('data.runtime_sensor_detail__runtime_sensor'); beestat.setting( { 'runtime_sensor_detail_range_type': self.state_.runtime_sensor_detail_range_type, diff --git a/js/component/modal/runtime_thermostat_detail_custom.js b/js/component/modal/runtime_thermostat_detail_custom.js index b59f09b..e214b3a 100644 --- a/js/component/modal/runtime_thermostat_detail_custom.js +++ b/js/component/modal/runtime_thermostat_detail_custom.js @@ -43,9 +43,9 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_cont beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_range_type_ = function(parent) { var self = this; - var button_group = new beestat.component.tile_group(); + var tile_group = new beestat.component.tile_group(); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -59,7 +59,7 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_rang self.rerender(); })); - button_group.add_button(new beestat.component.tile() + tile_group.add_tile(new beestat.component.tile() .set_background_hover_color(beestat.style.color.lightblue.base) .set_text_color('#fff') .set_background_color( @@ -78,7 +78,7 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_rang parent.appendChild(row); var column = $.createElement('div').addClass(['column column_12']); row.appendChild(column); - button_group.render(column); + tile_group.render(column); }; /** @@ -298,6 +298,7 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.get_buttons_ .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); @@ -332,7 +333,7 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.get_buttons_ self.state_.runtime_thermostat_detail_range_static_end = temp; } - beestat.cache.delete('runtime_thermostat'); + beestat.cache.delete('data.runtime_thermostat_detail__runtime_thermostat'); beestat.setting( { 'runtime_thermostat_detail_range_type': self.state_.runtime_thermostat_detail_range_type, diff --git a/js/component/modal/runtime_thermostat_summary_custom.js b/js/component/modal/runtime_thermostat_summary_custom.js index d6eb2af..bacf0ee 100644 --- a/js/component/modal/runtime_thermostat_summary_custom.js +++ b/js/component/modal/runtime_thermostat_summary_custom.js @@ -43,13 +43,13 @@ beestat.component.modal.runtime_thermostat_summary_custom.prototype.decorate_con ] }; - var button_groups = {}; + var tile_groups = {}; this.selected_buttons_ = {}; for (let key in options) { let current_type = beestat.setting(key); - let button_group = new beestat.component.tile_group(); + let tile_group = new beestat.component.tile_group(); options[key].forEach(function(value) { let text = value.replace('runtime_thermostat_summary_', '') .charAt(0) @@ -105,9 +105,9 @@ beestat.component.modal.runtime_thermostat_summary_custom.prototype.decorate_con button.set_background_color(beestat.style.color.bluegray.base); } - button_group.add_button(button); + tile_group.add_tile(button); }); - button_groups[key] = button_group; + tile_groups[key] = tile_group; } // Display it all @@ -122,13 +122,13 @@ beestat.component.modal.runtime_thermostat_summary_custom.prototype.decorate_con time_count.render(column); column = $.createElement('div').addClass(['column column_10']); row.appendChild(column); - button_groups.runtime_thermostat_summary_time_period.render(column); + tile_groups.runtime_thermostat_summary_time_period.render(column); (new beestat.component.title('Group By')).render(parent); row = $.createElement('div').addClass('row'); parent.appendChild(row); column = $.createElement('div').addClass(['column column_12']); row.appendChild(column); - button_groups.runtime_thermostat_summary_group_by.render(column); + tile_groups.runtime_thermostat_summary_group_by.render(column); }; /** @@ -152,6 +152,7 @@ beestat.component.modal.runtime_thermostat_summary_custom.prototype.get_buttons_ .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); diff --git a/js/component/modal/update_floor_plan.js b/js/component/modal/update_floor_plan.js index 36e8cac..c399c56 100644 --- a/js/component/modal/update_floor_plan.js +++ b/js/component/modal/update_floor_plan.js @@ -69,8 +69,8 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio sorted_groups.forEach(function(group) { new beestat.component.tile.floor_plan_group(group) - .set_background_color(beestat.style.color.bluegray.base) - .set_text_color('#fff') + .set_background_color(beestat.style.color.gray.dark) + .set_shadow(false) .set_display('block') .render($(grid)); }); @@ -136,6 +136,7 @@ beestat.component.modal.update_floor_plan.prototype.get_buttons_ = function() { .set_background_color('#fff') .set_text_color(beestat.style.color.gray.base) .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) .set_text('Cancel') .addEventListener('click', function() { self.dispose(); diff --git a/js/component/scene.js b/js/component/scene.js index 7bde710..c04af60 100644 --- a/js/component/scene.js +++ b/js/component/scene.js @@ -1,8 +1,12 @@ /** * Home Scene + * + * @param {number} floor_plan_id The floor plan to render. + * @param {object} data Sensor data. */ -beestat.component.scene = function(thermostat_id) { - this.thermostat_id_ = thermostat_id; +beestat.component.scene = function(floor_plan_id, data) { + this.floor_plan_id_ = floor_plan_id; + this.data_ = data; beestat.component.apply(this, arguments); }; @@ -11,13 +15,19 @@ beestat.extend(beestat.component.scene, beestat.component); beestat.component.scene.sun_light_intensity = 1; beestat.component.scene.moon_light_intensity = 0.3; -beestat.component.scene.ambient_light_intensity_base = 0.3; +beestat.component.scene.ambient_light_intensity_base = 1.5; beestat.component.scene.ambient_light_intensity_sky = 0.4; +beestat.component.scene.moon_opacity = 0.9; + beestat.component.scene.turbidity = 10; beestat.component.scene.rayleigh = 0.5; beestat.component.scene.mie_coefficient = 0.001; beestat.component.scene.mie_directional_g = 0.95; -beestat.component.scene.moon_opacity = 0.9; + +// beestat.component.scene.turbidity = 14; +// beestat.component.scene.rayleigh = 0.7; +// beestat.component.scene.mie_coefficient = 0.008; +// beestat.component.scene.mie_directional_g = 0.9; beestat.component.scene.shadow_map_size = 4096; @@ -32,6 +42,20 @@ beestat.component.scene.prototype.rerender = function() { this.add_floor_plan_(); }; +/** + * Set the width of this component. + * + * @param {number} width + */ +beestat.component.scene.prototype.set_width = function(width) { + this.width_ = width; + + this.camera_.aspect = this.width_ / this.height_; + this.camera_.updateProjectionMatrix(); + + this.renderer_.setSize(this.width_, this.height_); +}; + /** * Decorate * @@ -48,17 +72,21 @@ beestat.component.scene.prototype.decorate_ = function(parent) { 'watcher': false }; + this.width_ = this.state_.scene_width || 800; + this.height_ = 500; this.add_scene_(parent); + this.add_background_(parent); this.add_renderer_(parent); this.add_camera_(); this.add_controls_(parent); - this.add_sky_(); - this.add_moon_(); - this.add_moon_light_(); + // this.add_sky_(); + // this.add_moon_(); + // this.add_moon_light_(); this.add_sun_light_(); this.add_ambient_light_(); - this.add_ground_(); + // this.add_ground_(); + // this.add_ground_limited_(); this.add_group_(); this.add_floor_plan_(); @@ -85,6 +113,7 @@ beestat.component.scene.prototype.decorate_ = function(parent) { const animate = function() { requestAnimationFrame(animate); + self.controls_.update(); self.renderer_.render(self.scene_, self.camera_); }; animate(); @@ -134,7 +163,7 @@ beestat.component.scene.prototype.add_renderer_ = function(parent) { this.renderer_ = new THREE.WebGLRenderer({ 'antialias': true }); - this.renderer_.setSize(window.innerWidth / 2.2, window.innerHeight / 2.2); + this.renderer_.setSize(window.innerWidth /1.1, window.innerHeight / 1.1); this.renderer_.shadowMap.enabled = true; this.renderer_.shadowMap.autoUpdate = false; @@ -143,7 +172,8 @@ beestat.component.scene.prototype.add_renderer_ = function(parent) { * https://threejs.org/examples/webgl_shaders_sky.html */ this.renderer_.toneMapping = THREE.ACESFilmicToneMapping; - this.renderer_.toneMappingExposure = 0.5; + // this.renderer_.toneMappingExposure = 0.5; + this.renderer_.toneMappingExposure = 0.2; parent[0].appendChild(this.renderer_.domElement); }; @@ -175,7 +205,11 @@ beestat.component.scene.prototype.add_camera_ = function() { * @param {rocket.Elements} parent */ beestat.component.scene.prototype.add_controls_ = function(parent) { - new THREE.OrbitControls(this.camera_, parent[0]); // eslint-disable-line no-new + this.controls_ = new THREE.OrbitControls(this.camera_, parent[0]); + this.controls_.enableDamping = true; + this.controls_.enablePan = false; + this.controls_.maxDistance = 1000; + this.controls_.minDistance = 400; }; /** @@ -262,7 +296,8 @@ beestat.component.scene.prototype.add_sun_light_ = function() { beestat.component.scene.sun_light_intensity ); this.directional_light_sun_.castShadow = true; - this.directional_light_sun_.shadow.bias = -0.00009; + // this.directional_light_sun_.shadow.bias = -0.00009; + // this.directional_light_sun_.shadow.radius = 32; this.directional_light_sun_.shadow.mapSize.width = beestat.component.scene.shadow_map_size; this.directional_light_sun_.shadow.mapSize.height = beestat.component.scene.shadow_map_size; this.directional_light_sun_.shadow.camera.left = -1000; @@ -311,15 +346,88 @@ beestat.component.scene.prototype.add_ambient_light_ = function() { /** * Set the date and thus the position of the sun/moon. - * - * @param {Date} date */ -beestat.component.scene.prototype.set_date = function(date) { - const thermostat = beestat.cache.thermostat[this.thermostat_id_]; - const address = beestat.cache.address[thermostat.address_id]; +beestat.component.scene.prototype.update_ = function() { + const self = this; + + const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; + + const time = this.date_.format('HH:mm'); + + // TODO Memoize this + let gradient; + if (self.data_type_ === 'temperature') { + gradient = beestat.style.generate_gradient( + [ + beestat.style.hex_to_rgb(beestat.style.color.blue.dark), + beestat.style.hex_to_rgb(beestat.style.color.gray.base), + beestat.style.hex_to_rgb(beestat.style.color.red.dark) + ], + 100 + ); + } else if (self.data_type_ === 'occupancy') { + gradient = beestat.style.generate_gradient( + [ + beestat.style.hex_to_rgb(beestat.style.color.gray.base), + beestat.style.hex_to_rgb(beestat.style.color.orange.dark) + ], + 200 + ); + } + + // Set the color of each room + floor_plan.data.groups.forEach(function(group) { + group.rooms.forEach(function(room) { + let color; + if ( + room.sensor_id !== undefined && + self.data_.series[self.data_type_][room.sensor_id] !== undefined && + self.data_.series[self.data_type_][room.sensor_id][time] !== undefined + ) { + const value = self.data_.series[self.data_type_][room.sensor_id][time]; + + const percentage = Math.min( + 1, + Math.max( + 0, + (value - self.heat_map_min_) / (self.heat_map_max_ - self.heat_map_min_) + ) + ); + color = beestat.style.rgb_to_hex(gradient[Math.floor((gradient.length - 1) * percentage)]); + } else { + color = beestat.style.color.gray.dark; + } + + self.rooms_[room.room_id].material.color.setHex(color.replace('#', '0x')); + }); + }); + + let address; + if (floor_plan.address_id !== null) { // todo should be undefined? + address = beestat.cache.address[floor_plan.address_id]; + } else { + address = { + 'normalized': { + 'metadata': { + 'latitude': 0, + 'longitude': 0 + } + } + }; + } + // After sunset may need to hide the sun light source...or maybe wean it's brightness off or something. + // TODO TEMP TO KEEP LIGHTING CONSISTENT + // const date = new Date('2022-08-16 12:00:00'); + // const date = this.date_.toDate(); + const date = moment() + .hour(12) + .minute(0) + .second(0) + .toDate(); + const sun_object_vector = new THREE.Vector3(); const moon_object_vector = new THREE.Vector3(); @@ -386,29 +494,33 @@ beestat.component.scene.prototype.set_date = function(date) { beestat.component.scene.ambient_light_intensity_sky * Math.sin(sun_position.altitude) * (1 - eclipse_percentage) ); - this.moon_.material.opacity = 0.2; + // this.moon_.material.opacity = 0.2; /** * Mess up the sky during an eclipse */ // Turn down to 0 - this.sky_.material.uniforms.rayleigh.value = - beestat.component.scene.rayleigh * (1 - eclipse_percentage); + if (this.sky_ !== undefined) { + this.sky_.material.uniforms.rayleigh.value = + beestat.component.scene.rayleigh * (1 - eclipse_percentage); - // Turn down to almost 0 - this.sky_.material.uniforms.mieCoefficient.value = - Math.max( - 0.00001, - beestat.component.scene.mie_coefficient * (1 - eclipse_percentage) - ); + // Turn down to almost 0 + this.sky_.material.uniforms.mieCoefficient.value = + Math.max( + 0.00001, + beestat.component.scene.mie_coefficient * (1 - eclipse_percentage) + ); - // Increase to almost 1 - this.sky_.material.uniforms.mieDirectionalG.value = - Math.max( - beestat.component.scene.mie_directional_g, - 0.9999 * eclipse_percentage - ); + // Increase to almost 1 + this.sky_.material.uniforms.mieDirectionalG.value = + Math.max( + beestat.component.scene.mie_directional_g, + 0.9999 * eclipse_percentage + ); + + this.sky_.material.uniforms.sunPosition.value.copy(sun_object_vector); + } /* * this.renderer_.toneMappingExposure = Math.max( @@ -418,12 +530,23 @@ beestat.component.scene.prototype.set_date = function(date) { */ // Set the brightness of the sun - this.directional_light_sun_.intensity = - beestat.component.scene.sun_light_intensity * Math.sin(sun_position.altitude) * (1 - eclipse_percentage); + if (this.directional_light_sun_ !== undefined) { + this.directional_light_sun_.intensity = + beestat.component.scene.sun_light_intensity * Math.sin(sun_position.altitude) * (1 - eclipse_percentage); + this.directional_light_sun_.position.copy(sun_light_vector); + + } // Set the brightness of the moon - this.directional_light_moon_.intensity = - beestat.component.scene.moon_light_intensity * moon_illumination.fraction; + if (this.directional_light_moon_ !== undefined) { + this.directional_light_moon_.intensity = + beestat.component.scene.moon_light_intensity * moon_illumination.fraction; + this.directional_light_moon_.position.copy(moon_light_vector); + } + + if (this.moon_ !== undefined) { + this.moon_.position.copy(moon_object_vector); + } // TODO size of moon based on distance? Might not be worth it haha. @@ -433,12 +556,8 @@ beestat.component.scene.prototype.set_date = function(date) { */ // Update the directional light positions - this.directional_light_sun_.position.copy(sun_light_vector); - this.directional_light_moon_.position.copy(moon_light_vector); // Update the position of the sun and moon in the sky - this.sky_.material.uniforms.sunPosition.value.copy(sun_object_vector); - this.moon_.position.copy(moon_object_vector); // this.sky2_.material.uniforms.sunPosition.value.copy(moon_object_vector); // Update shadows @@ -484,8 +603,6 @@ beestat.component.scene.prototype.add_ground_ = function() { plane.rotation.x += Math.PI / 2; plane.receiveShadow = true; - - this.scene_.add(plane); if (this.debug_.grid === true) { @@ -502,6 +619,45 @@ beestat.component.scene.prototype.add_ground_ = function() { this.scene_.add(grid_helper); } }; +beestat.component.scene.prototype.add_ground_limited_ = function() { + const height = 24; + + const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_); + + const size = Math.max(bounding_box.width, bounding_box.height) + 120; + + // const texture = new THREE.TextureLoader().load('img/grass.jpg'); + // texture.wrapS = THREE.RepeatWrapping; + // texture.wrapT = THREE.RepeatWrapping; + // texture.repeat.set(1000, 1000); + + const geometry = new THREE.BoxGeometry(size, size, height); + const material = new THREE.MeshLambertMaterial({ + // 'map': texture, + 'color': new THREE.Color(beestat.style.color.green.dark), + 'side': THREE.DoubleSide + }); + const box = new THREE.Mesh(geometry, material); + box.translateY(height / -2); + box.rotation.x += Math.PI / 2; + // box.receiveShadow = true; + + this.scene_.add(box); + + if (this.debug_.grid === true) { + const grid_size = 100; + const grid_divisions = grid_size; + const grid_color_center_line = new THREE.Color(beestat.style.color.lightblue.base); + const grid_color_grid = new THREE.Color(beestat.style.color.green.base); + const grid_helper = new THREE.GridHelper( + grid_size, + grid_divisions, + grid_color_center_line, + grid_color_grid + ); + this.scene_.add(grid_helper); + } +}; @@ -512,11 +668,10 @@ beestat.component.scene.prototype.add_ground_ = function() { /** * Add a background. */ -/* - * beestat.component.scene.prototype.add_background_ = function() { - * this.scene_.background = new THREE.Color(beestat.style.color.bluegray.dark); - * } - */ +beestat.component.scene.prototype.add_background_ = function() { + this.scene_.background = new THREE.Color(beestat.style.color.bluegray.dark); +} + @@ -529,97 +684,58 @@ beestat.component.scene.prototype.add_ground_ = function() { * @param {object} room The room to add. */ beestat.component.scene.prototype.add_room_ = function(group, room) { - const color = new THREE.Color(beestat.style.color.blue.base); + const color = beestat.style.color.gray.base; var clipper_offset = new ClipperLib.ClipperOffset(); - clipper_offset.AddPath(room.points, ClipperLib.JoinType.jtMiter, ClipperLib.EndType.etClosedPolygon); + clipper_offset.AddPath( + room.points, + ClipperLib.JoinType.jtSquare, + ClipperLib.EndType.etClosedPolygon + ); var clipper_hole = new ClipperLib.Path(); - // var offsetted_paths = new ClipperLib.Path(); - clipper_offset.Execute(clipper_hole, -5); + clipper_offset.Execute(clipper_hole, -3); + + // Full height + // const extrude_height = (room.height || group.height) - 3; + + // Just the floor plan + const extrude_height = 6; + + - room.height = 12 * 1; // Create a shape using the points of the room. const shape = new THREE.Shape(); - const first_point = room.points[0]; + const first_point = clipper_hole[0].shift(); shape.moveTo(first_point.x, first_point.y); - room.points.forEach(function(point) { + clipper_hole[0].forEach(function(point) { shape.lineTo(point.x, point.y); }); - - // FLOOR - const floor = shape.clone(); - const floor_geometry = new THREE.ShapeGeometry(floor); - const floor_material = new THREE.MeshLambertMaterial({ - 'color': color, - 'side': THREE.DoubleSide - }); - const floor_mesh = new THREE.Mesh(floor_geometry, floor_material); - floor_mesh.position.z = ((room.elevation || group.elevation) + room.height) * -1; - - // Translate the floor_mesh to the room x/y position. - floor_mesh.translateX(room.x); - floor_mesh.translateY(room.y); - floor_mesh.translateZ(room.height - 1); - - // floor.rotation.x += Math.PI/2; - - // Shadows are neat. - floor_mesh.castShadow = true; - floor_mesh.receiveShadow = true; - - // Add the mesh to the group. - this.group_.add(floor_mesh); - - - - - - // Create a hole - const hole = new THREE.Shape(); - const hole_first_point = clipper_hole[0].shift(); - hole.moveTo(hole_first_point.x, hole_first_point.y); - clipper_hole[0].forEach(function(point) { - hole.lineTo(point.x, point.y); - }); - - // Hole - // const hole = shape.clone(); - shape.holes.push(hole); - -/* var paths = [[{x:30,y:30},{x:130,y:30},{x:130,y:130},{x:30,y:130}], - [{x:60,y:60},{x:60,y:100},{x:100,y:100},{x:100,y:60}]]; - var scale = 100; - ClipperLib.JS.ScaleUpPaths(paths, scale); - // Possibly ClipperLib.Clipper.SimplifyPolygons() here - // Possibly ClipperLib.Clipper.CleanPolygons() here - var co = new ClipperLib.ClipperOffset(2, 0.25); - - // ClipperLib.EndType = {etOpenSquare: 0, etOpenRound: 1, etOpenButt: 2, etClosedPolygon: 3, etClosedLine : 4 }; - co.AddPaths(paths, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon); - var offsetted_paths = new ClipperLib.Paths(); - co.Execute(offsetted_paths, -10 * scale);*/ - - - // Extrude the shape and create the mesh. const extrude_settings = { - 'depth': room.height, - 'bevelEnabled': false + 'depth': extrude_height, + 'bevelEnabled': true, + 'bevelThickness': 1, + 'bevelSize': 1, + 'bevelOffset': 1, + 'bevelSegments': 5 }; const geometry = new THREE.ExtrudeGeometry( shape, extrude_settings ); + // const material = new THREE.MeshBasicMaterial({ const material = new THREE.MeshPhongMaterial({ - // 'color': new THREE.Color(beestat.style.color.red.base) - 'color': color + 'color': color, + // 'transparent': true, + // 'opacity': 0.5 }); + // material.opacity = 0.5; const mesh = new THREE.Mesh(geometry, material); - mesh.position.z = ((room.elevation || group.elevation) + room.height) * -1; + mesh.position.z = -extrude_height - (room.elevation || group.elevation); // Translate the mesh to the room x/y position. mesh.translateX(room.x); @@ -627,10 +743,16 @@ beestat.component.scene.prototype.add_room_ = function(group, room) { // Shadows are neat. mesh.castShadow = true; - mesh.receiveShadow = true; + // mesh.receiveShadow = true; // Add the mesh to the group. this.group_.add(mesh); + + // Store a reference to the mesh representing each room. + if (this.rooms_ === undefined) { + this.rooms_ = {}; + } + this.rooms_[room.room_id] = mesh; }; /** @@ -671,7 +793,16 @@ beestat.component.scene.prototype.update_debug_ = function() { * Add a group containing all of the extruded geometry. */ beestat.component.scene.prototype.add_group_ = function() { + const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_); + // this.floor_plan_center_x_ = ; + // this.floor_plan_center_y_ = (bounding_box.bottom + bounding_box.top) / 2; + + // this.view_box_.x = center_x - (this.view_box_.width / 2); + // this.view_box_.y = center_y - (this.view_box_.height / 2); + this.group_ = new THREE.Group(); + this.group_.translateX((bounding_box.right + bounding_box.left) / -2); + this.group_.translateZ((bounding_box.bottom + bounding_box.top) / -2); // this.group_.rotation.x = -Math.PI / 2; // this.group_.rotation.x = Math.PI / 2; @@ -683,10 +814,79 @@ beestat.component.scene.prototype.add_group_ = function() { */ beestat.component.scene.prototype.add_floor_plan_ = function() { const self = this; - const floor_plan = beestat.cache.floor_plan[1]; + const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; + floor_plan.data.groups.forEach(function(group) { group.rooms.forEach(function(room) { self.add_room_(group, room); }); }); }; + +/** + * Set the current date. + * + * @param {moment} date + * + * @return {beestat.component.scene} + */ +beestat.component.scene.prototype.set_date = function(date) { + this.date_ = date; + + if (this.rendered_ === true) { + this.update_(); + } + + return this; +}; + +/** + * Set the type of data this scene is visualizing. + * + * @param {string} data_type temperature|occupancy + * + * @return {beestat.component.scene} + */ +beestat.component.scene.prototype.set_data_type = function(data_type) { + this.data_type_ = data_type; + + if (this.rendered_ === true) { + this.update_(); + } + + return this; +}; + +/** + * Set the min value of the heat map. + * + * @param {string} heat_map_min + * + * @return {beestat.component.scene} + */ +beestat.component.scene.prototype.set_heat_map_min = function(heat_map_min) { + this.heat_map_min_ = heat_map_min; + + if (this.rendered_ === true) { + this.update_(); + } + + return this; +}; + +/** + * Set the max value of the heat map. + * + * @param {string} heat_map_max + * + * @return {beestat.component.scene} + */ +beestat.component.scene.prototype.set_heat_map_max = function(heat_map_max) { + this.heat_map_max_ = heat_map_max; + + if (this.rendered_ === true) { + this.update_(); + } + + return this; +}; diff --git a/js/component/tile.js b/js/component/tile.js index c950e45..34ad239 100644 --- a/js/component/tile.js +++ b/js/component/tile.js @@ -17,10 +17,11 @@ beestat.component.tile.prototype.decorate_ = function(parent) { const background_color = this.background_color_ || 'none'; const text_color = this.text_color_ || '#fff'; const tabbable = this.tabbable_ || false; + const shadow = this.shadow_ === undefined ? true : this.shadow_; const display = this.display_ === 'block' ? 'flex' : 'inline-flex'; let border_radius; if (this.type_ === 'pill') { - border_radius = (this.get_size_() === 'large' ? 48 : 32); + border_radius = (this.get_size_() === 'large' ? 48 : 36); } else { border_radius = beestat.style.size.border_radius; } @@ -30,7 +31,7 @@ beestat.component.tile.prototype.decorate_ = function(parent) { Object.assign(this.container_.style, { 'background': background_color, 'border-radius': `${border_radius}px`, - 'height': `${(this.get_size_() === 'large' ? 48 : 32)}px`, + 'height': `${(this.get_size_() === 'large' ? 48 : 36)}px`, 'display': display, 'align-items': 'center', 'color': text_color, @@ -40,12 +41,18 @@ beestat.component.tile.prototype.decorate_ = function(parent) { 'text-align': 'left' }); + if (shadow === true) { + Object.assign(this.container_.style, { + 'box-shadow': '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)' + }); + } + parent.appendChild(this.container_); // Padding. Basically for icon only make it a nice square button. if (this.get_text_() === undefined) { Object.assign(this.container_.style, { - 'width': `${(this.get_size_() === 'large' ? 48 : 32)}px`, + 'width': `${(this.get_size_() === 'large' ? 48 : 36)}px`, 'justify-content': 'center' }); } else { @@ -230,7 +237,8 @@ beestat.component.tile.prototype.get_icon_ = function() { /** * Set the text of the button. * - * @param {string|array} text A single string or array of strings. If an array is passed multiple lines of text will be shown. + * @param {string|array} text A single string or array of strings. If an array + * is passed multiple lines of text will be shown. * * @return {beestat.component.tile} This. */ @@ -268,6 +276,21 @@ beestat.component.tile.prototype.set_background_color = function(background_colo return this; }; +/** + * Set whether or not there is a shadow. Default true. + * + * @param {boolean} shadow + * + * @return {beestat.component.tile} This. + */ +beestat.component.tile.prototype.set_shadow = function(shadow) { + this.shadow_ = shadow; + if (this.rendered_ === true) { + this.rerender(); + } + return this; +}; + /** * Set the text color. * diff --git a/js/component/tile_group.js b/js/component/tile_group.js index 5a8e951..c10e725 100644 --- a/js/component/tile_group.js +++ b/js/component/tile_group.js @@ -2,7 +2,7 @@ * A group of tiles. */ beestat.component.tile_group = function() { - this.buttons_ = []; + this.tiles_ = []; beestat.component.apply(this, arguments); }; beestat.extend(beestat.component.tile_group, beestat.component); @@ -13,53 +13,37 @@ beestat.extend(beestat.component.tile_group, beestat.component); * @param {rocket.Elements} parent */ beestat.component.tile_group.prototype.decorate_ = function(parent) { - var self = this; + const flex = document.createElement('div'); + Object.assign(flex.style, { + 'display': 'inline-flex', + 'flex-wrap': 'wrap', + 'grid-gap': `${beestat.style.size.gutter / 2}px` + }); + parent.appendChild(flex); - // Only exists so that there can be spacing between wrapped elements. - var outer_container = $.createElement('div') - .style({ - 'margin-top': (beestat.style.size.gutter / -2) - }); - parent.appendChild(outer_container); - - this.buttons_.forEach(function(button, i) { - var container = $.createElement('div').style({ - 'display': 'inline-block', - 'margin-right': (i < self.buttons_.length) ? (beestat.style.size.gutter / 2) : 0, - 'margin-top': (beestat.style.size.gutter / 2) - }); - button.render(container); - outer_container.appendChild(container); + this.tiles_.forEach(function(tile) { + tile.render($(flex)); }); }; /** - * Add a button to this group. + * Add a tile to this group. * - * @param {beestat.component.button} button The button to add. + * @param {beestat.component.tile} tile The tile to add. */ -beestat.component.tile_group.prototype.add_button = function(button) { - this.buttons_.push(button); +beestat.component.tile_group.prototype.add_tile = function(tile) { + this.tiles_.push(tile); if (this.rendered_ === true) { this.rerender(); } }; /** - * Get all of the buttons in this button group. - * - * @return {[beestat.component.button]} The buttons in this group. - */ -beestat.component.tile_group.prototype.get_buttons = function() { - return this.buttons_; -}; - -/** - * Remove this component from the page. Disposes the buttons first. + * Remove this component from the page. Disposes the tiles first. */ beestat.component.tile_group.prototype.dispose = function() { - this.buttons_.forEach(function(button) { - button.dispose(); + this.tiles_.forEach(function(tile) { + tile.dispose(); }); beestat.component.prototype.dispose.apply(this, arguments); }; diff --git a/js/js.php b/js/js.php index 0edf9d4..9ccc191 100755 --- a/js/js.php +++ b/js/js.php @@ -38,7 +38,6 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -77,7 +76,8 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; + echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -119,12 +119,12 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; diff --git a/js/layer/visualize.js b/js/layer/visualize.js index 59f982d..6b2e5aa 100644 --- a/js/layer/visualize.js +++ b/js/layer/visualize.js @@ -33,7 +33,21 @@ beestat.layer.visualize.prototype.decorate_ = function(parent) { cards.push([ { - 'card': new beestat.component.card.early_access(), + 'card': new beestat.component.card.visualize_settings(), + 'size': 12 + } + ]); + + const three_d = new beestat.component.card.three_d() + .set_floor_plan_id(beestat.setting('visualize.floor_plan_id')); + + beestat.dispatcher.addEventListener('setting.visualize.floor_plan_id', function() { + three_d.set_floor_plan_id(beestat.setting('visualize.floor_plan_id')); + }); + + cards.push([ + { + 'card': three_d, 'size': 12 } ]);