From 8237467e8b246e7d1d7d6cde762ef605d8837214 Mon Sep 17 00:00:00 2001 From: Jon Ziebell Date: Sat, 3 Sep 2022 23:16:08 -0400 Subject: [PATCH] Added settings for temperature, distance, and area. --- js/beestat/area.js | 73 ++++++++++ js/beestat/comparisons.js | 2 +- js/beestat/distance.js | 84 ++++++++++++ js/beestat/floor_plan.js | 8 +- js/beestat/setting.js | 2 - js/beestat/temperature.js | 4 +- js/component/card/comparison_settings.js | 2 +- js/component/card/floor_plan_editor.js | 126 +++++++++++++----- js/component/card/my_home.js | 2 +- js/component/card/settings.js | 70 ++++++++++ js/component/card/temperature_profiles.js | 2 +- js/component/card/three_d.js | 2 +- js/component/card/visualize_settings.js | 16 +-- .../runtime_sensor_detail_temperature.js | 4 +- .../runtime_thermostat_detail_temperature.js | 4 +- .../chart/runtime_thermostat_summary.js | 8 +- js/component/chart/temperature_profiles.js | 10 +- js/component/floor_plan.js | 14 +- js/component/floor_plan_entity/wall.js | 25 ++-- js/component/input.js | 2 +- js/component/input/checkbox.js | 2 +- js/component/input/radio.js | 2 +- js/component/input/text.js | 29 ++++ js/component/metric/balance_point.js | 2 +- js/component/metric/setback.js | 2 +- js/component/metric/setpoint.js | 2 +- js/component/modal/create_floor_plan.js | 42 ++++-- js/component/radio_group.js | 36 ++++- js/component/tile/floor_plan.js | 9 +- js/component/tile/floor_plan_group.js | 8 +- js/js.php | 3 + js/layer/load.js | 34 ++++- js/lib/polylabel/polylabel.js | 3 + 33 files changed, 523 insertions(+), 111 deletions(-) create mode 100644 js/beestat/area.js create mode 100644 js/beestat/distance.js create mode 100644 js/lib/polylabel/polylabel.js diff --git a/js/beestat/area.js b/js/beestat/area.js new file mode 100644 index 0000000..cf581e0 --- /dev/null +++ b/js/beestat/area.js @@ -0,0 +1,73 @@ +/** + * Format a area in a number of different ways. + * + * @param {object} args Instructions on how to format: + * area (required) - area to work with + * output_area_unit (optional, default ft) - Output area unit; default matches setting. + * convert (optional, default true) - Whether or not to convert to Celcius if necessary + * round (optional, default 1) - Number of decimal points to round to + * units (optional, default false) - Whether or not to include units in the result + * type (optional, default number) - Type of value to return (string|number) + * + * @return {string} The formatted area. + */ +beestat.area = function(args) { + // Allow passing a single argument of area for convenience. + if (typeof args !== 'object' || args === null) { + args = { + 'area': args + }; + } + + const input_area_unit = 'in²'; + var output_area_unit = beestat.default_value( + args.output_area_unit, + beestat.setting('units.area') + ); + var round = beestat.default_value(args.round, 1); + var units = beestat.default_value(args.units, false); + var type = beestat.default_value(args.type, 'number'); + + var area = parseFloat(args.area); + + // Check for invalid values. + if (isNaN(area) === true || isFinite(area) === false) { + return null; + } + + const conversion_factors = { + 'in²': { + 'ft²': 0.00694444, + 'm²': 0.00064516 + } + }; + + // Convert if necessary and asked for. + if (input_area_unit !== output_area_unit) { + area *= conversion_factors[input_area_unit][output_area_unit]; + } + + /* + * Get to the appropriate number of decimal points. This will turn the number + * into a string. Then do a couple silly operations to fix -0.02 from showing + * up as -0.0 in string form. + */ + area = area.toFixed(round); + area = parseFloat(area); + area = area.toFixed(round); + + /* + * Convert the previous string back to a number if requested. Format matters + * because HighCharts doesn't accept strings in some cases. + */ + if (type === 'number' && units === false) { + area = Number(area); + } + + // Append units if asked for. + if (units === true) { + area = Number(area).toLocaleString() + ' ' + output_area_unit; + } + + return area; +}; diff --git a/js/beestat/comparisons.js b/js/beestat/comparisons.js index 953a3fd..ac5943d 100644 --- a/js/beestat/comparisons.js +++ b/js/beestat/comparisons.js @@ -36,7 +36,7 @@ beestat.comparisons.get_attributes = function() { }; } - // Always a 1000sqft size delta on both sides (total 2000 sqft). + // Always a 1000ft² size delta on both sides (total 2000 ft²). if (thermostat.property.square_feet !== null) { var property_square_feet_delta = 1000; var min_property_square_feet = Math.max( diff --git a/js/beestat/distance.js b/js/beestat/distance.js new file mode 100644 index 0000000..114d8d5 --- /dev/null +++ b/js/beestat/distance.js @@ -0,0 +1,84 @@ +/** + * Format a distance in a number of different ways. + * + * @param {object} args Instructions on how to format: + * distance (required) - distance to work with + * output_distance_unit (optional, default ft) - Output distance unit; default matches setting. + * convert (optional, default true) - Whether or not to convert to Celcius if necessary + * round (optional, default 1) - Number of decimal points to round to + * units (optional, default false) - Whether or not to include units in the result + * type (optional, default number) - Type of value to return (string|number) + * + * @return {string} The formatted distance. + */ +beestat.distance = function(args) { + // Allow passing a single argument of distance for convenience. + if (typeof args !== 'object' || args === null) { + args = { + 'distance': args + }; + } + + var input_distance_unit = beestat.default_value( + args.input_distance_unit, + 'in' + ); + var output_distance_unit = beestat.default_value( + args.output_distance_unit, + beestat.setting('units.distance') + ); + var round = beestat.default_value(args.round, 1); + var units = beestat.default_value(args.units, false); + var type = beestat.default_value(args.type, 'number'); + + var distance = parseFloat(args.distance); + + // Check for invalid values. + if (isNaN(distance) === true || isFinite(distance) === false) { + return null; + } + + const conversion_factors = { + 'in': { + 'ft': 0.0833, + 'm': 0.0254 + }, + 'm': { + 'in': 39.3701, + 'ft': 3.28084 + }, + 'ft': { + 'm': 0.3048, + 'in': 12 + } + }; + + // Convert if necessary and asked for. + if (input_distance_unit !== output_distance_unit) { + distance *= conversion_factors[input_distance_unit][output_distance_unit]; + } + + /* + * Get to the appropriate number of decimal points. This will turn the number + * into a string. Then do a couple silly operations to fix -0.02 from showing + * up as -0.0 in string form. + */ + distance = distance.toFixed(round); + distance = parseFloat(distance); + distance = distance.toFixed(round); + + /* + * Convert the previous string back to a number if requested. Format matters + * because HighCharts doesn't accept strings in some cases. + */ + if (type === 'number' && units === false) { + distance = Number(distance); + } + + // Append units if asked for. + if (units === true) { + distance = Number(distance).toLocaleString() + ' ' + output_distance_unit; + } + + return distance; +}; diff --git a/js/beestat/floor_plan.js b/js/beestat/floor_plan.js index c6a9fc0..34a5eb5 100644 --- a/js/beestat/floor_plan.js +++ b/js/beestat/floor_plan.js @@ -5,7 +5,7 @@ beestat.floor_plan = {}; * * @param {number} floor_plan_id * - * @return {number} The area of the floor plan in sqft. + * @return {number} The area of the floor plan as in². */ beestat.floor_plan.get_area = function(floor_plan_id) { const floor_plan = beestat.cache.floor_plan[floor_plan_id]; @@ -24,7 +24,7 @@ beestat.floor_plan.get_area = function(floor_plan_id) { * @param {object} group The group. * @param {boolean} round Whether or not to round the result. * - * @return {number} Area of the group in sqft. + * @return {number} Area of the group as in². */ beestat.floor_plan.get_area_group = function(group, round = true) { let area = 0; @@ -46,10 +46,10 @@ beestat.floor_plan.get_area_group = function(group, round = true) { * @param {object} room The room. * @param {boolean} round Whether or not to round the result. * - * @return {number} Area of the room in sqft. + * @return {number} Area of the room as in². */ beestat.floor_plan.get_area_room = function(room, round = true) { - let area = Math.abs(ClipperLib.Clipper.Area(room.points) / 144); + let area = Math.abs(ClipperLib.Clipper.Area(room.points)); if (round === true) { return Math.round(area); diff --git a/js/beestat/setting.js b/js/beestat/setting.js index 345d69a..d825e78 100644 --- a/js/beestat/setting.js +++ b/js/beestat/setting.js @@ -62,8 +62,6 @@ beestat.setting = function(argument_1, opt_value, opt_callback) { 'comparison_region': 'global', 'comparison_property_type': 'similar', - 'temperature_unit': '°F', - 'first_run': true, 'thermostat.#.profile.ignore_solar_gain': false, diff --git a/js/beestat/temperature.js b/js/beestat/temperature.js index 0f9aa90..d8a4b69 100644 --- a/js/beestat/temperature.js +++ b/js/beestat/temperature.js @@ -6,7 +6,7 @@ * @param {object} args Instructions on how to format: * temperature (required) - Temperature to work with * input_temperature_unit (optional, default °F) - Input temperature unit - * output_temperature_unit (optional, default °F|°C) - Input temperature unit; default matches setting. + * output_temperature_unit (optional, default current setting) - Output temperature unit; default matches setting. * convert (optional, default true) - Whether or not to convert to Celcius if necessary * delta (optional, default false) - Whether or not the convert action is for a delta instead of a normal value * round (optional, default 1) - Number of decimal points to round to @@ -29,7 +29,7 @@ beestat.temperature = function(args) { ); var output_temperature_unit = beestat.default_value( args.output_temperature_unit, - beestat.setting('temperature_unit') + beestat.setting('units.temperature') ); var delta = beestat.default_value(args.delta, false); var round = beestat.default_value(args.round, 1); diff --git a/js/component/card/comparison_settings.js b/js/component/card/comparison_settings.js index 65b68c3..67d58a3 100644 --- a/js/component/card/comparison_settings.js +++ b/js/component/card/comparison_settings.js @@ -299,7 +299,7 @@ beestat.component.card.comparison_settings.prototype.decorate_detail_ = function } if (comparison_attributes.property_square_feet !== undefined) { - strings.push(this.get_comparison_string_(comparison_attributes.property_square_feet, 'sqft')); + strings.push(this.get_comparison_string_(comparison_attributes.property_square_feet, 'ft²')); } else { strings.push('Any square footage'); } diff --git a/js/component/card/floor_plan_editor.js b/js/component/card/floor_plan_editor.js index 642b588..279bf4e 100644 --- a/js/component/card/floor_plan_editor.js +++ b/js/component/card/floor_plan_editor.js @@ -331,7 +331,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f .set_label('Floor Name') .set_placeholder('Unnamed Floor') .set_width('100%') - .set_maxlength('50') + .set_maxlength(50) .set_requirements({ 'required': true }) @@ -354,26 +354,41 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f div = $.createElement('div'); grid.appendChild(div); const elevation_input = new beestat.component.input.text() - .set_label('Elevation (feet)') - .set_placeholder(this.state_.active_group.elevation / 12) - .set_value(this.state_.active_group.elevation / 12 || '') + .set_label('Elevation (' + beestat.setting('units.distance') + ')') + .set_placeholder(beestat.distance({ + 'distance': this.state_.active_group.elevation, + 'round': 2 + })) + .set_value(beestat.distance({ + 'distance': this.state_.active_group.elevation, + 'round': 2 + }) || '') .set_width('100%') - .set_maxlength('5') + .set_maxlength(5) .set_requirements({ - 'type': 'integer', - 'min_value': -50, - 'max_value': 50, + 'type': 'decimal', + 'min_value': beestat.distance(-600), + 'max_value': beestat.distance(600), 'required': true }) + .set_transform({ + 'type': 'round', + 'decimals': 2 + }) .render(div); elevation_input.addEventListener('change', function() { if (elevation_input.meets_requirements() === true) { - self.state_.active_group.elevation = elevation_input.get_value() * 12; + self.state_.active_group.elevation = beestat.distance({ + 'distance': elevation_input.get_value(), + 'input_distance_unit': beestat.setting('units.distance'), + 'output_distance_unit': 'in', + 'round': 2 + }); self.update_floor_plan_(); self.rerender(); } else { - elevation_input.set_value(self.state_.active_group.elevation / 12, false); + elevation_input.set_value(beestat.distance(self.state_.active_group.elevation), false); new beestat.component.modal.floor_plan_elevation_help().render(); } }); @@ -382,21 +397,36 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f div = $.createElement('div'); grid.appendChild(div); const height_input = new beestat.component.input.text() - .set_label('Ceiling Height (feet)') - .set_placeholder(this.state_.active_group.height / 12) - .set_value(this.state_.active_group.height / 12 || '') + .set_label('Ceiling Height (' + beestat.setting('units.distance') + ')') + .set_placeholder(beestat.distance({ + 'distance': this.state_.active_group.height, + 'round': 2 + })) + .set_value(beestat.distance({ + 'distance': this.state_.active_group.height, + 'round': 2 + }) || '') .set_width('100%') - .set_maxlength('4') + .set_maxlength(5) .set_requirements({ - 'type': 'integer', - 'min_value': 1, + 'type': 'decimal', + 'min_value': beestat.distance(60), 'required': true }) + .set_transform({ + 'type': 'round', + 'decimals': 2 + }) .render(div); height_input.addEventListener('change', function() { if (height_input.meets_requirements() === true) { - self.state_.active_group.height = height_input.get_value() * 12; + self.state_.active_group.height = beestat.distance({ + 'distance': height_input.get_value(), + 'input_distance_unit': beestat.setting('units.distance'), + 'output_distance_unit': 'in', + 'round': 2 + }); self.update_floor_plan_(); } else { height_input.set_value(self.state_.active_group.height, false); @@ -433,7 +463,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu .set_label('Room Name') .set_placeholder('Unnamed Room') .set_width('100%') - .set_maxlength('50') + .set_maxlength(50) .set_requirements({ 'required': true }) @@ -456,21 +486,36 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu div = $.createElement('div'); grid.appendChild(div); const elevation_input = new beestat.component.input.text() - .set_label('Elevation (feet)') - .set_placeholder(this.state_.active_group.elevation / 12) - .set_value(this.state_.active_room_entity.get_room().elevation / 12 || '') + .set_label('Elevation (' + beestat.setting('units.distance') + ')') + .set_placeholder(beestat.distance({ + 'distance': this.state_.active_group.elevation, + 'round': 2 + })) + .set_value(beestat.distance({ + 'distance': this.state_.active_room_entity.get_room().elevation, + 'round': 2 + }) || '') .set_width('100%') - .set_maxlength('5') + .set_maxlength(5) .set_requirements({ - 'min_value': -50, - 'max_value': 50, - 'type': 'integer' + 'type': 'decimal', + 'min_value': beestat.distance(-600), + 'max_value': beestat.distance(600) + }) + .set_transform({ + 'type': 'round', + 'decimals': 2 }) .render(div); elevation_input.addEventListener('change', function() { if (elevation_input.meets_requirements() === true) { - self.state_.active_room_entity.get_room().elevation = elevation_input.get_value() * 12; + self.state_.active_room_entity.get_room().elevation = beestat.distance({ + 'distance': elevation_input.get_value(), + 'input_distance_unit': beestat.setting('units.distance'), + 'output_distance_unit': 'in', + 'round': 2 + }); self.update_floor_plan_(); self.rerender(); } else { @@ -483,20 +528,35 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu div = $.createElement('div'); grid.appendChild(div); const height_input = new beestat.component.input.text() - .set_label('Ceiling Height (feet)') - .set_placeholder(this.state_.active_group.height / 12) - .set_value(this.state_.active_room_entity.get_room().height / 12 || '') + .set_label('Ceiling Height (' + beestat.setting('units.distance') + ')') + .set_placeholder(beestat.distance({ + 'distance': this.state_.active_group.height, + 'round': 2 + })) + .set_value(beestat.distance({ + 'distance': this.state_.active_room_entity.get_room().height, + 'round': 2 + }) || '') .set_width('100%') - .set_maxlength('4') + .set_maxlength(5) .set_requirements({ - 'type': 'integer', - 'min_value': 1 + 'type': 'decimal', + 'min_value': beestat.distance(60) + }) + .set_transform({ + 'type': 'round', + 'decimals': 2 }) .render(div); height_input.addEventListener('change', function() { if (height_input.meets_requirements() === true) { - self.state_.active_room_entity.get_room().height = height_input.get_value() * 12; + self.state_.active_room_entity.get_room().height = beestat.distance({ + 'distance': height_input.get_value(), + 'input_distance_unit': beestat.setting('units.distance'), + 'output_distance_unit': 'in', + 'round': 2 + }); self.update_floor_plan_(); } else { height_input.set_value('', false); diff --git a/js/component/card/my_home.js b/js/component/card/my_home.js index b877ad3..117ad66 100644 --- a/js/component/card/my_home.js +++ b/js/component/card/my_home.js @@ -186,7 +186,7 @@ beestat.component.card.my_home.prototype.decorate_property_ = function(parent) { .set_background_color(beestat.style.color.purple.base) .set_text_color('#fff') .set_icon('view_quilt') - .set_text(Number(thermostat.property.square_feet).toLocaleString() + ' sqft')); + .set_text(Number(thermostat.property.square_feet).toLocaleString() + ' ft²')); } if (thermostat.property.age !== null) { diff --git a/js/component/card/settings.js b/js/component/card/settings.js index 10e1b57..27a3160 100644 --- a/js/component/card/settings.js +++ b/js/component/card/settings.js @@ -16,6 +16,73 @@ beestat.component.card.settings.prototype.decorate_contents_ = function(parent) beestat.setting('thermostat_id') ]; + /** + * Units + */ + parent.appendChild( + $.createElement('p') + .style('font-weight', '400') + .innerText('Units') + ); + + // Temperature + parent.appendChild( + $.createElement('p') + .innerText('Temperature') + ); + + const temperature_radio_group = new beestat.component.radio_group() + .set_arrangement('horizontal'); + [ + '°F', + '°C' + ].forEach(function(temperature_unit) { + temperature_radio_group.add_radio( + new beestat.component.input.radio() + .set_label(temperature_unit) + .set_value(temperature_unit) + .set_checked(beestat.setting('units.temperature') === temperature_unit) + ); + }); + + temperature_radio_group.addEventListener('change', function() { + beestat.setting('units.temperature', temperature_radio_group.get_value()); + }); + + temperature_radio_group.render(parent); + + // Distance + parent.appendChild( + $.createElement('p') + .innerText('Distance / Area') + ); + + const distance_radio_group = new beestat.component.radio_group() + .set_arrangement('horizontal'); + [ + 'ft', + 'm' + ].forEach(function(distance_unit) { + distance_radio_group.add_radio( + new beestat.component.input.radio() + .set_label(distance_unit + ' / ' + distance_unit + '²') + .set_value(distance_unit) + .set_checked(beestat.setting('units.distance') === distance_unit) + ); + }); + + distance_radio_group.addEventListener('change', function() { + beestat.setting({ + 'units.distance': distance_radio_group.get_value(), + 'units.area': distance_radio_group.get_value() + '²' + }); + }); + + distance_radio_group.render(parent); + + /** + * Thermosat Summary + */ parent.appendChild( $.createElement('p') .style('font-weight', '400') @@ -58,6 +125,9 @@ beestat.component.card.settings.prototype.decorate_contents_ = function(parent) ); }); + /** + * Temperature Profiles + */ parent.appendChild( $.createElement('p') .style({ diff --git a/js/component/card/temperature_profiles.js b/js/component/card/temperature_profiles.js index 8ffbb76..d40994c 100644 --- a/js/component/card/temperature_profiles.js +++ b/js/component/card/temperature_profiles.js @@ -144,7 +144,7 @@ beestat.component.card.temperature_profiles.prototype.get_data_ = function() { */ var increment; var fixed; - if (beestat.setting('temperature_unit') === '°F') { + if (beestat.setting('units.temperature') === '°F') { increment = 1; fixed = 0; } else { diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index 59230d6..c453648 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -625,7 +625,7 @@ beestat.component.card.three_d.prototype.decorate_legend_ = function(parent) { if (beestat.setting('visualize.data_type') === 'temperature') { min = beestat.temperature(min); max = beestat.temperature(max); - units = beestat.setting('temperature_unit'); + units = beestat.setting('units.temperature'); } else { min *= 100; max *= 100; diff --git a/js/component/card/visualize_settings.js b/js/component/card/visualize_settings.js index 1f3c585..04f1ffa 100644 --- a/js/component/card/visualize_settings.js +++ b/js/component/card/visualize_settings.js @@ -176,6 +176,10 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_type_ = fu 'type': type, 'required': true }) + .set_transform({ + 'type': 'round', + 'decimals': 1 + }) .set_value( beestat.temperature(beestat.setting( 'visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.min' @@ -184,15 +188,11 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_type_ = fu .set_width(50); min.addEventListener('change', function() { if (min.meets_requirements() === true) { - // Round to one decimal. - const value = Math.round(min.get_value() * 10) / 10; - min.set_value(value, false); - beestat.setting( 'visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.min', beestat.temperature({ - 'temperature': value, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'temperature': min.get_value(), + 'input_temperature_unit': beestat.setting('units.temperature'), 'output_temperature_unit': '°F' }) ); @@ -230,7 +230,7 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_type_ = fu 'visualize.heat_map_absolute.' + beestat.setting('visualize.data_type') + '.max', beestat.temperature({ 'temperature': max.get_value(), - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'output_temperature_unit': '°F' }) ); @@ -268,7 +268,7 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_type_ = fu span = document.createElement('span'); switch (beestat.setting('visualize.data_type')) { case 'temperature': - span.innerText = beestat.setting('temperature_unit'); + span.innerText = beestat.setting('units.temperature'); break; case 'occupancy': span.innerText = '%'; diff --git a/js/component/chart/runtime_sensor_detail_temperature.js b/js/component/chart/runtime_sensor_detail_temperature.js index 2a0a1d4..1436398 100644 --- a/js/component/chart/runtime_sensor_detail_temperature.js +++ b/js/component/chart/runtime_sensor_detail_temperature.js @@ -118,7 +118,7 @@ beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_ 'labels': { 'style': {'color': beestat.style.color.gray.base}, 'formatter': function() { - return this.value + beestat.setting('temperature_unit'); + return this.value + beestat.setting('units.temperature'); } } } @@ -230,7 +230,7 @@ beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_ } else { value = beestat.temperature({ 'temperature': point.value, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true }); point_value = point.value; diff --git a/js/component/chart/runtime_thermostat_detail_temperature.js b/js/component/chart/runtime_thermostat_detail_temperature.js index b66602b..c17811c 100644 --- a/js/component/chart/runtime_thermostat_detail_temperature.js +++ b/js/component/chart/runtime_thermostat_detail_temperature.js @@ -131,7 +131,7 @@ beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_opti 'labels': { 'style': {'color': beestat.style.color.gray.base}, 'formatter': function() { - return this.value + beestat.setting('temperature_unit'); + return this.value + beestat.setting('units.temperature'); } } }, @@ -288,7 +288,7 @@ beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_opti value = beestat.temperature({ 'temperature': value, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true }); } else if (point.series_code.includes('humidity') === true) { diff --git a/js/component/chart/runtime_thermostat_summary.js b/js/component/chart/runtime_thermostat_summary.js index c3d7ec5..b1cb2dd 100755 --- a/js/component/chart/runtime_thermostat_summary.js +++ b/js/component/chart/runtime_thermostat_summary.js @@ -187,7 +187,7 @@ beestat.component.chart.runtime_thermostat_summary.prototype.get_options_yAxis_ 'color': beestat.style.color.gray.base }, 'formatter': function() { - return this.value + beestat.setting('temperature_unit'); + return this.value + beestat.setting('units.temperature'); } } } @@ -234,14 +234,14 @@ beestat.component.chart.runtime_thermostat_summary.prototype.get_options_tooltip ) { value = beestat.temperature({ 'temperature': values.min_outdoor_temperature, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true, 'round': 0 }); value += ' to '; value += beestat.temperature({ 'temperature': values.max_outdoor_temperature, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true, 'round': 0 }); @@ -252,7 +252,7 @@ beestat.component.chart.runtime_thermostat_summary.prototype.get_options_tooltip color = point.series.color; value = beestat.temperature({ 'temperature': values.avg_outdoor_temperature, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true, 'round': 0 }); diff --git a/js/component/chart/temperature_profiles.js b/js/component/chart/temperature_profiles.js index d71d806..4c7ba87 100644 --- a/js/component/chart/temperature_profiles.js +++ b/js/component/chart/temperature_profiles.js @@ -17,7 +17,7 @@ beestat.extend(beestat.component.chart.temperature_profiles, beestat.component.c */ beestat.component.chart.temperature_profiles.prototype.get_options_xAxis_labels_formatter_ = function() { return function() { - return this.value + beestat.setting('temperature_unit'); + return this.value + beestat.setting('units.temperature'); }; }; @@ -261,7 +261,7 @@ beestat.component.chart.temperature_profiles.prototype.get_options_yAxis_ = func 'labels': { 'style': {'color': beestat.style.color.gray.base}, 'formatter': function() { - return this.value + beestat.setting('temperature_unit'); + return this.value + beestat.setting('units.temperature'); } }, 'min': y_min, @@ -296,7 +296,7 @@ beestat.component.chart.temperature_profiles.prototype.get_options_tooltip_forma var value = beestat.temperature({ 'temperature': point.y, 'units': true, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'delta': true, 'type': 'string' }) + ' / h'; @@ -321,7 +321,7 @@ beestat.component.chart.temperature_profiles.prototype.get_options_tooltip_forma 'temperature': this.x, 'round': 0, 'units': true, - 'input_temperature_unit': beestat.setting('temperature_unit') + 'input_temperature_unit': beestat.setting('units.temperature') }), sections ); @@ -380,7 +380,7 @@ beestat.component.chart.temperature_profiles.prototype.get_options_xAxis_ = func 'useHTML': true, 'text': 'Now: ' + beestat.temperature({ 'temperature': this.data_.metadata.chart.outdoor_temperature, - 'input_temperature_unit': beestat.setting('temperature_unit'), + 'input_temperature_unit': beestat.setting('units.temperature'), 'units': true, 'round': 0 }) diff --git a/js/component/floor_plan.js b/js/component/floor_plan.js index ddad377..1c0d652 100644 --- a/js/component/floor_plan.js +++ b/js/component/floor_plan.js @@ -657,14 +657,20 @@ beestat.component.floor_plan.prototype.update_infobox = function() { if (this.state_.active_room_entity !== undefined) { parts.push(this.state_.active_room_entity.get_room().name || 'Unnamed Room'); parts.push( - beestat.floor_plan.get_area_room(this.state_.active_room_entity.get_room()) - .toLocaleString() + ' sqft' + beestat.area({ + 'area': beestat.floor_plan.get_area_room(this.state_.active_room_entity.get_room()), + 'round': 0, + 'units': true + }) ); } else { parts.push(this.state_.active_group.name || 'Unnamed Floor'); parts.push( - beestat.floor_plan.get_area_group(this.state_.active_group) - .toLocaleString() + ' sqft' + beestat.area({ + 'area': beestat.floor_plan.get_area_group(this.state_.active_group), + 'round': 0, + 'units': true + }) ); } this.infobox_container_.innerText(parts.join(' • ')); diff --git a/js/component/floor_plan_entity/wall.js b/js/component/floor_plan_entity/wall.js index 3522b96..2d3cec3 100644 --- a/js/component/floor_plan_entity/wall.js +++ b/js/component/floor_plan_entity/wall.js @@ -46,9 +46,6 @@ beestat.component.floor_plan_entity.wall.prototype.decorate_line_ = function(par this.path_.addEventListener('mousedown', function() { self.dispatchEvent('mousedown'); }); - // this.path_.addEventListener('touchstart', function() { - // self.dispatchEvent('mousedown'); - // }); this.decorate_text_(parent); @@ -236,14 +233,24 @@ beestat.component.floor_plan_entity.wall.prototype.update_text_ = function() { this.text_.style.fontSize = '11px'; } - const length_feet = Math.floor(length / 12); - const length_inches = length % 12; + let length_string; + if (beestat.setting('units.distance') === 'ft') { + const length_feet = Math.floor(length / 12); + const length_inches = length % 12; - let length_parts = []; - length_parts.push(length_feet + '\''); - length_parts.push(length_inches + '"'); + let length_parts = []; + length_parts.push(length_feet + '\''); + length_parts.push(length_inches + '"'); + + length_string = length_parts.join(' '); + } else { + length_string = beestat.distance({ + 'distance': length, + 'units': true, + 'round': 2 + }); + } - const length_string = length_parts.join(' '); this.text_path_.textContent = length_string; }; diff --git a/js/component/input.js b/js/component/input.js index ab09880..46660a2 100644 --- a/js/component/input.js +++ b/js/component/input.js @@ -80,7 +80,7 @@ beestat.component.input.prototype.meets_requirements = function() { this.requirements_.regexp = /^-?\d+$/; break; case 'decimal': - this.requirements_.regexp = /^-?\d+(?:\.\d+)?$/; + this.requirements_.regexp = /^-?\d*(?:\.\d+)?$/; break; } diff --git a/js/component/input/checkbox.js b/js/component/input/checkbox.js index 8264520..5920f41 100644 --- a/js/component/input/checkbox.js +++ b/js/component/input/checkbox.js @@ -36,7 +36,7 @@ beestat.component.input.checkbox.prototype.decorate_ = function(parent) { const span = document.createElement('span'); span.style.cursor = 'pointer'; - span.style.marginLeft = (beestat.style.size.gutter / 2) + 'px'; + span.style.paddingLeft = (beestat.style.size.gutter / 4) + 'px'; span.innerText = this.label_; span.addEventListener('click', function() { self.input_.click(); diff --git a/js/component/input/radio.js b/js/component/input/radio.js index adfeaa8..a5542f0 100644 --- a/js/component/input/radio.js +++ b/js/component/input/radio.js @@ -37,7 +37,7 @@ beestat.component.input.radio.prototype.decorate_ = function(parent) { const span = document.createElement('span'); span.style.cursor = 'pointer'; - span.style.marginLeft = (beestat.style.size.gutter / 2) + 'px'; + span.style.paddingLeft = (beestat.style.size.gutter / 4) + 'px'; span.innerText = this.label_; span.addEventListener('click', function() { self.input_.click(); diff --git a/js/component/input/text.js b/js/component/input/text.js index 8b45845..04c7220 100644 --- a/js/component/input/text.js +++ b/js/component/input/text.js @@ -19,6 +19,22 @@ beestat.component.input.text = function() { }); this.input_.addEventListener('change', function() { + if ( + self.transform_ !== undefined && + self.meets_requirements() === true + ) { + switch (self.transform_.type) { + case 'round': + // If the value is a number, then round it. + if (new RegExp(/^[\d\.]+$/).test(self.input_.value) === true) { + self.input_.value = Math.round( + self.input_.value * 10 ** self.transform_.decimals + ) / 10 ** self.transform_.decimals; + } + break; + } + } + self.dispatchEvent('change'); }); @@ -212,3 +228,16 @@ beestat.component.input.text.prototype.set_maxlength = function(maxlength) { return this; }; + +/** + * Set the auto format properties. + * + * @param {object} transform + * + * @return {beestat.component.input.text} This. + */ +beestat.component.input.text.prototype.set_transform = function(transform) { + this.transform_ = transform; + + return this; +}; diff --git a/js/component/metric/balance_point.js b/js/component/metric/balance_point.js index 50ac463..1d88d27 100644 --- a/js/component/metric/balance_point.js +++ b/js/component/metric/balance_point.js @@ -20,7 +20,7 @@ beestat.component.metric.balance_point.prototype.is_temperature_ = true; * @return {string} The units for this metric. */ beestat.component.metric.balance_point.prototype.get_units_ = function() { - return beestat.setting('temperature_unit'); + return beestat.setting('units.temperature'); }; /** diff --git a/js/component/metric/setback.js b/js/component/metric/setback.js index 8be0c99..6bf435e 100644 --- a/js/component/metric/setback.js +++ b/js/component/metric/setback.js @@ -22,7 +22,7 @@ beestat.component.metric.setback.prototype.is_temperature_delta_ = true; * @return {string} The units for this metric. */ beestat.component.metric.setback.prototype.get_units_ = function() { - return beestat.setting('temperature_unit'); + return beestat.setting('units.temperature'); }; /** diff --git a/js/component/metric/setpoint.js b/js/component/metric/setpoint.js index e9ec191..4a1377a 100644 --- a/js/component/metric/setpoint.js +++ b/js/component/metric/setpoint.js @@ -20,7 +20,7 @@ beestat.component.metric.setpoint.prototype.is_temperature_ = true; * @return {string} The units for this metric. */ beestat.component.metric.setpoint.prototype.get_units_ = function() { - return beestat.setting('temperature_unit'); + return beestat.setting('units.temperature'); }; /** diff --git a/js/component/modal/create_floor_plan.js b/js/component/modal/create_floor_plan.js index fe34d9e..2d152b6 100644 --- a/js/component/modal/create_floor_plan.js +++ b/js/component/modal/create_floor_plan.js @@ -92,27 +92,34 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio } // Ceiling height - (new beestat.component.title('How tall are your ceilings (feet)?')).render(parent); + (new beestat.component.title('How tall are your ceilings (' + beestat.setting('units.distance') + ')?')).render(parent); - const height_input = new beestat.component.input.text() + const ceiling_height_input = new beestat.component.input.text() .set_icon('arrow_expand_vertical') - .set_maxlength(2) + .set_maxlength(5) + .set_transform({ + 'type': 'round', + 'decimals': 2 + }) .set_requirements({ - 'min_value': 1, - 'type': 'integer', + 'min_value': beestat.distance(60), + 'type': 'decimal', 'required': true }) .render(parent); - height_input.addEventListener('change', function() { - self.state_.height = height_input.get_value(); - self.state_.error.height = !height_input.meets_requirements(); + ceiling_height_input.addEventListener('change', function() { + self.state_.ceiling_height = ceiling_height_input.get_value(); + self.state_.error.height = !ceiling_height_input.meets_requirements(); }); - if (self.state_.height !== undefined) { - height_input.set_value(self.state_.height); + if (self.state_.ceiling_height !== undefined) { + ceiling_height_input.set_value(self.state_.ceiling_height); } else if (self.state_.error.height !== true) { - height_input.set_value(8); + ceiling_height_input.set_value(beestat.distance({ + 'distance': 96, + 'round': 2 + })); } // Address @@ -203,6 +210,13 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { return; } + const ceiling_height = beestat.distance({ + 'distance': self.state_.ceiling_height, + 'input_distance_unit': beestat.setting('units.distance'), + 'output_distance_unit': 'in', + 'round': 2 + }); + const attributes = { 'name': self.state_.name }; @@ -212,7 +226,7 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { attributes.data = { 'groups': [] }; - let elevation = (self.state_.basement === true) ? (self.state_.height * -12) : 0; + let elevation = (self.state_.basement === true) ? (ceiling_height * -1) : 0; let floor = (self.state_.basement === true) ? 0 : 1; const ordinals = [ 'First', @@ -229,12 +243,12 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { attributes.data.groups.push({ 'name': floor === 0 ? 'Basement' : (ordinals[floor - 1] + ' Floor'), 'elevation': elevation, - 'height': self.state_.height * 12, + 'height': ceiling_height, 'rooms': [] }); floor++; - elevation += (self.state_.height * 12); + elevation += (ceiling_height); } new beestat.api() diff --git a/js/component/radio_group.js b/js/component/radio_group.js index 29ad93c..67c67df 100644 --- a/js/component/radio_group.js +++ b/js/component/radio_group.js @@ -3,7 +3,7 @@ */ beestat.component.radio_group = function() { this.radios_ = []; - this.name_ = Math.random(); + this.name_ = window.crypto.randomUUID(); beestat.component.apply(this, arguments); }; beestat.extend(beestat.component.radio_group, beestat.component); @@ -16,33 +16,42 @@ beestat.extend(beestat.component.radio_group, beestat.component); beestat.component.radio_group.prototype.decorate_ = function(parent) { const self = this; - const container = $.createElement('div'); - container.style('margin-bottom', beestat.style.size.gutter); + // Outer container + const container = document.createElement('div'); + if (this.arrangement_ === 'horizontal') { + Object.assign(container.style, { + 'display': 'flex', + 'grid-gap': `${beestat.style.size.gutter}px` + }); + } + parent.appendChild(container); + // Radios this.radios_.forEach(function(radio) { - radio.set_name('name', this.name_); + radio.set_name(self.name_); radio.addEventListener('change', function() { self.value_ = radio.get_value(); self.dispatchEvent('change'); }); - radio.render(container); + radio.render($(container)); }); - - parent.appendChild(container); }; /** * Add a radio to this group. * * @param {beestat.component.radio} radio The radio to add. + * + * @return {beestat.component.radio_group} */ beestat.component.radio_group.prototype.add_radio = function(radio) { this.radios_.push(radio); if (this.rendered_ === true) { this.rerender(); } + return this; }; /** @@ -69,3 +78,16 @@ beestat.component.radio_group.prototype.get_value = function() { return null; }; + +/** + * Set the arrangement of the radio buttons in the group. + * + * @param {string} arrangement horizontal|vertical + * + * @return {beestat.component.radio_group} + */ +beestat.component.radio_group.prototype.set_arrangement = function(arrangement) { + this.arrangement_ = arrangement; + + return this; +}; diff --git a/js/component/tile/floor_plan.js b/js/component/tile/floor_plan.js index 1ed5486..030bbf7 100644 --- a/js/component/tile/floor_plan.js +++ b/js/component/tile/floor_plan.js @@ -2,6 +2,7 @@ * A tile representing a floor plan. * * @param {integer} floor_plan_id + * */ beestat.component.tile.floor_plan = function(floor_plan_id) { this.floor_plan_id_ = floor_plan_id; @@ -30,7 +31,13 @@ beestat.component.tile.floor_plan.prototype.get_text_ = function() { const line_2_parts = []; let floor_count = floor_plan.data.groups.length; line_2_parts.push(floor_count + (floor_count === 1 ? ' Floor' : ' Floors')); - line_2_parts.push(beestat.floor_plan.get_area(this.floor_plan_id_).toLocaleString() + ' sqft'); + line_2_parts.push( + beestat.area({ + 'area': beestat.floor_plan.get_area(this.floor_plan_id_), + 'round': 0, + 'units': true + }) + ); return [ floor_plan.name, diff --git a/js/component/tile/floor_plan_group.js b/js/component/tile/floor_plan_group.js index 36c424e..9563e87 100644 --- a/js/component/tile/floor_plan_group.js +++ b/js/component/tile/floor_plan_group.js @@ -28,7 +28,13 @@ beestat.component.tile.floor_plan_group.prototype.get_text_ = function() { const line_2_parts = []; let room_count = this.floor_plan_group_.rooms.length; line_2_parts.push(room_count + (room_count === 1 ? ' Room' : ' Rooms')); - line_2_parts.push(beestat.floor_plan.get_area_group(this.floor_plan_group_).toLocaleString() + ' sqft'); + line_2_parts.push( + beestat.area({ + 'area': beestat.floor_plan.get_area_group(this.floor_plan_group_), + 'round': 0, + 'units': true + }) + ); return [ this.floor_plan_group_.name, diff --git a/js/js.php b/js/js.php index 2d6e238..539c4e7 100755 --- a/js/js.php +++ b/js/js.php @@ -16,6 +16,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; // Beestat echo '' . PHP_EOL; @@ -28,6 +29,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; diff --git a/js/layer/load.js b/js/layer/load.js index bb9e51d..dc950ab 100644 --- a/js/layer/load.js +++ b/js/layer/load.js @@ -170,8 +170,38 @@ beestat.layer.load.prototype.decorate_ = function(parent) { document.title = 'beestat'; } - // Set the active temperature unit. - beestat.setting('temperature_unit', thermostat.temperature_unit); + // Set the temperature unit if it hasn't been set before. + if (beestat.setting('units.temperature') === undefined) { + beestat.setting('units.temperature', thermostat.temperature_unit); + } + + // Set the distance/area units if they hasn't been set before. + const imperial_countries = [ + 'USA', + 'CAN' + ]; + if ( + beestat.setting('units.distance') === undefined && + thermostat.address_id !== null && + beestat.address.is_valid(thermostat.address_id) === true + ) { + const address = beestat.cache.address[thermostat.address_id]; + beestat.setting( + 'units.distance', + imperial_countries.includes(address.normalized.components.country_iso_3) === true ? 'ft' : 'm' + ); + } + if ( + beestat.setting('units.area') === undefined && + thermostat.address_id !== null && + beestat.address.is_valid(thermostat.address_id) === true + ) { + const address = beestat.cache.address[thermostat.address_id]; + beestat.setting( + 'units.area', + imperial_countries.includes(address.normalized.components.country_iso_3) === true ? 'ft²' : 'm²' + ); + } // Rename series if there are multiple stages. if (beestat.thermostat.get_stages(thermostat.thermostat_id, 'heat') > 1) { diff --git a/js/lib/polylabel/polylabel.js b/js/lib/polylabel/polylabel.js new file mode 100644 index 0000000..7d2916f --- /dev/null +++ b/js/lib/polylabel/polylabel.js @@ -0,0 +1,3 @@ +/* eslint-disable */ + +!function(a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define([],a):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).polylabel=a()}(function(){return(function d(e,f,b){function c(a,k){if(!f[a]){if(!e[a]){var i="function"==typeof require&&require;if(!k&&i)return i(a,!0);if(g)return g(a,!0);var j=new Error("Cannot find module '"+a+"'");throw j.code="MODULE_NOT_FOUND",j}var h=f[a]={exports:{}};e[a][0].call(h.exports,function(b){return c(e[a][1][b]||b)},h,h.exports,d,e,f,b)}return f[a].exports}for(var g="function"==typeof require&&require,a=0;ab!=c[1]>b&&j<(c[0]-a[0])*(b-a[1])/(c[1]-a[1])+a[0]&&(d=!d),e=Math.min(e,h(j,b,a,c))}return(d?1:-1)*Math.sqrt(e)}function h(g,h,i,e){var c=i[0],d=i[1],a=e[0]-c,b=e[1]-d;if(0!==a||0!==b){var f=((g-c)*a+(h-d)*b)/(a*a+b*b);f>1?(c=e[0],d=e[1]):f>0&&(c+=a*f,d+=b*f)}return(a=g-c)*a+(b=h-d)*b}b.exports=function(c,o,t){o=o||1;for(var k,l,m,n,i=0;im)&&(m=g[0]),(!i||g[1]>n)&&(n=g[1])}for(var p=Math.min(m-k,n-l),a=p/2,h=new d(null,e),q=k;qj.d&&(j=b,t&&console.log("found best %d after %d probes",Math.round(1e4*b.d)/1e4,s)),b.max-j.d<=o||(a=b.h/2,h.push(new f(b.x-a,b.y-a,a,c)),h.push(new f(b.x+a,b.y-a,a,c)),h.push(new f(b.x-a,b.y+a,a,c)),h.push(new f(b.x+a,b.y+a,a,c)),s+=4)}return t&&(console.log("num probes: "+s),console.log("best distance: "+j.d)),[j.x,j.y]}},{tinyqueue:2}],2:[function(c,b,d){"use strict";function a(b,d){if(!(this instanceof a))return new a(b,d);if(this.data=b||[],this.length=this.data.length,this.compare=d||e,b)for(var c=Math.floor(this.length/2);c>=0;c--)this._down(c)}function e(a,b){return ab?1:0}function f(a,b,c){var d=a[b];a[b]=a[c],a[c]=d}b.exports=a,a.prototype={push:function(a){this.data.push(a),this.length++,this._up(this.length-1)},pop:function(){var a=this.data[0];return this.data[0]=this.data[this.length-1],this.length--,this.data.pop(),this._down(0),a},peek:function(){return this.data[0]},_up:function(a){for(var b=this.data,d=this.compare;a>0;){var c=Math.floor((a-1)/2);if(0>d(b[a],b[c]))f(b,c,a),a=c;else break}},_down:function(b){for(var c=this.data,g=this.compare,h=this.length;;){var d=2*b+1,e=d+1,a=b;if(dg(c[d],c[a])&&(a=d),eg(c[e],c[a])&&(a=e),a===b)return;f(c,a,b),b=a}}}},{}]},{},[1])(1)})