mirror of
https://github.com/beestat/app.git
synced 2026-02-26 13:10:23 -05:00
2534 lines
78 KiB
JavaScript
2534 lines
78 KiB
JavaScript
/**
|
||
* 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');
|
||
}));
|
||
};
|