mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
1145 lines
33 KiB
JavaScript
1145 lines
33 KiB
JavaScript
/**
|
|
* SVG Document
|
|
*
|
|
* @param {number} floor_plan_id The floor_plan_id to show.
|
|
* @param {object} state Shared state.
|
|
*/
|
|
beestat.component.floor_plan = function(floor_plan_id, state) {
|
|
this.toolbar_buttons_ = {};
|
|
this.floor_plan_id_ = floor_plan_id;
|
|
|
|
beestat.component.apply(this, arguments);
|
|
|
|
/**
|
|
* Override this component's state with a state common to all floor plan
|
|
* entities.
|
|
*/
|
|
this.state_ = state;
|
|
};
|
|
beestat.extend(beestat.component.floor_plan, beestat.component);
|
|
|
|
/**
|
|
* Render the SVG document to the parent.
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*
|
|
* @return {beestat.component.floor_plan} This
|
|
*/
|
|
beestat.component.floor_plan.prototype.render = function(parent) {
|
|
const self = this;
|
|
|
|
this.parent_ = parent;
|
|
|
|
this.svg_ = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
this.defs_ = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
this.svg_.appendChild(this.defs_);
|
|
|
|
this.g_ = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
this.svg_.appendChild(this.g_);
|
|
|
|
this.add_grid_();
|
|
|
|
this.width_ = this.state_.floor_plan_width || 800;
|
|
this.height_ = 500;
|
|
|
|
this.svg_.setAttribute('width', this.width_);
|
|
this.svg_.setAttribute('height', this.height_);
|
|
|
|
this.svg_.style.background = beestat.style.color.bluegray.dark;
|
|
this.svg_.style.userSelect = 'none';
|
|
this.svg_.style.touchAction = 'none';
|
|
|
|
this.set_zoomable_();
|
|
this.set_draggable_();
|
|
|
|
if (this.state_.floor_plan_view_box === undefined) {
|
|
this.view_box_ = {
|
|
'x': 0,
|
|
'y': 0,
|
|
'width': this.width_,
|
|
'height': this.height_
|
|
};
|
|
} else {
|
|
this.view_box_ = this.state_.floor_plan_view_box;
|
|
}
|
|
this.update_view_box_();
|
|
|
|
this.toolbar_container_ = $.createElement('div');
|
|
this.toolbar_container_.style({
|
|
'position': 'absolute',
|
|
'top': beestat.style.size.gutter,
|
|
'left': beestat.style.size.gutter + (beestat.style.size.gutter / 2),
|
|
'width': '40px'
|
|
});
|
|
parent.appendChild(this.toolbar_container_);
|
|
|
|
this.floors_container_ = $.createElement('div');
|
|
this.floors_container_.style({
|
|
'position': 'absolute',
|
|
'top': beestat.style.size.gutter,
|
|
'left': beestat.style.size.gutter * 4
|
|
});
|
|
parent.appendChild(this.floors_container_);
|
|
|
|
this.update_toolbar();
|
|
|
|
this.infobox_container_ = $.createElement('div');
|
|
this.infobox_container_.style({
|
|
'position': 'absolute',
|
|
'color': beestat.style.color.gray.base,
|
|
'top': beestat.style.size.gutter,
|
|
'right': beestat.style.size.gutter,
|
|
'line-height': 32,
|
|
'user-select': 'none'
|
|
});
|
|
parent.appendChild(this.infobox_container_);
|
|
|
|
this.update_infobox();
|
|
|
|
parent.appendChild(this.svg_);
|
|
|
|
this.keydown_handler_ = function(e) {
|
|
if (e.target.nodeName === 'BODY') {
|
|
if (e.key === 'Escape') {
|
|
if (self.state_.active_room_entity !== undefined) {
|
|
self.clear_room_();
|
|
}
|
|
} else if (e.key === 'Delete') {
|
|
if (self.state_.active_point_entity !== undefined) {
|
|
self.remove_point_();
|
|
} else if (self.state_.active_room_entity !== undefined) {
|
|
self.remove_room_();
|
|
}
|
|
} else if (e.key.toLowerCase() === 'r') {
|
|
if (e.ctrlKey === false) {
|
|
self.add_room_();
|
|
}
|
|
} else if (e.key.toLowerCase() === 's') {
|
|
self.toggle_snapping_();
|
|
} else if (
|
|
e.key.toLowerCase() === 'c' &&
|
|
e.ctrlKey === true &&
|
|
self.state_.active_room_entity !== undefined
|
|
) {
|
|
self.state_.copied_room = beestat.clone(
|
|
self.state_.active_room_entity.get_room()
|
|
);
|
|
} else if (
|
|
e.key.toLowerCase() === 'v' &&
|
|
e.ctrlKey === true &&
|
|
self.state_.copied_room !== undefined
|
|
) {
|
|
self.add_room_(self.state_.copied_room);
|
|
} else if (
|
|
e.key.toLowerCase() === 'z' &&
|
|
e.ctrlKey === true
|
|
) {
|
|
self.undo_();
|
|
} else if (
|
|
e.key.toLowerCase() === 'y' &&
|
|
e.ctrlKey === true
|
|
) {
|
|
self.redo_();
|
|
} else if (
|
|
e.key === 'ArrowLeft' ||
|
|
e.key === 'ArrowRight' ||
|
|
e.key === 'ArrowUp' ||
|
|
e.key === 'ArrowDown'
|
|
) {
|
|
const entity =
|
|
self.state_.active_point_entity ||
|
|
self.state_.active_wall_entity ||
|
|
self.state_.active_room_entity;
|
|
|
|
if (entity !== undefined) {
|
|
const x = entity.get_x();
|
|
const y = entity.get_y();
|
|
|
|
switch (e.key) {
|
|
case 'ArrowLeft':
|
|
entity.set_xy(x === null ? null : x - 1, y, 'update');
|
|
break;
|
|
case 'ArrowRight':
|
|
entity.set_xy(x === null ? null : x + 1, y, 'update');
|
|
break;
|
|
case 'ArrowUp':
|
|
entity.set_xy(x, y === null ? null : y - 1, 'update');
|
|
break;
|
|
case 'ArrowDown':
|
|
entity.set_xy(x, y === null ? null : y + 1, 'update');
|
|
break;
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', this.keydown_handler_);
|
|
|
|
this.rendered_ = true;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Update the view box with the current values. Cap so the grid doesn't go out
|
|
* of view.
|
|
*/
|
|
beestat.component.floor_plan.prototype.update_view_box_ = function() {
|
|
// Cap x/y pan
|
|
const min_x = this.grid_pixels_ / -2;
|
|
const max_x = (this.grid_pixels_ / 2) - (this.width_ * this.get_scale());
|
|
this.view_box_.x = Math.min(Math.max(this.view_box_.x, min_x), max_x);
|
|
|
|
const min_y = this.grid_pixels_ / -2;
|
|
const max_y = (this.grid_pixels_ / 2) - (this.height_ * this.get_scale());
|
|
this.view_box_.y = Math.min(Math.max(this.view_box_.y, min_y), max_y);
|
|
|
|
this.svg_.setAttribute(
|
|
'viewBox',
|
|
this.view_box_.x + ' ' + this.view_box_.y + ' ' + this.view_box_.width + ' ' + this.view_box_.height
|
|
);
|
|
|
|
this.state_.floor_plan_view_box = this.view_box_;
|
|
};
|
|
|
|
/**
|
|
* Add a helpful grid.
|
|
*/
|
|
beestat.component.floor_plan.prototype.add_grid_ = function() {
|
|
const pixels_per_small_grid = 12;
|
|
const small_grids_per_large_grid = 10;
|
|
const pixels_per_large_grid = pixels_per_small_grid * small_grids_per_large_grid;
|
|
|
|
const large_grid_repeat = 100;
|
|
this.grid_pixels_ = pixels_per_large_grid * large_grid_repeat;
|
|
|
|
const grid_small_pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
|
|
grid_small_pattern.setAttribute('id', 'grid_small');
|
|
grid_small_pattern.setAttribute('width', pixels_per_small_grid);
|
|
grid_small_pattern.setAttribute('height', pixels_per_small_grid);
|
|
grid_small_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
|
|
this.defs_.appendChild(grid_small_pattern);
|
|
|
|
const grid_small_path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
grid_small_path.setAttribute('d', 'M ' + pixels_per_small_grid + ' 0 L 0 0 0 ' + pixels_per_small_grid);
|
|
grid_small_path.setAttribute('fill', 'none');
|
|
grid_small_path.setAttribute('stroke', beestat.style.color.bluegreen.dark);
|
|
grid_small_path.setAttribute('stroke-width', '0.5');
|
|
grid_small_pattern.appendChild(grid_small_path);
|
|
|
|
const grid_large_pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
|
|
grid_large_pattern.setAttribute('id', 'grid_large');
|
|
grid_large_pattern.setAttribute('width', pixels_per_large_grid);
|
|
grid_large_pattern.setAttribute('height', pixels_per_large_grid);
|
|
grid_large_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
|
|
this.defs_.appendChild(grid_large_pattern);
|
|
|
|
const grid_large_rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
grid_large_rect.setAttribute('width', pixels_per_large_grid);
|
|
grid_large_rect.setAttribute('height', pixels_per_large_grid);
|
|
grid_large_rect.setAttribute('fill', 'url("#grid_small")');
|
|
grid_large_pattern.appendChild(grid_large_rect);
|
|
|
|
const grid_large_path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
grid_large_path.setAttribute('d', 'M ' + pixels_per_large_grid + ' 0 L 0 0 0 ' + pixels_per_large_grid);
|
|
grid_large_path.setAttribute('fill', 'none');
|
|
grid_large_path.setAttribute('stroke', beestat.style.color.bluegreen.dark);
|
|
grid_large_path.setAttribute('stroke-width', '1');
|
|
grid_large_pattern.appendChild(grid_large_path);
|
|
|
|
const grid_rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
grid_rect.setAttribute('x', this.grid_pixels_ / -2);
|
|
grid_rect.setAttribute('y', this.grid_pixels_ / -2);
|
|
grid_rect.setAttribute('width', this.grid_pixels_);
|
|
grid_rect.setAttribute('height', this.grid_pixels_);
|
|
grid_rect.setAttribute('fill', 'url("#grid_large")');
|
|
this.g_.appendChild(grid_rect);
|
|
};
|
|
|
|
/**
|
|
* Make the SVG document zoomable.
|
|
*/
|
|
beestat.component.floor_plan.prototype.set_zoomable_ = function() {
|
|
const self = this;
|
|
|
|
this.wheel_handler_ = function(e) {
|
|
if (
|
|
e.ctrlKey === true &&
|
|
self.parent_[0].contains(e.target)
|
|
) {
|
|
e.preventDefault();
|
|
|
|
if (e.wheelDelta < 0) {
|
|
self.zoom_out_(e);
|
|
} else {
|
|
self.zoom_in_(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('wheel', this.wheel_handler_, {'passive': false});
|
|
};
|
|
|
|
/**
|
|
* Make the SVG document draggabe.
|
|
*/
|
|
beestat.component.floor_plan.prototype.set_draggable_ = function() {
|
|
const self = this;
|
|
|
|
const mousedown_handler = function(e) {
|
|
// Prevent things underneath from also dragging.
|
|
e.stopPropagation();
|
|
|
|
self.drag_start_mouse_ = {
|
|
'x': e.clientX || e.touches[0].clientX,
|
|
'y': e.clientY || e.touches[0].clientY
|
|
};
|
|
|
|
self.drag_start_pan_ = {
|
|
'x': self.view_box_.x,
|
|
'y': self.view_box_.y
|
|
};
|
|
|
|
self.dragging_ = true;
|
|
};
|
|
|
|
this.mousemove_handler_ = function(e) {
|
|
if (self.dragging_ === true) {
|
|
const dx = ((e.clientX || e.touches[0].clientX) - self.drag_start_mouse_.x);
|
|
const dy = ((e.clientY || e.touches[0].clientY) - self.drag_start_mouse_.y);
|
|
self.view_box_.x = self.drag_start_pan_.x - (dx * self.get_scale());
|
|
self.view_box_.y = self.drag_start_pan_.y - (dy * self.get_scale());
|
|
self.update_view_box_();
|
|
}
|
|
};
|
|
|
|
this.mouseup_handler_ = function(e) {
|
|
// Deselect when clicking on the background.
|
|
if (
|
|
self.parent_.contains(e.target) &&
|
|
self.drag_start_mouse_ !== undefined &&
|
|
e.clientX === self.drag_start_mouse_.x &&
|
|
e.clientY === self.drag_start_mouse_.y
|
|
) {
|
|
self.clear_room_();
|
|
}
|
|
|
|
self.dragging_ = false;
|
|
};
|
|
|
|
this.svg_.addEventListener('mousedown', mousedown_handler.bind(this));
|
|
this.svg_.addEventListener('touchstart', mousedown_handler.bind(this));
|
|
|
|
window.addEventListener('mousemove', this.mousemove_handler_);
|
|
window.addEventListener('touchmove', this.mousemove_handler_);
|
|
|
|
window.addEventListener('mouseup', this.mouseup_handler_);
|
|
window.addEventListener('touchend', this.mouseup_handler_);
|
|
};
|
|
|
|
/**
|
|
* Get the root group so other things can put stuff here.
|
|
*
|
|
* @return {SVGGElement} The root group.
|
|
*/
|
|
beestat.component.floor_plan.prototype.get_g = function() {
|
|
return this.g_;
|
|
};
|
|
|
|
/**
|
|
* Get the scale.
|
|
*
|
|
* @return {SVGGElement} The scale.
|
|
*/
|
|
beestat.component.floor_plan.prototype.get_scale = function() {
|
|
return this.view_box_.width / this.width_;
|
|
};
|
|
|
|
/**
|
|
* Get the grid pixels.
|
|
*
|
|
* @return {SVGGElement} The scale.
|
|
*/
|
|
beestat.component.floor_plan.prototype.get_grid_pixels = function() {
|
|
return this.grid_pixels_;
|
|
};
|
|
|
|
/**
|
|
* Convert from screen (global) coordinates to svg (local) coordinates.
|
|
*
|
|
* @param {Event} e
|
|
*
|
|
* @return {SVGPoint} A point in the SVG local coordinate space.
|
|
*/
|
|
beestat.component.floor_plan.prototype.get_local_point = function(e) {
|
|
const global_point = this.svg_.createSVGPoint();
|
|
global_point.x = e.clientX;
|
|
global_point.y = e.clientY;
|
|
|
|
return global_point.matrixTransform(
|
|
this.svg_.getScreenCTM().inverse()
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Remove this component from the page.
|
|
*/
|
|
beestat.component.floor_plan.prototype.dispose = function() {
|
|
if (this.rendered_ === true) {
|
|
window.removeEventListener('keydown', this.keydown_handler_);
|
|
window.removeEventListener('wheel', this.wheel_handler_);
|
|
window.removeEventListener('mousemove', this.mousemove_handler_);
|
|
window.removeEventListener('touchmove', this.mousemove_handler_);
|
|
window.removeEventListener('mouseup', this.mouseup_handler_);
|
|
window.removeEventListener('touchend', this.mouseup_handler_);
|
|
this.rendered_ = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the toolbar to match the current state.
|
|
*/
|
|
beestat.component.floor_plan.prototype.update_toolbar = function() {
|
|
const self = this;
|
|
|
|
if (this.tile_group_ !== undefined) {
|
|
this.tile_group_.dispose();
|
|
}
|
|
|
|
if (this.tile_group_floors_ !== undefined) {
|
|
this.tile_group_floors_.dispose();
|
|
}
|
|
|
|
this.tile_group_ = new beestat.component.tile_group();
|
|
|
|
// Add floor
|
|
this.tile_group_.add_tile(new beestat.component.tile()
|
|
.set_icon('layers')
|
|
.set_shadow(false)
|
|
.set_text_color(beestat.style.color.lightblue.base)
|
|
);
|
|
|
|
// Add room
|
|
this.tile_group_.add_tile(new beestat.component.tile()
|
|
.set_icon('card_plus_outline')
|
|
.set_title('Add Room [R]')
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.set_background_color(beestat.style.color.bluegray.base)
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.addEventListener('click', function() {
|
|
self.add_room_();
|
|
})
|
|
);
|
|
|
|
// Remove room
|
|
const remove_room_button = new beestat.component.tile()
|
|
.set_icon('card_remove_outline')
|
|
.set_title('Remove Room [Delete]')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(remove_room_button);
|
|
|
|
if (this.state_.active_room_entity !== undefined) {
|
|
remove_room_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.red.base)
|
|
.addEventListener('click', this.remove_room_.bind(this));
|
|
} else {
|
|
remove_room_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Add point
|
|
const add_point_button = new beestat.component.tile()
|
|
.set_icon('vector_square_plus')
|
|
.set_title('Add Point [Double click]')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(add_point_button);
|
|
|
|
if (this.state_.active_wall_entity !== undefined) {
|
|
add_point_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.addEventListener('click', this.add_point_.bind(this));
|
|
} else {
|
|
add_point_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Remove point
|
|
const remove_point_button = new beestat.component.tile()
|
|
.set_background_color(beestat.style.color.bluegray.base)
|
|
.set_title('Remove Point [Delete]')
|
|
.set_icon('vector_square_remove');
|
|
this.tile_group_.add_tile(remove_point_button);
|
|
|
|
if (
|
|
this.state_.active_point_entity !== undefined &&
|
|
this.state_.active_room_entity.get_room().points.length > 3
|
|
) {
|
|
remove_point_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.red.base)
|
|
.addEventListener('click', this.remove_point_.bind(this));
|
|
} else {
|
|
remove_point_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Toggle snap to grid
|
|
let snapping_icon;
|
|
let snapping_title;
|
|
if (this.state_.snapping === true) {
|
|
snapping_icon = 'grid';
|
|
snapping_title = 'Disable Snapping [S]';
|
|
} else {
|
|
snapping_icon = 'grid_off';
|
|
snapping_title = 'Enable Snapping [S]';
|
|
}
|
|
|
|
this.tile_group_.add_tile(new beestat.component.tile()
|
|
.set_icon(snapping_icon)
|
|
.set_title(snapping_title)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.set_background_color(beestat.style.color.bluegray.base)
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.addEventListener('click', this.toggle_snapping_.bind(this))
|
|
);
|
|
|
|
// Undo
|
|
const undo_button = new beestat.component.tile()
|
|
.set_icon('undo')
|
|
.set_title('Undo [Ctrl+Z]')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(undo_button);
|
|
|
|
if (
|
|
this.can_undo_() === true
|
|
) {
|
|
undo_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.addEventListener('click', function() {
|
|
self.undo_();
|
|
});
|
|
} else {
|
|
undo_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Redo
|
|
const redo_button = new beestat.component.tile()
|
|
.set_icon('redo')
|
|
.set_title('Redo [Ctrl+Y]')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(redo_button);
|
|
|
|
if (
|
|
this.can_redo_() === true
|
|
) {
|
|
redo_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.addEventListener('click', function() {
|
|
self.redo_();
|
|
});
|
|
} else {
|
|
redo_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Zoom in
|
|
const zoom_in_button = new beestat.component.tile()
|
|
.set_icon('magnify_plus_outline')
|
|
.set_title('Zoom In')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(zoom_in_button);
|
|
|
|
if (
|
|
this.can_zoom_in_() === true
|
|
) {
|
|
zoom_in_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.addEventListener('click', function() {
|
|
self.zoom_in_();
|
|
});
|
|
} else {
|
|
zoom_in_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Zoom out
|
|
const zoom_out_button = new beestat.component.tile()
|
|
.set_icon('magnify_minus_outline')
|
|
.set_title('Zoom out')
|
|
.set_background_color(beestat.style.color.bluegray.base);
|
|
this.tile_group_.add_tile(zoom_out_button);
|
|
|
|
if (
|
|
this.can_zoom_out_() === true
|
|
) {
|
|
zoom_out_button
|
|
.set_background_hover_color(beestat.style.color.bluegray.light)
|
|
.set_text_color(beestat.style.color.gray.light)
|
|
.addEventListener('click', function() {
|
|
self.zoom_out_();
|
|
});
|
|
} else {
|
|
zoom_out_button
|
|
.set_text_color(beestat.style.color.bluegray.dark);
|
|
}
|
|
|
|
// Render
|
|
this.tile_group_.render(this.toolbar_container_);
|
|
|
|
// FLOORS
|
|
this.tile_group_floors_ = new beestat.component.tile_group();
|
|
|
|
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
|
|
|
const sorted_groups = Object.values(floor_plan.data.groups)
|
|
.sort(function(a, b) {
|
|
return a.elevation > b.elevation;
|
|
});
|
|
|
|
let icon_number = 1;
|
|
sorted_groups.forEach(function(group) {
|
|
const button = new beestat.component.tile()
|
|
.set_title(group.name)
|
|
.set_shadow(false)
|
|
.set_text_hover_color(beestat.style.color.lightblue.light)
|
|
.set_text_color(beestat.style.color.lightblue.base);
|
|
|
|
let icon;
|
|
if (group.elevation < 0) {
|
|
icon = 'alpha_b';
|
|
} else {
|
|
icon = 'numeric_' + icon_number++;
|
|
}
|
|
|
|
if (group === self.state_.active_group) {
|
|
button
|
|
.set_icon(icon + '_box');
|
|
} else {
|
|
button
|
|
.set_icon(icon)
|
|
.addEventListener('click', function() {
|
|
if (self.state_.active_room_entity !== undefined) {
|
|
self.state_.active_room_entity.set_active(false);
|
|
}
|
|
if (self.state_.active_wall_entity !== undefined) {
|
|
self.state_.active_wall_entity.set_active(false);
|
|
}
|
|
if (self.state_.active_point_entity !== undefined) {
|
|
self.state_.active_point_entity.set_active(false);
|
|
}
|
|
|
|
self.state_.active_group = group;
|
|
self.dispatchEvent('change_group');
|
|
});
|
|
}
|
|
|
|
self.tile_group_floors_.add_tile(button);
|
|
});
|
|
|
|
this.tile_group_floors_.render(this.floors_container_);
|
|
};
|
|
|
|
/**
|
|
* Update the infobox to match the current state.
|
|
*/
|
|
beestat.component.floor_plan.prototype.update_infobox = function() {
|
|
const parts = [];
|
|
if (this.state_.active_room_entity !== undefined) {
|
|
parts.push(this.state_.active_room_entity.get_room().name || 'Unnamed Room');
|
|
parts.push(
|
|
beestat.floor_plan.get_area_room(this.state_.active_room_entity.get_room())
|
|
.toLocaleString() + ' sqft'
|
|
);
|
|
} else {
|
|
parts.push(this.state_.active_group.name || 'Unnamed Floor');
|
|
parts.push(
|
|
beestat.floor_plan.get_area_group(this.state_.active_group)
|
|
.toLocaleString() + ' sqft'
|
|
);
|
|
}
|
|
this.infobox_container_.innerText(parts.join(' • '));
|
|
};
|
|
|
|
/**
|
|
* Toggle snapping.
|
|
*/
|
|
beestat.component.floor_plan.prototype.toggle_snapping_ = function() {
|
|
this.state_.snapping = !this.state_.snapping;
|
|
this.update_toolbar();
|
|
};
|
|
|
|
/**
|
|
* Add a new room.
|
|
*
|
|
* @param {object} room Optional room to copy from.
|
|
*/
|
|
beestat.component.floor_plan.prototype.add_room_ = function(room) {
|
|
this.save_buffer();
|
|
|
|
const svg_view_box = this.view_box_;
|
|
|
|
let new_room;
|
|
if (room === undefined) {
|
|
const new_room_size = 120;
|
|
new_room = {
|
|
'room_id': window.crypto.randomUUID(),
|
|
'x': svg_view_box.x + (svg_view_box.width / 2) - (new_room_size / 2),
|
|
'y': svg_view_box.y + (svg_view_box.height / 2) - (new_room_size / 2),
|
|
'points': [
|
|
{
|
|
'x': 0,
|
|
'y': 0
|
|
},
|
|
{
|
|
'x': new_room_size,
|
|
'y': 0
|
|
},
|
|
{
|
|
'x': new_room_size,
|
|
'y': new_room_size
|
|
},
|
|
{
|
|
'x': 0,
|
|
'y': new_room_size
|
|
}
|
|
]
|
|
};
|
|
} else {
|
|
let min_x = Infinity;
|
|
let max_x = -Infinity;
|
|
let min_y = Infinity;
|
|
let max_y = -Infinity;
|
|
|
|
room.points.forEach(function(point) {
|
|
min_x = Math.min(room.x + point.x, min_x);
|
|
max_x = Math.max(room.x + point.x, max_x);
|
|
min_y = Math.min(room.y + point.y, min_y);
|
|
max_y = Math.max(room.y + point.y, max_y);
|
|
});
|
|
|
|
new_room = {
|
|
'room_id': window.crypto.randomUUID(),
|
|
'x': svg_view_box.x + (svg_view_box.width / 2) - ((max_x - min_x) / 2),
|
|
'y': svg_view_box.y + (svg_view_box.height / 2) - ((max_y - min_y) / 2),
|
|
'points': beestat.clone(room.points)
|
|
};
|
|
}
|
|
|
|
this.state_.active_group.rooms.push(new_room);
|
|
new beestat.component.floor_plan_entity.room(this, this.state_)
|
|
.set_room(new_room)
|
|
.set_group(this.state_.active_group)
|
|
.set_active(true);
|
|
|
|
this.dispatchEvent('add_room');
|
|
};
|
|
|
|
/**
|
|
* Remove the currently active room.
|
|
*/
|
|
beestat.component.floor_plan.prototype.remove_room_ = function() {
|
|
this.save_buffer();
|
|
|
|
const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
|
|
beestat.setting('visualize.floor_plan_id')
|
|
));
|
|
|
|
const self = this;
|
|
|
|
const index = this.state_.active_group.rooms.findIndex(function(room) {
|
|
return room === self.state_.active_room_entity.get_room();
|
|
});
|
|
|
|
if (this.state_.active_room_entity !== undefined) {
|
|
this.state_.active_room_entity.set_active(false);
|
|
}
|
|
if (this.state_.active_wall_entity !== undefined) {
|
|
this.state_.active_wall_entity.set_active(false);
|
|
}
|
|
if (this.state_.active_point_entity !== undefined) {
|
|
this.state_.active_point_entity.set_active(false);
|
|
}
|
|
|
|
this.state_.active_group.rooms.splice(index, 1);
|
|
|
|
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');
|
|
}
|
|
|
|
this.dispatchEvent('remove_room');
|
|
};
|
|
|
|
/**
|
|
* Clear the currently active room.
|
|
*/
|
|
beestat.component.floor_plan.prototype.clear_room_ = function() {
|
|
if (this.state_.active_room_entity !== undefined) {
|
|
this.state_.active_room_entity.set_active(false);
|
|
}
|
|
if (this.state_.active_wall_entity !== undefined) {
|
|
this.state_.active_wall_entity.set_active(false);
|
|
}
|
|
if (this.state_.active_point_entity !== undefined) {
|
|
this.state_.active_point_entity.set_active(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove the currently active point.
|
|
*/
|
|
beestat.component.floor_plan.prototype.remove_point_ = function() {
|
|
this.save_buffer();
|
|
|
|
if (this.state_.active_room_entity.get_room().points.length > 3) {
|
|
for (let i = 0; i < this.state_.active_room_entity.get_room().points.length; i++) {
|
|
if (this.state_.active_point_entity.get_point() === this.state_.active_room_entity.get_room().points[i]) {
|
|
this.state_.active_room_entity.get_room().points.splice(i, 1);
|
|
if (this.state_.active_point_entity !== undefined) {
|
|
this.state_.active_point_entity.set_active(false);
|
|
}
|
|
this.dispatchEvent('remove_point');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add a new point to the active wall.
|
|
*/
|
|
beestat.component.floor_plan.prototype.add_point_ = function() {
|
|
this.state_.active_wall_entity.add_point();
|
|
};
|
|
|
|
/**
|
|
* Set the width of this component. Also updates the view box to the
|
|
* appropriate values according to the current zoom.
|
|
*
|
|
* @param {number} width
|
|
*/
|
|
beestat.component.floor_plan.prototype.set_width = function(width) {
|
|
this.view_box_.width = this.view_box_.width * width / this.width_;
|
|
this.width_ = width;
|
|
this.svg_.setAttribute('width', width);
|
|
this.update_view_box_();
|
|
this.state_.floor_plan_width = width;
|
|
};
|
|
|
|
/**
|
|
* Zoom
|
|
*
|
|
* @param {number} scale_delta
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan.prototype.zoom_ = function(scale_delta, e) {
|
|
let local_point;
|
|
if (e === undefined) {
|
|
local_point = {
|
|
'x': this.width_ / 2,
|
|
'y': this.height_ / 2
|
|
};
|
|
} else {
|
|
local_point = this.get_local_point(e);
|
|
}
|
|
|
|
this.view_box_.x -= (local_point.x - this.view_box_.x) * (scale_delta - 1);
|
|
this.view_box_.y -= (local_point.y - this.view_box_.y) * (scale_delta - 1);
|
|
this.view_box_.width *= scale_delta;
|
|
this.view_box_.height *= scale_delta;
|
|
|
|
this.update_view_box_();
|
|
this.update_toolbar();
|
|
|
|
this.dispatchEvent('zoom');
|
|
};
|
|
|
|
/**
|
|
* Reset the view box
|
|
*/
|
|
beestat.component.floor_plan.prototype.reset_view_box_ = function() {
|
|
this.view_box_ = {
|
|
'x': 0,
|
|
'y': 0,
|
|
'width': this.width_,
|
|
'height': this.height_
|
|
};
|
|
this.update_view_box_();
|
|
};
|
|
|
|
/**
|
|
* Zoom in
|
|
*
|
|
* @param {Event} e Optional event when zooming to a specific mouse position.
|
|
*/
|
|
beestat.component.floor_plan.prototype.zoom_in_ = function(e) {
|
|
if (this.can_zoom_in_() === true) {
|
|
this.zoom_(0.9, e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Zoom out
|
|
*
|
|
* @param {Event} e Optional event when zooming to a specific mouse position.
|
|
*/
|
|
beestat.component.floor_plan.prototype.zoom_out_ = function(e) {
|
|
if (this.can_zoom_out_() === true) {
|
|
this.zoom_(1.1, e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Whether or not you can zoom in.
|
|
*
|
|
* @return {boolean} Whether or not you can zoom in.
|
|
*/
|
|
beestat.component.floor_plan.prototype.can_zoom_in_ = function() {
|
|
const min_width = this.width_ / 4;
|
|
const min_height = this.height_ / 4;
|
|
|
|
if (
|
|
this.view_box_.width * 0.9 < min_width ||
|
|
this.view_box_.height * 0.9 < min_height
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Whether or not you can zoom out
|
|
*
|
|
* @return {boolean} Whether or not you can zoom in.
|
|
*/
|
|
beestat.component.floor_plan.prototype.can_zoom_out_ = function() {
|
|
const max_width = this.width_ * 3;
|
|
const max_height = this.height_ * 3;
|
|
|
|
if (
|
|
this.view_box_.width * 1.1 > max_width ||
|
|
this.view_box_.height * 1.1 > max_height
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get the group below the specified one.
|
|
*
|
|
* @param {object} group The current group.
|
|
*
|
|
* @return {object} The group below the current group.
|
|
*/
|
|
beestat.component.floor_plan.prototype.get_group_below = function(group) {
|
|
let closest_group;
|
|
let closest_elevation_diff = Infinity;
|
|
|
|
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
|
floor_plan.data.groups.forEach(function(other_group) {
|
|
if (
|
|
other_group.elevation < group.elevation &&
|
|
group.elevation - other_group.elevation < closest_elevation_diff
|
|
) {
|
|
closest_elevation_diff = group.elevation - other_group.elevation;
|
|
closest_group = other_group;
|
|
}
|
|
});
|
|
|
|
return closest_group;
|
|
};
|
|
|
|
/**
|
|
* Center the view box on the content. Sets zoom and pan.
|
|
*/
|
|
beestat.component.floor_plan.prototype.center_content = function() {
|
|
const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);
|
|
|
|
this.reset_view_box_();
|
|
if (
|
|
bounding_box.x !== Infinity &&
|
|
bounding_box.y !== Infinity
|
|
) {
|
|
const width = (bounding_box.width) + 50;
|
|
const height = (bounding_box.height) + 50;
|
|
while (
|
|
(
|
|
this.view_box_.width < width ||
|
|
this.view_box_.height < height
|
|
) &&
|
|
this.can_zoom_out_() === true
|
|
) {
|
|
this.zoom_out_();
|
|
}
|
|
|
|
const center_x = (bounding_box.right + bounding_box.left) / 2;
|
|
const center_y = (bounding_box.bottom + bounding_box.top) / 2;
|
|
|
|
this.view_box_.x = center_x - (this.view_box_.width / 2);
|
|
this.view_box_.y = center_y - (this.view_box_.height / 2);
|
|
|
|
this.update_view_box_();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Save the current state to the undo/redo buffer.
|
|
*
|
|
* @param {boolean} clear Whether or not to allow clearing future buffer
|
|
* entries.
|
|
*/
|
|
beestat.component.floor_plan.prototype.save_buffer = function(clear = true) {
|
|
const buffer_size = 1000;
|
|
|
|
if (this.state_.buffer === undefined) {
|
|
this.state_.buffer = [];
|
|
this.state_.buffer_pointer = 0;
|
|
}
|
|
|
|
// If the buffer pointer is not at the end, clear those out.
|
|
if (
|
|
clear === true &&
|
|
this.state_.buffer_pointer !== this.state_.buffer.length + 1
|
|
) {
|
|
this.state_.buffer.length = this.state_.buffer_pointer;
|
|
}
|
|
|
|
this.state_.buffer.push({
|
|
'floor_plan': beestat.clone(beestat.cache.floor_plan[beestat.setting('visualize.floor_plan_id')]),
|
|
'active_room_entity': this.state_.active_room_entity,
|
|
'active_group_id': this.state_.active_group.group_id
|
|
});
|
|
|
|
// If the buffer gets too long shrink it.
|
|
if (this.state_.buffer.length > buffer_size) {
|
|
this.state_.buffer.shift();
|
|
}
|
|
|
|
/**
|
|
* Update the buffer pointer. It always points at the index where the next
|
|
* buffer write will happen.
|
|
*/
|
|
this.state_.buffer_pointer = this.state_.buffer.length;
|
|
|
|
this.update_toolbar();
|
|
};
|
|
|
|
/**
|
|
* Undo
|
|
*/
|
|
beestat.component.floor_plan.prototype.undo_ = function() {
|
|
if (this.can_undo_() === true) {
|
|
const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
|
|
beestat.setting('visualize.floor_plan_id')
|
|
));
|
|
|
|
/**
|
|
* When undoing, first save the buffer if the pointer is at the end to
|
|
* capture the current state then shift the buffer pointer back an extra.
|
|
*/
|
|
if (this.state_.buffer_pointer === this.state_.buffer.length) {
|
|
this.save_buffer(false);
|
|
this.state_.buffer_pointer--;
|
|
}
|
|
|
|
// Decrement buffer pointer back to the previous row.
|
|
this.state_.buffer_pointer--;
|
|
|
|
// Restore the floor plan.
|
|
beestat.cache.floor_plan[this.floor_plan_id_] =
|
|
beestat.clone(this.state_.buffer[this.state_.buffer_pointer].floor_plan);
|
|
|
|
// Restore any active room.
|
|
this.state_.active_room_entity =
|
|
this.state_.buffer[this.state_.buffer_pointer].active_room_entity;
|
|
|
|
// Restore any active group.
|
|
this.state_.active_group_id =
|
|
this.state_.buffer[this.state_.buffer_pointer].active_group_id;
|
|
|
|
// Delete data if the overall sensor set changes so it's re-fetched.
|
|
const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
|
|
beestat.setting('visualize.floor_plan_id')
|
|
));
|
|
|
|
if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
|
|
beestat.cache.delete('data.three_d__runtime_sensor');
|
|
}
|
|
|
|
this.update_toolbar();
|
|
this.dispatchEvent('undo');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Whether or not you can undo.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
beestat.component.floor_plan.prototype.can_undo_ = function() {
|
|
return this.state_.buffer_pointer > 0;
|
|
};
|
|
|
|
/**
|
|
* Redo
|
|
*/
|
|
beestat.component.floor_plan.prototype.redo_ = function() {
|
|
if (this.can_redo_() === true) {
|
|
const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
|
|
beestat.setting('visualize.floor_plan_id')
|
|
));
|
|
|
|
this.state_.buffer_pointer++;
|
|
// Restore the floor plan.
|
|
beestat.cache.floor_plan[this.floor_plan_id_] =
|
|
beestat.clone(this.state_.buffer[this.state_.buffer_pointer].floor_plan);
|
|
|
|
// Restore any active room.
|
|
this.state_.active_room_entity =
|
|
this.state_.buffer[this.state_.buffer_pointer].active_room_entity;
|
|
|
|
// Restore any active group.
|
|
this.state_.active_group_id =
|
|
this.state_.buffer[this.state_.buffer_pointer].active_group_id;
|
|
|
|
// Delete data if the overall sensor set changes so it's re-fetched.
|
|
const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
|
|
beestat.setting('visualize.floor_plan_id')
|
|
));
|
|
|
|
if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
|
|
beestat.cache.delete('data.three_d__runtime_sensor');
|
|
}
|
|
|
|
this.update_toolbar();
|
|
this.dispatchEvent('redo');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Whether or not you can redo.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
beestat.component.floor_plan.prototype.can_redo_ = function() {
|
|
return this.state_.buffer !== undefined &&
|
|
this.state_.buffer_pointer + 1 < this.state_.buffer.length;
|
|
};
|