1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 13:10:23 -05:00
beestat/js/component/card/floor_plan_editor.js
2026-02-22 13:39:23 -05:00

2534 lines
78 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Floor plan editor.
*
* @param {number} thermostat_id
*/
beestat.component.card.floor_plan_editor = function(thermostat_id) {
const self = this;
this.thermostat_id_ = thermostat_id;
// Whether or not to show the editor when loading.
this.show_editor_ = beestat.floor_plan.get_bounding_box(
beestat.setting('visualize.floor_plan_id')
).x === Infinity;
beestat.component.card.apply(this, arguments);
// Snapping initial
if (this.state_.snapping === undefined) {
this.state_.snapping = true;
}
this.state_.show_layers_sidebar = true;
if (this.state_.layers_sidebar === undefined) {
this.state_.layers_sidebar = {};
}
if (this.state_.layers_sidebar.mobile_mode === undefined) {
this.state_.layers_sidebar.mobile_mode = (
beestat.platform() === 'ios' ||
beestat.platform() === 'android'
);
}
// The first time this component renders center the content.
this.addEventListener('render', function() {
if (this.floor_plan_ !== undefined) {
self.floor_plan_.center_content();
self.removeEventListener('render');
}
});
};
beestat.extend(beestat.component.card.floor_plan_editor, beestat.component.card);
beestat.component.card.floor_plan_editor.layer_type_meta_ = {
'rooms': {
'active_state_key': 'active_room_entity',
'id_key': 'room_id',
'getter_name': 'get_room',
'clears_geometry_selection': true
},
'surfaces': {
'active_state_key': 'active_surface_entity',
'id_key': 'surface_id',
'getter_name': 'get_surface',
'clears_geometry_selection': true
},
'openings': {
'active_state_key': 'active_opening_entity',
'id_key': 'opening_id',
'getter_name': 'get_opening',
'clears_geometry_selection': false
},
'trees': {
'active_state_key': 'active_tree_entity',
'id_key': 'tree_id',
'getter_name': 'get_tree',
'clears_geometry_selection': false
},
'light_sources': {
'active_state_key': 'active_light_source_entity',
'id_key': 'light_source_id',
'getter_name': 'get_light_source',
'clears_geometry_selection': false
}
};
/**
* Decorate.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function(parent) {
const self = this;
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) {
if (group.group_id === undefined) {
group.group_id = window.crypto.randomUUID();
}
if (group.rooms === undefined) {
group.rooms = [];
}
if (group.surfaces === undefined) {
group.surfaces = [];
}
if (group.trees === undefined) {
group.trees = [];
}
if (group.openings === undefined) {
group.openings = [];
}
if (group.light_sources === undefined) {
group.light_sources = [];
}
group.rooms.forEach(function(room) {
if (room.room_id === undefined) {
room.room_id = window.crypto.randomUUID();
}
if (room.editor_hidden === undefined) {
room.editor_hidden = false;
}
if (room.editor_locked === undefined) {
room.editor_locked = false;
}
});
group.surfaces.forEach(function(surface) {
if (surface.surface_id === undefined) {
surface.surface_id = window.crypto.randomUUID();
}
if (surface.editor_hidden === undefined) {
surface.editor_hidden = false;
}
if (surface.editor_locked === undefined) {
surface.editor_locked = false;
}
});
group.trees.forEach(function(tree) {
if (tree.tree_id === undefined) {
tree.tree_id = window.crypto.randomUUID();
}
if (tree.editor_hidden === undefined) {
tree.editor_hidden = false;
}
if (tree.editor_locked === undefined) {
tree.editor_locked = false;
}
});
group.openings.forEach(function(opening) {
if (opening.opening_id === undefined) {
opening.opening_id = window.crypto.randomUUID();
}
if (opening.editor_hidden === undefined) {
opening.editor_hidden = false;
}
if (opening.editor_locked === undefined) {
opening.editor_locked = false;
}
if (['empty', 'door', 'window', 'glass'].includes(opening.type) !== true) {
opening.type = 'empty';
}
const is_window_like = opening.type === 'window' || opening.type === 'glass';
const default_opening_width = is_window_like ? 48 : 36;
const default_opening_height = is_window_like ? 60 : 78;
const default_opening_elevation = is_window_like ? 24 : 0;
const default_opening_color = '#7a573b';
const center_x = Number(opening.x || 0);
const center_y = Number(opening.y || 0);
const width = Number(opening.width || default_opening_width);
if (opening.width === undefined) {
opening.width = default_opening_width;
}
if (opening.height === undefined) {
opening.height = default_opening_height;
}
if (opening.elevation === undefined) {
opening.elevation = default_opening_elevation;
}
if (
opening.points === undefined ||
Array.isArray(opening.points) !== true ||
opening.points.length !== 2
) {
const half_width = Math.max(12, width) / 2;
opening.points = [
{
'x': center_x - half_width,
'y': center_y
},
{
'x': center_x + half_width,
'y': center_y
}
];
}
opening.x = (Number(opening.points[0].x || 0) + Number(opening.points[1].x || 0)) / 2;
opening.y = (Number(opening.points[0].y || 0) + Number(opening.points[1].y || 0)) / 2;
const dx = Number(opening.points[1].x || 0) - Number(opening.points[0].x || 0);
const dy = Number(opening.points[1].y || 0) - Number(opening.points[0].y || 0);
opening.width = Math.max(12, Math.round(Math.sqrt((dx * dx) + (dy * dy))));
if (opening.type === 'door') {
if (opening.color === undefined) {
opening.color = default_opening_color;
}
} else {
delete opening.color;
}
});
group.light_sources.forEach(function(light_source) {
if (light_source.light_source_id === undefined) {
light_source.light_source_id = window.crypto.randomUUID();
}
if (light_source.editor_hidden === undefined) {
light_source.editor_hidden = false;
}
if (light_source.editor_locked === undefined) {
light_source.editor_locked = false;
}
light_source.x = Number(light_source.x || 0);
light_source.y = Number(light_source.y || 0);
light_source.elevation = Number(light_source.elevation !== undefined ? light_source.elevation : 72);
light_source.intensity = ['dim', 'normal', 'bright'].includes(light_source.intensity)
? light_source.intensity
: 'normal';
light_source.temperature_k = Math.max(
1000,
Math.min(12000, Math.round(Number(light_source.temperature_k || 4000)))
);
});
});
/**
* If there is an active_group_id, override whatever the current active
* group is. Used for undo/redo.
*/
if (this.state_.active_group_id !== undefined) {
for (let i = 0; i < floor_plan.data.groups.length; i++) {
if (floor_plan.data.groups[i].group_id === this.state_.active_group_id) {
this.state_.active_group = floor_plan.data.groups[i];
delete this.state_.active_group_id;
break;
}
}
}
// If there is no active group, set it to best guess of ground floor.
if (this.state_.active_group === undefined) {
let closest_distance = Infinity;
let closest_group;
floor_plan.data.groups.forEach(function(group) {
if (Math.abs(group.elevation) < closest_distance) {
closest_group = group;
closest_distance = Math.abs(group.elevation);
}
});
this.state_.active_group = closest_group;
}
this.floor_plan_tile_ = new beestat.component.tile.floor_plan(
beestat.setting('visualize.floor_plan_id')
)
.set_background_color(beestat.style.color.lightblue.base)
.set_background_hover_color(beestat.style.color.lightblue.base)
.set_text_color('#fff')
.set_display('block')
.addEventListener('click', function() {
self.show_editor_ = !self.show_editor_;
self.rerender();
})
.render(parent);
// Decorate everything.
if (this.show_editor_ === true) {
const drawing_pane_container = $.createElement('div');
drawing_pane_container.style({
'margin-top': beestat.style.size.gutter,
'position': 'relative',
'overflow-x': 'hidden'
});
parent.appendChild(drawing_pane_container);
this.decorate_drawing_pane_(drawing_pane_container);
this.info_pane_container_ = $.createElement('div')
.style('margin-top', beestat.style.size.gutter / 2);
parent.appendChild(this.info_pane_container_);
this.decorate_info_pane_(this.info_pane_container_);
// Help container
if (beestat.floor_plan.get_area(beestat.setting('visualize.floor_plan_id')) === 0) {
const help_container = document.createElement('div');
Object.assign(help_container.style, {
'position': 'absolute',
'left': '65px',
'top': '20px'
});
drawing_pane_container.appendChild(help_container);
this.helper_tile_ = new beestat.component.tile()
.set_text('Start by adding a room')
.set_shadow(false)
.set_background_color(beestat.style.color.green.base)
.set_text_color('#fff')
.set_type('pill')
.set_size('small')
.set_icon('arrow_left')
.render($(help_container));
}
}
const expand_container = document.createElement('div');
Object.assign(expand_container.style, {
'position': 'absolute',
'right': '28px',
'top': '70px'
});
parent.appendChild(expand_container);
new beestat.component.tile()
.set_icon(this.show_editor_ === true ? 'chevron_up' : 'chevron_down')
.set_size('small')
.set_shadow(false)
.set_background_hover_color(beestat.style.color.lightblue.base)
.set_text_color('#fff')
.addEventListener('click', function() {
self.show_editor_ = !self.show_editor_;
self.rerender();
})
.render($(expand_container));
};
/**
* Decorate the drawing pane.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = function(parent) {
const self = this;
// Tear down prior drawing-pane components to avoid stale listeners.
// Dispose existing SVG to remove any global listeners.
if (this.floor_plan_ !== undefined) {
this.floor_plan_.dispose();
}
// Dispose existing compass
if (this.compass_ !== undefined) {
this.compass_.dispose();
}
// Dispose existing layers sidebar.
if (this.layers_sidebar_ !== undefined) {
this.layers_sidebar_.dispose();
}
// Build drawing canvas and floating sidebar containers.
const drawing_canvas_container = $.createElement('div');
drawing_canvas_container.style({
'position': 'relative',
'width': '100%',
'overflow': 'hidden',
'height': '500px'
});
parent.appendChild(drawing_canvas_container);
const layers_sidebar_container = $.createElement('div');
const sidebar_outer_gutter = beestat.style.size.gutter / 2;
const is_mobile_layers_sidebar = (
this.state_.layers_sidebar !== undefined &&
this.state_.layers_sidebar.mobile_mode === true
);
const layers_sidebar_width = is_mobile_layers_sidebar ? 56 : 250;
const layers_sidebar_padding = sidebar_outer_gutter;
layers_sidebar_container.style({
'position': 'absolute',
'top': sidebar_outer_gutter + 'px',
'right': sidebar_outer_gutter + 'px',
'bottom': sidebar_outer_gutter + 'px',
'width': layers_sidebar_width + 'px',
'height': 'auto',
'padding': layers_sidebar_padding + 'px',
'box-sizing': 'border-box',
'overflow': 'visible',
'z-index': 5,
'pointer-events': 'auto'
});
drawing_canvas_container.appendChild(layers_sidebar_container);
// Create and render floor plan + layer sidebar with editor callbacks.
// Create and render a new SVG component.
this.floor_plan_ = new beestat.component.floor_plan(
beestat.setting('visualize.floor_plan_id'),
this.state_
);
this.floor_plan_.render(drawing_canvas_container);
this.layers_sidebar_ = new beestat.component.floor_plan_layers_sidebar(
beestat.setting('visualize.floor_plan_id'),
this.state_
)
.style({
'height': '100%',
'width': '100%'
})
.set_on_select_floor(function(group) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.floor_plan_.set_active_group(group);
self.update_layers_sidebar_();
})
.set_on_select_object(function(group, type, object_id) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.select_layer_object_(group, type, object_id);
})
.set_on_toggle_visibility(function(group, type, object_id, visible) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_layer_object_visibility_(group, type, object_id, visible);
})
.set_on_toggle_lock(function(group, type, object_id, locked) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_layer_object_locked_(group, type, object_id, locked);
})
.set_on_toggle_layer_visibility(function(group, type, visible) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_layer_visible_(group, type, visible);
})
.set_on_toggle_layer_lock(function(group, type, locked) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_layer_locked_(group, type, locked);
})
.set_on_toggle_group_visibility(function(group, visible) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_group_visible_(group, visible);
})
.set_on_toggle_group_lock(function(group, locked) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.set_group_locked_(group, locked);
})
.set_on_reorder(function(group, type, from_index, to_index, drop_after) {
if (self.layers_sidebar_ !== undefined) {
self.layers_sidebar_.remember_scroll();
}
self.reorder_layer_object_(group, type, from_index, to_index, drop_after);
})
.set_on_toggle_mobile_mode(function(mobile_mode) {
if (self.state_.layers_sidebar === undefined) {
self.state_.layers_sidebar = {};
}
self.state_.layers_sidebar.mobile_mode = mobile_mode === true;
self.rerender();
})
.render(layers_sidebar_container);
this.ensure_active_entity_visibility_();
// Render orientation compass when available.
// Create and render the compass for setting orientation (early access only)
if (beestat.user.has_early_access() === true) {
this.compass_ = new beestat.component.compass(
beestat.setting('visualize.floor_plan_id')
);
this.compass_.render(drawing_canvas_container);
if (this.compass_.container_ !== undefined) {
this.compass_.container_.style.left = beestat.style.size.gutter + 'px';
this.compass_.container_.style.right = 'auto';
this.compass_.container_.style.bottom = beestat.style.size.gutter + 'px';
}
// Update floor plan when rotation changes
this.compass_.addEventListener('rotation_change', function() {
self.update_floor_plan_();
});
}
// Keep canvas width synchronized with container resize events.
setTimeout(function() {
if (drawing_canvas_container.getBoundingClientRect().width > 0) {
self.floor_plan_.set_width(drawing_canvas_container.getBoundingClientRect().width);
}
}, 0);
beestat.dispatcher.removeEventListener('resize.floor_plan_editor');
beestat.dispatcher.addEventListener('resize.floor_plan_editor', function() {
self.floor_plan_.set_width(drawing_canvas_container.getBoundingClientRect().width);
});
// Re-render editor shell when floor-plan model mutations occur.
// Rerender when floor-plan model changes.
const rerender_events = [
'add_room',
'remove_room',
'remove_point',
'add_surface',
'remove_surface',
'add_tree',
'remove_tree',
'add_opening',
'remove_opening',
'add_light_source',
'remove_light_source',
'undo',
'redo'
];
const handle_floor_plan_mutation = function() {
self.update_floor_plan_();
self.rerender();
};
rerender_events.forEach(function(event_name) {
self.floor_plan_.addEventListener(event_name, handle_floor_plan_mutation);
});
this.floor_plan_.addEventListener('change_group', self.rerender.bind(this));
// Initialize entity index + shared entity event handlers.
this.entity_index_ = {
'rooms': {},
'surfaces': {},
'trees': {},
'openings': {},
'light_sources': {}
};
const on_entity_update = function() {
self.floor_plan_.update_infobox();
self.update_info_pane_();
self.update_floor_plan_tile_();
self.update_floor_plan_();
self.update_layers_sidebar_();
};
const on_entity_activate = function() {
self.expand_layers_for_active_entity_();
self.scroll_layers_to_active_entity_();
self.floor_plan_.update_infobox();
self.floor_plan_.update_toolbar();
self.update_info_pane_();
self.update_floor_plan_tile_();
self.update_layers_sidebar_();
};
const on_entity_inactivate = function() {
self.restore_entity_draw_order_();
self.floor_plan_.update_infobox();
self.floor_plan_.update_toolbar();
self.update_info_pane_();
self.update_floor_plan_tile_();
self.update_layers_sidebar_();
};
// Render non-editable context from the group below the active floor.
const group_below = this.floor_plan_.get_group_below(this.state_.active_group);
if (group_below !== undefined) {
group_below.rooms.slice().reverse().forEach(function(room) {
if (room.editor_hidden === true) {
return;
}
const room_entity = new beestat.component.floor_plan_entity.room(self.floor_plan_, self.state_)
.set_enabled(false)
.set_room(room)
.set_group(self.state_.active_group);
room_entity.render(self.floor_plan_.get_g());
});
group_below.surfaces.slice().reverse().forEach(function(surface) {
if (surface.editor_hidden === true) {
return;
}
const surface_entity = new beestat.component.floor_plan_entity.surface(self.floor_plan_, self.state_)
.set_enabled(false)
.set_surface(surface)
.set_group(self.state_.active_group);
surface_entity.render(self.floor_plan_.get_g());
});
(group_below.openings || []).slice().reverse().forEach(function(opening) {
if (opening.editor_hidden === true) {
return;
}
const opening_entity = new beestat.component.floor_plan_entity.opening(self.floor_plan_, self.state_)
.set_enabled(false)
.set_opening(opening)
.set_group(self.state_.active_group);
opening_entity.render(self.floor_plan_.get_g());
});
(group_below.light_sources || []).slice().reverse().forEach(function(light_source) {
if (light_source.editor_hidden === true) {
return;
}
const light_source_entity = new beestat.component.floor_plan_entity.light_source(self.floor_plan_, self.state_)
.set_enabled(false)
.set_light_source(light_source)
.set_group(self.state_.active_group);
light_source_entity.render(self.floor_plan_.get_g());
});
}
// Render editable entities for the active group by type.
// Loop over the rooms in this group and add them.
let active_room_entity;
this.state_.active_group.rooms.slice().reverse().forEach(function(room) {
if (room.editor_hidden === true) {
return;
}
const room_entity = new beestat.component.floor_plan_entity.room(self.floor_plan_, self.state_)
.set_enabled(room.editor_locked !== true)
.set_room(room)
.set_group(self.state_.active_group);
room_entity.addEventListener('update', on_entity_update);
room_entity.addEventListener('activate', on_entity_activate);
room_entity.addEventListener('inactivate', on_entity_inactivate);
/**
* If there is currently an active room, use it to match to the newly
* created room entities and then store it. After this loop is done
* activate it to avoid other rooms getting written on top. Also delete
* the active room from the state or it will needlessly be inactivated in
* the set_active function.
*/
if (
self.state_.active_room_entity !== undefined &&
room.room_id === self.state_.active_room_entity.get_room().room_id
) {
delete self.state_.active_room_entity;
active_room_entity = room_entity;
}
// Render the room and save to the list of current entities.
room_entity.render(self.floor_plan_.get_g());
self.entity_index_.rooms[room.room_id] = room_entity;
});
if (active_room_entity !== undefined) {
active_room_entity.set_active(true);
}
/**
* If there was an active room, defer to adding it last so it ends up on
* top. The set_active function doesn't do anything if the room isn't
* rendered otherwise.
*/
if (this.state_.active_room_entity !== undefined) {
this.state_.active_room_entity.render(this.floor_plan_.get_g());
}
// Loop over surfaces in this group and add them.
let active_surface_entity;
this.state_.active_group.surfaces.slice().reverse().forEach(function(surface) {
if (surface.editor_hidden === true) {
return;
}
const surface_entity = new beestat.component.floor_plan_entity.surface(self.floor_plan_, self.state_)
.set_enabled(surface.editor_locked !== true)
.set_surface(surface)
.set_group(self.state_.active_group);
surface_entity.addEventListener('update', on_entity_update);
surface_entity.addEventListener('activate', on_entity_activate);
surface_entity.addEventListener('inactivate', on_entity_inactivate);
if (
self.state_.active_surface_entity !== undefined &&
surface.surface_id === self.state_.active_surface_entity.get_surface().surface_id
) {
delete self.state_.active_surface_entity;
active_surface_entity = surface_entity;
}
surface_entity.render(self.floor_plan_.get_g());
self.entity_index_.surfaces[surface.surface_id] = surface_entity;
});
if (active_surface_entity !== undefined) {
active_surface_entity.set_active(true);
}
if (this.state_.active_surface_entity !== undefined) {
this.state_.active_surface_entity.render(this.floor_plan_.get_g());
}
// Loop over openings in this group and add them.
let active_opening_entity;
(this.state_.active_group.openings || []).slice().reverse().forEach(function(opening) {
if (opening.editor_hidden === true) {
return;
}
const opening_entity = new beestat.component.floor_plan_entity.opening(self.floor_plan_, self.state_)
.set_enabled(opening.editor_locked !== true)
.set_opening(opening)
.set_group(self.state_.active_group);
opening_entity.addEventListener('update', on_entity_update);
opening_entity.addEventListener('activate', on_entity_activate);
opening_entity.addEventListener('inactivate', on_entity_inactivate);
if (
self.state_.active_opening_entity !== undefined &&
opening.opening_id === self.state_.active_opening_entity.get_opening().opening_id
) {
delete self.state_.active_opening_entity;
active_opening_entity = opening_entity;
}
opening_entity.render(self.floor_plan_.get_g());
self.entity_index_.openings[opening.opening_id] = opening_entity;
});
if (active_opening_entity !== undefined) {
active_opening_entity.set_active(true);
}
if (this.state_.active_opening_entity !== undefined) {
this.state_.active_opening_entity.render(this.floor_plan_.get_g());
}
// Loop over light sources in this group and add them.
let active_light_source_entity;
(this.state_.active_group.light_sources || []).slice().reverse().forEach(function(light_source) {
if (light_source.editor_hidden === true) {
return;
}
const light_source_entity = new beestat.component.floor_plan_entity.light_source(self.floor_plan_, self.state_)
.set_enabled(light_source.editor_locked !== true)
.set_light_source(light_source)
.set_group(self.state_.active_group);
light_source_entity.addEventListener('update', on_entity_update);
light_source_entity.addEventListener('activate', on_entity_activate);
light_source_entity.addEventListener('inactivate', on_entity_inactivate);
if (
self.state_.active_light_source_entity !== undefined &&
light_source.light_source_id === self.state_.active_light_source_entity.get_light_source().light_source_id
) {
delete self.state_.active_light_source_entity;
active_light_source_entity = light_source_entity;
}
light_source_entity.render(self.floor_plan_.get_g());
self.entity_index_.light_sources[light_source.light_source_id] = light_source_entity;
});
if (active_light_source_entity !== undefined) {
active_light_source_entity.set_active(true);
}
if (this.state_.active_light_source_entity !== undefined) {
this.state_.active_light_source_entity.render(this.floor_plan_.get_g());
}
// Trees are restricted to the designated tree group (typically first floor).
// Trees are only editable on the first floor.
const tree_group = this.floor_plan_.get_tree_group_();
if (tree_group === this.state_.active_group) {
if (tree_group.trees === undefined) {
tree_group.trees = [];
}
let active_tree_entity;
tree_group.trees.slice().reverse().forEach(function(tree) {
if (tree.editor_hidden === true) {
return;
}
const tree_entity = new beestat.component.floor_plan_entity.tree(self.floor_plan_, self.state_)
.set_enabled(tree.editor_locked !== true)
.set_tree(tree)
.set_group(tree_group);
tree_entity.addEventListener('update', on_entity_update);
tree_entity.addEventListener('activate', on_entity_activate);
tree_entity.addEventListener('inactivate', on_entity_inactivate);
if (
self.state_.active_tree_entity !== undefined &&
tree.tree_id === self.state_.active_tree_entity.get_tree().tree_id
) {
delete self.state_.active_tree_entity;
active_tree_entity = tree_entity;
}
tree_entity.render(self.floor_plan_.get_g());
self.entity_index_.trees[tree.tree_id] = tree_entity;
});
if (active_tree_entity !== undefined) {
active_tree_entity.set_active(true);
}
if (this.state_.active_tree_entity !== undefined) {
this.state_.active_tree_entity.render(this.floor_plan_.get_g());
}
}
// Re-apply deferred layer selection and refresh sidebar state.
this.apply_pending_layer_selection_();
this.update_layers_sidebar_();
};
/**
* Rerender layers sidebar if available.
*/
beestat.component.card.floor_plan_editor.prototype.update_layers_sidebar_ = function() {
if (this.layers_sidebar_ !== undefined) {
this.layers_sidebar_.remember_scroll();
this.layers_sidebar_.rerender();
}
};
/**
* Select an object from the layers sidebar.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {string} object_id
*/
beestat.component.card.floor_plan_editor.prototype.select_layer_object_ = function(group, type, object_id) {
let normalized_type = type;
if (normalized_type === 'room') {
normalized_type = 'rooms';
} else if (normalized_type === 'opening') {
normalized_type = 'openings';
} else if (normalized_type === 'light_source') {
normalized_type = 'light_sources';
}
const object = this.get_layer_object_by_id_(group, normalized_type, object_id);
const is_active_group = (
this.state_.active_group !== undefined &&
this.state_.active_group.group_id === group.group_id
);
this.state_.pending_layer_selection = {
'group_id': group.group_id,
'type': normalized_type,
'object_id': object_id
};
if (is_active_group !== true) {
this.floor_plan_.set_active_group(group);
return;
}
if (object !== undefined && object.editor_locked === true) {
delete this.state_.pending_layer_selection;
return;
}
const entity_map = this.entity_index_[normalized_type];
if (entity_map !== undefined && entity_map[object_id] !== undefined) {
entity_map[object_id].set_active(true);
delete this.state_.pending_layer_selection;
}
};
/**
* Set an object's editor visibility.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {string} object_id
* @param {boolean} visible
*/
beestat.component.card.floor_plan_editor.prototype.set_layer_object_visibility_ = function(group, type, object_id, visible) {
const object = this.get_layer_object_by_id_(group, type, object_id);
if (object === undefined) {
return;
}
object.editor_hidden = visible !== true;
if (visible !== true) {
this.deactivate_active_entity_for_layer_object_(type, object_id);
}
this.sync_after_layer_change_();
};
/**
* Set an object's editor lock.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {string} object_id
* @param {boolean} locked
*/
beestat.component.card.floor_plan_editor.prototype.set_layer_object_locked_ = function(group, type, object_id, locked) {
const object = this.get_layer_object_by_id_(group, type, object_id);
if (object === undefined) {
return;
}
object.editor_locked = locked;
if (locked === true) {
this.deactivate_active_entity_for_layer_object_(type, object_id);
}
this.sync_after_layer_change_();
};
/**
* Lock or unlock all objects in a layer type.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {boolean} locked
*/
beestat.component.card.floor_plan_editor.prototype.set_layer_locked_ = function(group, type, locked) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_locked = locked;
});
if (locked === true) {
this.deactivate_active_entity_for_group_type_(group, type);
}
this.sync_after_layer_change_();
};
/**
* Hide or show all objects in a type layer.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {boolean} visible
*/
beestat.component.card.floor_plan_editor.prototype.set_layer_visible_ = function(group, type, visible) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_hidden = visible !== true;
});
if (visible !== true) {
this.deactivate_active_entity_for_group_type_(group, type);
}
this.sync_after_layer_change_();
};
/**
* Lock or unlock all objects in a floor group.
*
* @param {object} group
* @param {boolean} locked
*/
beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(group, locked) {
this.get_layer_types_().forEach(function(type) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_locked = locked;
});
});
if (locked === true) {
this.get_layer_types_().forEach(function(type) {
this.deactivate_active_entity_for_group_type_(group, type);
}, this);
}
this.sync_after_layer_change_();
};
/**
* Hide or show all objects in a floor group.
*
* @param {object} group
* @param {boolean} visible
*/
beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function(group, visible) {
this.get_layer_types_().forEach(function(type) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_hidden = visible !== true;
});
});
if (visible !== true) {
this.get_layer_types_().forEach(function(type) {
this.deactivate_active_entity_for_group_type_(group, type);
}, this);
}
this.sync_after_layer_change_();
};
/**
* Deactivate active entity if it belongs to the specified group/type.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
*/
beestat.component.card.floor_plan_editor.prototype.deactivate_active_entity_for_group_type_ = function(group, type) {
const metadata = this.get_layer_type_meta_(type);
if (metadata === undefined) {
return;
}
const active_entity = this.state_[metadata.active_state_key];
if (active_entity !== undefined && active_entity.group_ === group) {
active_entity.set_active(false);
}
};
/**
* Refresh editor after layer/group state changes.
*/
beestat.component.card.floor_plan_editor.prototype.sync_after_layer_change_ = function() {
this.floor_plan_.update_infobox();
this.floor_plan_.update_toolbar();
this.update_info_pane_();
this.update_floor_plan_();
this.rerender();
};
/**
* Reorder an object in a fixed sub-layer.
*
* @param {object} group
* @param {string} type rooms|surfaces|trees
* @param {number} from_index
* @param {number} to_index
* @param {boolean} drop_after
*/
beestat.component.card.floor_plan_editor.prototype.reorder_layer_object_ = function(group, type, from_index, to_index, drop_after) {
const collection = group[type] || [];
let insert_index = to_index + (drop_after === true ? 1 : 0);
if (
from_index < 0 ||
insert_index < 0 ||
from_index >= collection.length ||
insert_index > collection.length
) {
return;
}
if (from_index < insert_index) {
insert_index--;
}
if (from_index === insert_index) {
return;
}
const moved = collection.splice(from_index, 1)[0];
collection.splice(insert_index, 0, moved);
this.update_floor_plan_();
this.rerender();
};
/**
* Ensure hidden active entities are cleared.
*/
beestat.component.card.floor_plan_editor.prototype.ensure_active_entity_visibility_ = function() {
this.get_layer_types_().forEach(function(type) {
const metadata = this.get_layer_type_meta_(type);
const active_entity = this.state_[metadata.active_state_key];
if (active_entity === undefined) {
return;
}
const getter = active_entity[metadata.getter_name];
if (typeof getter !== 'function') {
return;
}
const active_object = getter.call(active_entity);
if (
active_object !== undefined &&
(active_object.editor_hidden === true || active_object.editor_locked === true)
) {
delete this.state_[metadata.active_state_key];
if (metadata.clears_geometry_selection === true) {
delete this.state_.active_wall_entity;
delete this.state_.active_point_entity;
}
}
}, this);
};
/**
* Apply deferred sidebar selection after rerender.
*/
beestat.component.card.floor_plan_editor.prototype.apply_pending_layer_selection_ = function() {
if (this.state_.pending_layer_selection === undefined) {
return;
}
const pending = this.state_.pending_layer_selection;
if (
this.state_.active_group === undefined ||
pending.group_id !== this.state_.active_group.group_id
) {
return;
}
const entity_map = this.entity_index_[pending.type];
const pending_object = this.get_layer_object_by_id_(this.state_.active_group, pending.type, pending.object_id);
if (
pending_object !== undefined &&
pending_object.editor_locked !== true &&
entity_map !== undefined &&
entity_map[pending.object_id] !== undefined
) {
entity_map[pending.object_id].set_active(true);
}
delete this.state_.pending_layer_selection;
};
/**
* Layer metadata by type.
*
* @param {string} type rooms|surfaces|openings|trees|light_sources
*
* @return {object|undefined}
*/
beestat.component.card.floor_plan_editor.prototype.get_layer_type_meta_ = function(type) {
return beestat.component.card.floor_plan_editor.layer_type_meta_[type];
};
/**
* Get all supported layer types.
*
* @return {string[]}
*/
beestat.component.card.floor_plan_editor.prototype.get_layer_types_ = function() {
return Object.keys(beestat.component.card.floor_plan_editor.layer_type_meta_);
};
/**
* Deactivate active entity for a specific object id/type.
*
* @param {string} type rooms|surfaces|openings|trees|light_sources
* @param {string} object_id
*/
beestat.component.card.floor_plan_editor.prototype.deactivate_active_entity_for_layer_object_ = function(type, object_id) {
const metadata = this.get_layer_type_meta_(type);
if (metadata === undefined) {
return;
}
const active_entity = this.state_[metadata.active_state_key];
if (active_entity === undefined) {
return;
}
const getter = active_entity[metadata.getter_name];
if (typeof getter !== 'function') {
return;
}
const active_object = getter.call(active_entity);
if (
active_object !== undefined &&
active_object[metadata.id_key] === object_id
) {
active_entity.set_active(false);
}
};
/**
* Get object by id from a group/type.
*
* @param {object} group
* @param {string} type rooms|surfaces|openings|trees
* @param {string} object_id
*
* @return {object|undefined}
*/
beestat.component.card.floor_plan_editor.prototype.get_layer_object_by_id_ = function(group, type, object_id) {
const metadata = this.get_layer_type_meta_(type);
if (metadata === undefined) {
return;
}
const collection = group[type] || [];
for (let i = 0; i < collection.length; i++) {
if (collection[i][metadata.id_key] === object_id) {
return collection[i];
}
}
};
/**
* Expand sidebar nodes needed to reveal the currently active entity.
*/
beestat.component.card.floor_plan_editor.prototype.expand_layers_for_active_entity_ = function() {
if (this.state_.layers_sidebar === undefined || this.state_.active_group === undefined) {
return;
}
let type;
if (this.state_.active_tree_entity !== undefined) {
type = 'trees';
} else if (this.state_.active_light_source_entity !== undefined) {
type = 'light_sources';
} else if (this.state_.active_opening_entity !== undefined) {
type = 'openings';
} else if (this.state_.active_surface_entity !== undefined) {
type = 'surfaces';
} else if (this.state_.active_room_entity !== undefined) {
type = 'rooms';
} else {
return;
}
const sidebar_state = this.state_.layers_sidebar;
const group_id = this.state_.active_group.group_id;
sidebar_state.collapsed_groups[group_id] = false;
sidebar_state.collapsed_types[group_id + '.' + type] = false;
};
/**
* Queue sidebar scroll to currently active entity.
*/
beestat.component.card.floor_plan_editor.prototype.scroll_layers_to_active_entity_ = function() {
if (this.state_.layers_sidebar === undefined || this.state_.active_group === undefined) {
return;
}
let type;
let object_id;
if (this.state_.active_tree_entity !== undefined) {
type = 'trees';
object_id = this.state_.active_tree_entity.get_tree().tree_id;
} else if (this.state_.active_light_source_entity !== undefined) {
type = 'light_sources';
object_id = this.state_.active_light_source_entity.get_light_source().light_source_id;
} else if (this.state_.active_opening_entity !== undefined) {
type = 'openings';
object_id = this.state_.active_opening_entity.get_opening().opening_id;
} else if (this.state_.active_surface_entity !== undefined) {
type = 'surfaces';
object_id = this.state_.active_surface_entity.get_surface().surface_id;
} else if (this.state_.active_room_entity !== undefined) {
type = 'rooms';
object_id = this.state_.active_room_entity.get_room().room_id;
} else {
return;
}
this.state_.layers_sidebar.scroll_to = {
'group_id': this.state_.active_group.group_id,
'type': type,
'object_id': object_id
};
};
/**
* Restore visible entity draw order to persisted layer order.
*/
beestat.component.card.floor_plan_editor.prototype.restore_entity_draw_order_ = function() {
if (
this.floor_plan_ === undefined ||
this.state_.active_group === undefined ||
this.entity_index_ === undefined
) {
return;
}
const append_entities_in_order = function(collection, entity_map, id_key) {
collection.slice().reverse().forEach(function(object) {
const entity = entity_map[object[id_key]];
if (
entity !== undefined &&
entity.g_ !== undefined &&
entity.g_.parentNode !== null
) {
entity.g_.parentNode.appendChild(entity.g_);
}
});
};
append_entities_in_order(
this.state_.active_group.rooms || [],
this.entity_index_.rooms || {},
'room_id'
);
append_entities_in_order(
this.state_.active_group.surfaces || [],
this.entity_index_.surfaces || {},
'surface_id'
);
append_entities_in_order(
this.state_.active_group.openings || [],
this.entity_index_.openings || {},
'opening_id'
);
append_entities_in_order(
this.state_.active_group.light_sources || [],
this.entity_index_.light_sources || {},
'light_source_id'
);
const tree_group = this.floor_plan_.get_tree_group_();
if (tree_group === this.state_.active_group) {
append_entities_in_order(
tree_group.trees || [],
this.entity_index_.trees || {},
'tree_id'
);
}
};
/**
* Decorate the info pane.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_ = function(parent) {
// Route to the entity-specific pane based on the active selection state.
if (this.state_.active_tree_entity !== undefined) {
this.decorate_info_pane_tree_(parent);
} else if (this.state_.active_light_source_entity !== undefined) {
this.decorate_info_pane_light_source_(parent);
} else if (this.state_.active_opening_entity !== undefined) {
this.decorate_info_pane_opening_(parent);
} else if (this.state_.active_surface_entity !== undefined) {
this.decorate_info_pane_surface_(parent);
} else if (this.state_.active_room_entity !== undefined) {
this.decorate_info_pane_room_(parent);
} else {
this.decorate_info_pane_floor_(parent);
}
};
/**
* Decorate the info pane for a floor.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = function(parent) {
const self = this;
// Build floor-level form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
});
parent.appendChild(grid);
let div;
// Bind floor metadata inputs.
// Group Name
div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Floor Name')
.set_placeholder('Unnamed Floor')
.set_width('100%')
.set_maxlength(50)
.set_requirements({
'required': true
})
.render(div);
if (this.state_.active_group.name !== undefined) {
name_input.set_value(this.state_.active_group.name);
}
name_input.addEventListener('input', function() {
self.state_.active_group.name = name_input.get_value();
self.floor_plan_.update_infobox();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
self.state_.active_group.name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
// Elevation
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
.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_requirements({
'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 = 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(beestat.distance(self.state_.active_group.elevation), false);
new beestat.component.modal.floor_plan_elevation_help().render();
}
});
// Ceiling Height
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
.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(5)
.set_requirements({
'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 = 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);
}
});
// Sensor
div = $.createElement('div');
grid.appendChild(div);
};
/**
* Decorate the info pane for a tree.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_tree_ = function(parent) {
const self = this;
const tree = this.state_.active_tree_entity.get_tree();
// Build tree form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(4, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter,
'width': '100%'
});
parent.appendChild(grid);
let div;
// Bind tree identity + geometry inputs.
// Name
div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Tree Name')
.set_placeholder('Unnamed Tree')
.set_width('100%')
.set_maxlength(50)
.render(div);
if (tree.name !== undefined) {
name_input.set_value(tree.name);
}
name_input.addEventListener('input', function() {
tree.name = name_input.get_value();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
tree.name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
// Type
div = $.createElement('div');
grid.appendChild(div);
const format_tree_type = function(value) {
return String(value || '')
.replace(/_/g, ' ')
.replace(/\b\w/g, function(letter) {
return letter.toUpperCase();
});
};
const type_input = new beestat.component.input.select()
.set_label('Type')
.set_width('100%')
.add_option({
'label': format_tree_type('conical'),
'value': 'conical'
})
.add_option({
'label': format_tree_type('round'),
'value': 'round'
})
.add_option({
'label': format_tree_type('oval'),
'value': 'oval'
})
.render(div);
type_input.set_value(['conical', 'round', 'oval'].includes(tree.type) ? tree.type : 'round');
type_input.addEventListener('change', function() {
tree.type = type_input.get_value();
self.update_floor_plan_();
});
// Height
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
.set_label('Height (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': tree.height,
'round': 2
}))
.set_value(beestat.distance({
'distance': tree.height,
'round': 2
}) || '')
.set_width('100%')
.set_maxlength(5)
.set_requirements({
'type': 'decimal',
'min_value': beestat.distance(1),
'required': true
})
.set_transform({
'type': 'round',
'decimals': 2
})
.render(div);
height_input.addEventListener('change', function() {
if (height_input.meets_requirements() === true) {
tree.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(beestat.distance({
'distance': tree.height,
'round': 2
}) || '', false);
}
});
};
/**
* Decorate the info pane for a light source.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_light_source_ = function(parent) {
const self = this;
const light_source = this.state_.active_light_source_entity.get_light_source();
const default_light_elevation = 72;
const get_light_elevation_input_value = function() {
if (
light_source.elevation === undefined ||
Number(light_source.elevation) === default_light_elevation
) {
return '';
}
return beestat.distance({
'distance': Number(light_source.elevation),
'round': 2
}) || '';
};
// Build light-source form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
});
parent.appendChild(grid);
// Bind light-source identity, output, and position inputs.
const div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Light Source Name')
.set_placeholder('Unnamed Light Source')
.set_width('100%')
.set_maxlength(50)
.render(div);
if (light_source.name !== undefined) {
name_input.set_value(light_source.name);
}
name_input.addEventListener('input', function() {
light_source.name = name_input.get_value();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
light_source.name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
const intensity_div = $.createElement('div');
grid.appendChild(intensity_div);
const intensity_input = new beestat.component.input.select()
.set_label('Intensity')
.set_width('100%')
.add_option({'label': 'Dim', 'value': 'dim'})
.add_option({'label': 'Normal', 'value': 'normal'})
.add_option({'label': 'Bright', 'value': 'bright'})
.render(intensity_div);
const normalized_intensity = ['dim', 'normal', 'bright'].includes(light_source.intensity)
? light_source.intensity
: 'normal';
intensity_input.set_value(normalized_intensity);
intensity_input.addEventListener('change', function() {
const value = intensity_input.get_value();
light_source.intensity = ['dim', 'normal', 'bright'].includes(value) ? value : 'normal';
self.update_floor_plan_();
});
const temperature_div = $.createElement('div');
grid.appendChild(temperature_div);
const temperature_input = new beestat.component.input.select()
.set_label('Temperature (K)')
.set_width('100%')
.add_option({'label': '2200K (Candle)', 'value': '2200'})
.add_option({'label': '2700K (Warm)', 'value': '2700'})
.add_option({'label': '3000K (Soft Warm)', 'value': '3000'})
.add_option({'label': '3500K (Neutral Warm)', 'value': '3500'})
.add_option({'label': '4000K (Neutral)', 'value': '4000'})
.add_option({'label': '5000K (Cool)', 'value': '5000'})
.add_option({'label': '6500K (Daylight)', 'value': '6500'})
.render(temperature_div);
const common_temperatures = [2200, 2700, 3000, 3500, 4000, 5000, 6500];
const current_temperature = Math.max(1000, Math.min(12000, Math.round(Number(light_source.temperature_k || 4000))));
let closest_temperature = common_temperatures[0];
let closest_distance = Math.abs(current_temperature - closest_temperature);
for (let i = 1; i < common_temperatures.length; i++) {
const candidate = common_temperatures[i];
const distance = Math.abs(current_temperature - candidate);
if (distance < closest_distance) {
closest_distance = distance;
closest_temperature = candidate;
}
}
temperature_input.set_value(String(closest_temperature));
temperature_input.addEventListener('change', function() {
light_source.temperature_k = Number(temperature_input.get_value() || 4000);
self.update_floor_plan_();
});
const elevation_div = $.createElement('div');
grid.appendChild(elevation_div);
const elevation_input = new beestat.component.input.text()
.set_label('Elevation (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': default_light_elevation,
'round': 2
}))
.set_width('100%')
.set_maxlength(6)
.set_value(get_light_elevation_input_value())
.set_requirements({
'type': 'decimal',
'min_value': beestat.distance(-600),
'max_value': beestat.distance(600),
'required': true
})
.set_transform({
'type': 'round',
'decimals': 2
})
.render(elevation_div);
elevation_input.addEventListener('change', function() {
if (elevation_input.meets_requirements() === true) {
light_source.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_();
return;
}
elevation_input.set_value(get_light_elevation_input_value(), false);
});
};
/**
* Decorate the info pane for a surface.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_surface_ = function(parent) {
const self = this;
const surface = this.state_.active_surface_entity.get_surface();
// Build surface form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(4, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter,
'width': '100%'
});
parent.appendChild(grid);
let div;
// Bind surface identity, appearance, and elevation/height inputs.
// Name
div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Surface Name')
.set_placeholder('Unnamed Surface')
.set_width('100%')
.set_maxlength(50)
.render(div);
if (surface.name !== undefined) {
name_input.set_value(surface.name);
}
name_input.addEventListener('input', function() {
surface.name = name_input.get_value();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
surface.name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
// Surface color preset
div = $.createElement('div');
grid.appendChild(div);
const color_input = new beestat.component.input.select()
.set_label('Color')
.set_width('100%');
const normalize_hex_color = function(value) {
if (value === undefined || value === null) {
return undefined;
}
let normalized = String(value).trim();
if (normalized === '') {
return undefined;
}
if (normalized.charAt(0) !== '#') {
normalized = '#' + normalized;
}
if (/^#[0-9a-fA-F]{6}$/.test(normalized) !== true) {
return undefined;
}
return normalized.toLowerCase();
};
const apply_surface_color = function(color) {
surface.color = color;
self.floor_plan_.update_infobox();
self.update_floor_plan_();
self.rerender();
};
const surface_colors = [
{'label': 'Pavement - Concrete', 'value': '#9a9a96'},
{'label': 'Pavement - Asphalt', 'value': '#1f2328'},
{'label': 'Pavers - Brick', 'value': '#7a2f2a'},
{'label': 'Pavers - Stone', 'value': '#8f877e'},
{'label': 'Wood - Light', 'value': '#c79a6b'},
{'label': 'Wood - Dark', 'value': '#4b2f1f'},
{'label': 'Mulch - Brown', 'value': '#6b4a2f'},
{'label': 'Mulch - Red', 'value': '#7a3f32'},
{'label': 'Mulch - Black', 'value': '#2e3136'},
{'label': 'Water - Pool', 'value': '#3e89b8'},
{'label': 'Water - Natural', 'value': '#3f6f5b'}
];
surface_colors.sort(function(a, b) {
return a.label.localeCompare(b.label, 'en', {'sensitivity': 'base'});
});
surface_colors.push({'label': 'Custom', 'value': '__custom__'});
const preset_color_map = {};
surface_colors.forEach(function(surface_color) {
if (surface_color.value !== '__custom__') {
preset_color_map[surface_color.value] = true;
}
color_input.add_option(surface_color);
});
color_input.render(div);
const custom_color_container = $.createElement('div');
custom_color_container.style('display', 'none');
grid.appendChild(custom_color_container);
const custom_color_input = new beestat.component.input.text()
.set_label('Custom Hex')
.set_placeholder('#RRGGBB')
.set_width('100%')
.set_maxlength(7)
.render(custom_color_container);
const current_surface_color = normalize_hex_color(surface.color) || '#9a9a96';
const is_preset_color = preset_color_map[current_surface_color] === true;
if (is_preset_color === true) {
color_input.set_value(current_surface_color);
custom_color_input.set_value('', false);
custom_color_container.style('display', 'none');
} else {
color_input.set_value('__custom__');
custom_color_input.set_value(current_surface_color, false);
custom_color_container.style('display', 'block');
}
color_input.addEventListener('change', function() {
const selected_value = color_input.get_value();
if (selected_value === '__custom__') {
const custom_color = normalize_hex_color(custom_color_input.get_value()) ||
normalize_hex_color(surface.color) ||
'#9a9a96';
custom_color_input.set_value(custom_color, false);
custom_color_container.style('display', 'block');
custom_color_input.input_.focus();
return;
}
custom_color_input.set_value('', false);
custom_color_container.style('display', 'none');
apply_surface_color(selected_value);
});
custom_color_input.addEventListener('change', function() {
if (color_input.get_value() !== '__custom__') {
return;
}
const custom_color = normalize_hex_color(custom_color_input.get_value());
if (custom_color === undefined) {
custom_color_input.set_value(surface.color || '#9a9a96', false);
return;
}
custom_color_input.set_value(custom_color, false);
apply_surface_color(custom_color);
});
// Elevation
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
.set_label('Elevation (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': self.state_.active_group.elevation,
'round': 2
}))
.set_value(beestat.distance({
'distance': surface.elevation,
'round': 2
}) || '')
.set_width('100%')
.set_maxlength(5)
.set_requirements({
'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) {
surface.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('', false);
}
});
// Height
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
.set_label('Height (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': surface.height || 0,
'round': 2
}))
.set_value(beestat.distance({
'distance': surface.height || 0,
'round': 2
}) || '')
.set_width('100%')
.set_maxlength(5)
.set_requirements({
'type': 'decimal',
'min_value': beestat.distance(0)
})
.set_transform({
'type': 'round',
'decimals': 2
})
.render(div);
height_input.addEventListener('change', function() {
if (height_input.meets_requirements() === true) {
surface.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);
}
});
};
/**
* Decorate the info pane for an opening.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_opening_ = function(parent) {
const self = this;
const opening = this.state_.active_opening_entity.get_opening();
const get_opening_default_height = function(type) {
return (type === 'window' || type === 'glass') ? 60 : 78;
};
const get_opening_default_elevation = function(type) {
return (type === 'window' || type === 'glass') ? 24 : 0;
};
const get_opening_height_input_value = function() {
const default_height = get_opening_default_height(opening.type);
if (
opening.height === undefined ||
Number(opening.height) === default_height
) {
return '';
}
return beestat.distance({
'distance': Number(opening.height),
'round': 2
}) || '';
};
const get_opening_elevation_input_value = function() {
const default_elevation = get_opening_default_elevation(opening.type);
if (
opening.elevation === undefined ||
Number(opening.elevation) === default_elevation
) {
return '';
}
return beestat.distance({
'distance': Number(opening.elevation),
'round': 2
}) || '';
};
// Build opening form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
});
parent.appendChild(grid);
let div;
// Bind opening identity, type, and dimensional inputs.
// Name
div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Opening Name')
.set_placeholder('Unnamed Opening')
.set_width('100%')
.set_maxlength(50)
.render(div);
if (opening.name !== undefined) {
name_input.set_value(opening.name);
}
name_input.addEventListener('input', function() {
opening.name = name_input.get_value();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
opening.name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
// Type
div = $.createElement('div');
grid.appendChild(div);
const type_input = new beestat.component.input.select()
.set_label('Type')
.set_width('100%')
.add_option({'label': 'Window', 'value': 'window'})
.add_option({'label': 'Door', 'value': 'door'})
.add_option({'label': 'Glass', 'value': 'glass'})
.add_option({'label': 'Opening', 'value': 'empty'})
.render(div);
type_input.set_value(['empty', 'door', 'window', 'glass'].includes(opening.type) ? opening.type : 'empty');
type_input.addEventListener('change', function() {
const previous_type = opening.type;
opening.type = type_input.get_value();
const previous_default_height = get_opening_default_height(previous_type);
const previous_default_elevation = get_opening_default_elevation(previous_type);
const next_default_height = get_opening_default_height(opening.type);
const next_default_elevation = get_opening_default_elevation(opening.type);
if (Number(opening.height !== undefined ? opening.height : previous_default_height) === previous_default_height) {
opening.height = next_default_height;
}
if (Number(opening.elevation !== undefined ? opening.elevation : previous_default_elevation) === previous_default_elevation) {
opening.elevation = next_default_elevation;
}
if (opening.type === 'door') {
opening.color = opening.color || '#7a573b';
} else {
delete opening.color;
}
self.update_floor_plan_();
self.rerender();
});
if (opening.type === 'door') {
div = $.createElement('div');
grid.appendChild(div);
const door_color_input = new beestat.component.input.select()
.set_label('Door Color')
.set_width('100%')
.add_option({'label': 'Black', 'value': '#4a4a4a'})
.add_option({'label': 'Blue', 'value': '#365e9d'})
.add_option({'label': 'Brown', 'value': '#7a573b'})
.add_option({'label': 'Gray', 'value': '#808890'})
.add_option({'label': 'Green', 'value': '#4b6a4b'})
.add_option({'label': 'Red', 'value': '#8a3e3a'})
.add_option({'label': 'White', 'value': '#f4f4f2'})
.render(div);
door_color_input.set_value(String(opening.color || '#7a573b'));
door_color_input.addEventListener('change', function() {
opening.color = door_color_input.get_value();
self.update_floor_plan_();
});
}
// Height
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
.set_label('Height (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': get_opening_default_height(opening.type),
'round': 2
}))
.set_value(get_opening_height_input_value())
.set_width('100%')
.set_maxlength(5)
.set_requirements({
'type': 'decimal',
'min_value': beestat.distance(1),
'required': true
})
.set_transform({
'type': 'round',
'decimals': 2
})
.render(div);
height_input.addEventListener('change', function() {
if (height_input.meets_requirements() === true) {
opening.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(get_opening_height_input_value(), false);
}
});
// Elevation
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
.set_label('Elevation (' + beestat.setting('units.distance') + ')')
.set_placeholder(beestat.distance({
'distance': get_opening_default_elevation(opening.type),
'round': 2
}))
.set_value(get_opening_elevation_input_value())
.set_width('100%')
.set_maxlength(5)
.set_requirements({
'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) {
opening.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_();
} else {
elevation_input.set_value(get_opening_elevation_input_value(), false);
}
});
};
/**
* Decorate the info pane for a room.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = function(parent) {
const self = this;
// Build room form layout.
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
});
parent.appendChild(grid);
let div;
// Bind room identity, geometry, and sensor assignment inputs.
// Room Name
div = $.createElement('div');
grid.appendChild(div);
const name_input = new beestat.component.input.text()
.set_label('Room Name')
.set_placeholder('Unnamed Room')
.set_width('100%')
.set_maxlength(50)
.set_requirements({
'required': true
})
.render(div);
if (this.state_.active_room_entity.get_room().name !== undefined) {
name_input.set_value(this.state_.active_room_entity.get_room().name);
}
name_input.addEventListener('input', function() {
self.state_.active_room_entity.get_room().name = name_input.get_value();
self.floor_plan_.update_infobox();
self.update_layers_sidebar_();
});
name_input.addEventListener('change', function() {
self.state_.active_room_entity.get_room().name = name_input.get_value();
self.update_floor_plan_();
self.update_layers_sidebar_();
});
// Elevation
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
.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_requirements({
'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 = 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('', false);
new beestat.component.modal.floor_plan_elevation_help().render();
}
});
// Ceiling Height
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
.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(5)
.set_requirements({
'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 = 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);
}
});
// Sensor
div = $.createElement('div');
div.style('position', 'relative');
grid.appendChild(div);
const sensor_input = new beestat.component.input.select()
.add_option({
'label': 'None',
'value': ''
})
.set_width('100%')
.set_label('Sensor');
const sensors = {};
Object.values(beestat.cache.thermostat).forEach(function(thermostat) {
const thermostat_sensors = Object.values(beestat.cache.sensor).filter(function(sensor) {
return sensor.thermostat_id === thermostat.thermostat_id;
})
.sort(function(a, b) {
return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'});
});
sensors[thermostat.thermostat_id] = thermostat_sensors;
});
// Put the sensors in the select.
for (let thermostat_id in sensors) {
const thermostat = beestat.cache.thermostat[thermostat_id];
sensors[thermostat_id].forEach(function(sensor) {
sensor_input.add_option({
'group': thermostat.name,
'value': sensor.sensor_id,
'label': sensor.name
});
});
}
sensor_input.render(div);
if (
self.state_.active_room_entity.get_room().sensor_id !== undefined &&
beestat.cache.sensor[self.state_.active_room_entity.get_room().sensor_id] !== undefined
) {
sensor_input.set_value(self.state_.active_room_entity.get_room().sensor_id);
} else {
sensor_input.set_value('');
}
sensor_input.addEventListener('change', function() {
const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
beestat.setting('visualize.floor_plan_id')
));
if (sensor_input.get_value() === '') {
delete self.state_.active_room_entity.get_room().sensor_id;
} else {
self.state_.active_room_entity.get_room().sensor_id = Number(sensor_input.get_value());
}
const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
beestat.setting('visualize.floor_plan_id')
));
// Delete data if the overall sensor set changes so it's re-fetched.
if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
beestat.cache.delete('data.three_d__runtime_sensor');
}
// For the help box
self.update_info_pane_();
self.update_floor_plan_();
});
// Help container
if (
Object.keys(beestat.floor_plan.get_sensor_ids_map(beestat.setting('visualize.floor_plan_id'))).length === 0 &&
this.state_.active_room_entity !== undefined
) {
const help_container = document.createElement('div');
Object.assign(help_container.style, {
'position': 'absolute',
'left': 0,
'top': '-9px'
});
div.appendChild(help_container);
this.helper_tile_ = new beestat.component.tile()
.set_text('Assign a sensor')
.set_shadow(false)
.set_background_color(beestat.style.color.green.base)
.set_text_color('#fff')
.set_type('pill')
.set_size('small')
.set_icon('arrow_down')
.render($(help_container));
sensor_input.set_label('');
}
};
/**
* Rerender just the info pane to avoid rerendering the entire SVG for
* resizes, drags, etc. This isn't super ideal but without making the info
* pane a separate component this is the way.
*/
beestat.component.card.floor_plan_editor.prototype.update_info_pane_ = function() {
var old_parent = this.info_pane_container_;
this.info_pane_container_ = $.createElement('div')
.style('margin-top', beestat.style.size.gutter / 2);
this.decorate_info_pane_(this.info_pane_container_);
old_parent.parentNode().replaceChild(this.info_pane_container_, old_parent);
};
/**
* Rerender just the top floor pane tile to avoid rerendering the entire SVG
* for resizes, drags, etc. This isn't super ideal but without making the info
* pane a separate component this is the way.
*/
beestat.component.card.floor_plan_editor.prototype.update_floor_plan_tile_ = function() {
this.floor_plan_tile_.rerender();
};
/**
* Get the title of the card.
*
* @return {string} The title.
*/
beestat.component.card.floor_plan_editor.prototype.get_title_ = function() {
return 'Floor Plan';
};
/**
* Update the floor plan in the database. This is throttled so the update can
* only run so fast.
*/
beestat.component.card.floor_plan_editor.prototype.update_floor_plan_ = function() {
const floor_plan_id = beestat.setting('visualize.floor_plan_id');
// Fake this event since the cache is being directly modified.
beestat.dispatcher.dispatchEvent('cache.floor_plan');
beestat.floor_plan.queue_data_save_(floor_plan_id, 1000);
};
/**
* Decorate the menu.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.floor_plan_editor.prototype.decorate_top_right_ = function(parent) {
const self = this;
const menu = (new beestat.component.menu()).render(parent);
if (window.is_demo === false) {
if (Object.keys(beestat.cache.floor_plan).length > 1) {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Switch')
.set_icon('home_switch')
.set_callback(function() {
(new beestat.component.modal.change_floor_plan()).render();
}));
}
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Add New')
.set_icon('plus')
.set_callback(function() {
new beestat.component.modal.create_floor_plan(
self.thermostat_id_
).render();
}));
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('visualize.floor_plan_id')
).render();
}));
}
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('visualize.floor_plan_id')
).render();
}));
}
}
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
window.open('https://doc.beestat.io/86f6e4c44fc84c3cb4e8fb7b16d3d160');
}));
};