mirror of
https://github.com/beestat/app.git
synced 2025-05-23 18:04:14 -04:00
615 lines
16 KiB
JavaScript
615 lines
16 KiB
JavaScript
/**
|
|
* Floor plan room.
|
|
*
|
|
* @param {object} room The room.
|
|
*/
|
|
beestat.component.floor_plan_entity.room = function() {
|
|
this.walls_ = [];
|
|
this.point_entities_ = [];
|
|
this.enabled_ = true;
|
|
|
|
this.snap_lines_ = {
|
|
'x': {},
|
|
'y': {}
|
|
};
|
|
beestat.component.floor_plan_entity.apply(this, arguments);
|
|
};
|
|
beestat.extend(beestat.component.floor_plan_entity.room, beestat.component.floor_plan_entity);
|
|
|
|
/**
|
|
* Decorate
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.decorate_ = function(parent) {
|
|
this.decorate_polygon_(parent);
|
|
if (this.active_ === true) {
|
|
this.decorate_walls_(parent);
|
|
this.decorate_points_(parent);
|
|
this.update_snap_points_(parent);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Draw the polygon.
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.decorate_polygon_ = function(parent) {
|
|
const self = this;
|
|
|
|
this.polygon_ = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
parent.appendChild(this.polygon_);
|
|
|
|
this.polygon_.style.strokeWidth = '2';
|
|
|
|
if (this.active_ === true) {
|
|
this.set_draggable_(true);
|
|
|
|
this.polygon_.style.cursor = 'pointer';
|
|
this.polygon_.style.fillOpacity = '0.5';
|
|
this.polygon_.style.fill = beestat.style.color.green.light;
|
|
this.polygon_.style.stroke = '#ffffff';
|
|
this.polygon_.style.filter = 'drop-shadow(3px 3px 3px #000000)';
|
|
} else if (this.enabled_ === true) {
|
|
this.polygon_.style.cursor = 'pointer';
|
|
this.polygon_.style.fillOpacity = '0.5';
|
|
this.polygon_.style.fill = beestat.style.color.blue.base;
|
|
this.polygon_.style.stroke = beestat.style.color.gray.base;
|
|
} else {
|
|
this.polygon_.style.cursor = 'default';
|
|
this.polygon_.style.fillOpacity = '0.2';
|
|
this.polygon_.style.fill = beestat.style.color.gray.base;
|
|
this.polygon_.style.stroke = beestat.style.color.gray.dark;
|
|
}
|
|
|
|
// Activate room on click if the mouse didn't move.
|
|
if (this.enabled_ === true) {
|
|
this.polygon_.addEventListener('mousedown', function(e) {
|
|
self.mousedown_mouse_ = {
|
|
'x': e.clientX,
|
|
'y': e.clientY
|
|
};
|
|
});
|
|
this.polygon_.addEventListener('mouseup', function(e) {
|
|
if (
|
|
e.clientX === self.mousedown_mouse_.x &&
|
|
e.clientY === self.mousedown_mouse_.y
|
|
) {
|
|
self.set_active(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.update_polygon_();
|
|
};
|
|
|
|
/**
|
|
* Update the points attribute of the polygon to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.update_polygon_ = function() {
|
|
const points = [];
|
|
this.room_.points.forEach(function(point) {
|
|
points.push(point.x + ',' + point.y);
|
|
});
|
|
this.polygon_.setAttribute('points', points.join(' '));
|
|
};
|
|
|
|
/**
|
|
* Add drag pointx for each point.
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.decorate_points_ = function(parent) {
|
|
const self = this;
|
|
|
|
this.room_.points.forEach(function(point) {
|
|
const point_entity = new beestat.component.floor_plan_entity.point(self.floor_plan_, self.state_)
|
|
.set_room(self)
|
|
.set_point(point)
|
|
.render(parent);
|
|
|
|
// Update when a point is moved
|
|
point_entity.addEventListener('update', function() {
|
|
self.update_polygon_();
|
|
self.update_walls_();
|
|
// self.dispatchEvent('update');
|
|
});
|
|
|
|
// When a point is done moving normalize the points
|
|
point_entity.addEventListener('drag_stop', function() {
|
|
self.normalize_points_();
|
|
self.update_points_();
|
|
self.update_walls_();
|
|
self.update_polygon_();
|
|
self.update_snap_points_();
|
|
self.dispatchEvent('update');
|
|
});
|
|
|
|
// Activate on click
|
|
point_entity.addEventListener('mousedown', function() {
|
|
point_entity.set_active(true);
|
|
});
|
|
|
|
// Add toolbar button on activate.
|
|
point_entity.addEventListener('activate', function() {
|
|
self.floor_plan_.update_toolbar();
|
|
});
|
|
|
|
// Activate the currently active point (mostly for rerenders).
|
|
if (self.state_.active_point === point) {
|
|
point_entity.set_active(true);
|
|
}
|
|
|
|
self.point_entities_.push(point_entity);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Update the points to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.update_points_ = function() {
|
|
this.point_entities_.forEach(function(point_entity) {
|
|
point_entity.update();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Add walls between each point.
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.decorate_walls_ = function(parent) {
|
|
const self = this;
|
|
|
|
this.room_.points.forEach(function(point, i) {
|
|
const i_1 = i;
|
|
const i_2 = (i + 1) % self.room_.points.length;
|
|
|
|
const wall_entity = new beestat.component.floor_plan_entity.wall(self.floor_plan_, self.state_)
|
|
.set_room(self)
|
|
.set_point_1(self.room_.points[i_1])
|
|
.set_point_2(self.room_.points[i_2])
|
|
.render(parent);
|
|
self.walls_.push(wall_entity);
|
|
|
|
wall_entity.addEventListener('update', function() {
|
|
self.update_polygon_();
|
|
self.update_points_();
|
|
self.update_walls_();
|
|
});
|
|
|
|
// Clear any active points on drag start.
|
|
wall_entity.addEventListener('drag_start', function() {
|
|
if (self.active_point_entity_ !== undefined) {
|
|
self.active_point_entity_.set_active(false);
|
|
delete self.active_point_entity_;
|
|
}
|
|
});
|
|
|
|
wall_entity.addEventListener('drag_stop', function() {
|
|
self.normalize_points_();
|
|
self.update_polygon_();
|
|
self.update_points_();
|
|
self.update_walls_();
|
|
self.update_snap_points_();
|
|
self.dispatchEvent('update');
|
|
});
|
|
|
|
wall_entity.addEventListener('add_point', function() {
|
|
self.rerender();
|
|
self.dispatchEvent('update');
|
|
});
|
|
|
|
// Activate on mousedown
|
|
wall_entity.addEventListener('mousedown', function() {
|
|
wall_entity.set_active(true);
|
|
});
|
|
|
|
// Add toolbar button on activate.
|
|
wall_entity.addEventListener('activate', function() {
|
|
self.floor_plan_.update_toolbar();
|
|
});
|
|
|
|
// Activate the currently active wall (mostly for rerenders).
|
|
if (
|
|
self.state_.active_wall_entity !== undefined &&
|
|
point === self.state_.active_wall_entity.get_point_1()
|
|
) {
|
|
wall_entity.set_active(true);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Update the walls to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.prototype.update_walls_ = function() {
|
|
this.walls_.forEach(function(wall) {
|
|
wall.update();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Make this room active or not.
|
|
*
|
|
* @param {boolean} active Whether or not the room is active.
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.room} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.set_active = function(active) {
|
|
if (active !== this.active_) {
|
|
this.active_ = active;
|
|
|
|
if (this.active_ === true) {
|
|
// Inactivate any other active room.
|
|
if (
|
|
this.state_.active_room_entity !== undefined &&
|
|
this.state_.active_room !== this.room_
|
|
) {
|
|
this.state_.active_room_entity.set_active(false);
|
|
}
|
|
|
|
this.state_.active_room = this.room_;
|
|
this.state_.active_room_entity = this;
|
|
|
|
this.dispatchEvent('activate');
|
|
this.update_snap_points_();
|
|
this.bring_to_front();
|
|
} else {
|
|
// throw 'foo';
|
|
delete this.state_.active_room;
|
|
delete this.state_.active_room_entity;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (this.rendered_ === true) {
|
|
this.rerender();
|
|
}
|
|
|
|
this.floor_plan_.update_toolbar();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Pre-generate a list of snappable x/y values.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.update_snap_points_ = function() {
|
|
const snap_x = {};
|
|
const snap_y = {};
|
|
|
|
// Snap to rooms in this group.
|
|
this.group_.rooms.forEach(function(room) {
|
|
room.points.forEach(function(point) {
|
|
snap_x[point.x + room.x] = true;
|
|
snap_y[point.y + room.y] = true;
|
|
});
|
|
});
|
|
|
|
// Snap to rooms in the group under this one.
|
|
const group_below = this.floor_plan_.get_group_below(this.group_);
|
|
if (group_below !== undefined) {
|
|
group_below.rooms.forEach(function(room) {
|
|
room.points.forEach(function(point) {
|
|
snap_x[point.x + room.x] = true;
|
|
snap_y[point.y + room.y] = true;
|
|
});
|
|
});
|
|
}
|
|
|
|
this.snap_x_ = Object.keys(snap_x).map(function(key) {
|
|
return Number(key);
|
|
});
|
|
this.snap_y_ = Object.keys(snap_y).map(function(key) {
|
|
return Number(key);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get the room.
|
|
*
|
|
* @return {object} The room.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.get_room = function() {
|
|
return this.room_;
|
|
};
|
|
|
|
/**
|
|
* Set the room.
|
|
*
|
|
* @param {object} room
|
|
*
|
|
* @return {beestat.component.floor_plan_entity} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.set_room = function(room) {
|
|
this.room_ = room;
|
|
this.x_ = room.x;
|
|
this.y_ = room.y;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set the group this room is part of. Used so the room can look at other
|
|
* points in the group for snapping.
|
|
*
|
|
* @param {object} group
|
|
*
|
|
* @return {beestat.component.floor_plan_entity} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.set_group = function(group) {
|
|
this.group_ = group;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set the x and y positions of this entity.
|
|
*
|
|
* @param {number} x The x position of this entity.
|
|
* @param {number} y The y position of this entity.
|
|
*
|
|
* @return {beestat.component.floor_plan_entity} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.set_xy = function(x, y) {
|
|
this.room_.x = Math.round(x);
|
|
this.room_.y = Math.round(y);
|
|
|
|
return beestat.component.floor_plan_entity.prototype.set_xy.apply(this, arguments);
|
|
};
|
|
|
|
/**
|
|
* Normalize the points so that they always fill the top and left. This
|
|
* prevents weird stuff like a polygon existing at 0,0 but the points all
|
|
* being shifted really far off which causes other issues.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.normalize_points_ = function() {
|
|
const x_values = [];
|
|
const y_values = [];
|
|
|
|
this.room_.points.forEach(function(point) {
|
|
x_values.push(point.x);
|
|
y_values.push(point.y);
|
|
});
|
|
|
|
const min_x = Math.min.apply(null, x_values);
|
|
const min_y = Math.min.apply(null, y_values);
|
|
|
|
this.room_.points.forEach(function(point) {
|
|
point.x -= min_x;
|
|
point.y -= min_y;
|
|
});
|
|
|
|
this.set_xy(this.x_ + min_x, this.y_ + min_y);
|
|
};
|
|
|
|
/**
|
|
* Set an appropriate drag_start_entity_ on mousedown.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.after_mousedown_handler_ = function() {
|
|
this.drag_start_entity_ = {
|
|
'x': this.x_,
|
|
'y': this.y_
|
|
};
|
|
};
|
|
|
|
/**
|
|
* point dragging the room around. Snaps to X and Y of other points.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.after_mousemove_handler_ = function(e) {
|
|
const self = this;
|
|
|
|
let desired_x = this.drag_start_entity_.x + ((e.clientX - this.drag_start_mouse_.x) * this.floor_plan_.get_scale());
|
|
let desired_y = this.drag_start_entity_.y + ((e.clientY - this.drag_start_mouse_.y) * this.floor_plan_.get_scale());
|
|
|
|
// Snap
|
|
if (this.state_.snapping === true) {
|
|
const snap_distance = 6;
|
|
this.room_.points.forEach(function(point) {
|
|
const point_x = point.x + desired_x;
|
|
const point_y = point.y + desired_y;
|
|
|
|
// Snap X
|
|
for (let i = 0; i < self.snap_x_.length; i++) {
|
|
const snap_x = self.snap_x_[i];
|
|
const distance = Math.abs(snap_x - point_x);
|
|
if (distance <= snap_distance) {
|
|
desired_x = snap_x - point.x;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Snap Y
|
|
for (let i = 0; i < self.snap_y_.length; i++) {
|
|
const snap_y = self.snap_y_[i];
|
|
const distance = Math.abs(snap_y - point_y);
|
|
if (distance <= snap_distance) {
|
|
desired_y = snap_y - point.y;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.update_snap_lines_();
|
|
} else {
|
|
this.clear_snap_lines_();
|
|
}
|
|
|
|
this.set_xy(
|
|
desired_x,
|
|
desired_y
|
|
);
|
|
};
|
|
|
|
/**
|
|
* What happens when you stop moving the room.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.after_mouseup_handler_ = function() {
|
|
if (this.dragged_ === true) {
|
|
this.clear_snap_lines_();
|
|
this.update_snap_points_();
|
|
this.dispatchEvent('update');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update snap lines to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.update_snap_lines_ = function() {
|
|
const self = this;
|
|
|
|
let current_snap_x = {};
|
|
this.room_.points.forEach(function(point) {
|
|
current_snap_x[point.x + self.room_.x] = true;
|
|
});
|
|
|
|
// Remove any snap lines that no longer exist.
|
|
for (let x in this.snap_lines_.x) {
|
|
if (current_snap_x[x] === undefined) {
|
|
this.snap_lines_.x[x].parentNode.removeChild(this.snap_lines_.x[x]);
|
|
delete this.snap_lines_.x[x];
|
|
}
|
|
}
|
|
|
|
current_snap_x = Object.keys(current_snap_x).map(function(key) {
|
|
return Number(key);
|
|
});
|
|
|
|
const intersected_snap_x = this.snap_x_.filter(function(x) {
|
|
return current_snap_x.includes(x) === true;
|
|
});
|
|
|
|
// Add any new snap lines.
|
|
intersected_snap_x.forEach(function(x) {
|
|
if (self.snap_lines_.x[x] === undefined) {
|
|
self.snap_lines_.x[x] = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
self.snap_lines_.x[x].style.strokeDasharray = '7, 3';
|
|
self.snap_lines_.x[x].style.stroke = beestat.style.color.yellow.base;
|
|
self.snap_lines_.x[x].setAttribute('x1', x);
|
|
self.snap_lines_.x[x].setAttribute('x2', x);
|
|
self.snap_lines_.x[x].setAttribute('y1', self.floor_plan_.get_grid_pixels() / -2);
|
|
self.snap_lines_.x[x].setAttribute('y2', self.floor_plan_.get_grid_pixels() / 2);
|
|
self.floor_plan_.get_g().appendChild(self.snap_lines_.x[x]);
|
|
}
|
|
});
|
|
|
|
let current_snap_y = {};
|
|
this.room_.points.forEach(function(point) {
|
|
current_snap_y[point.y + self.room_.y] = true;
|
|
});
|
|
|
|
// Remove any snap lines that no longer exist.
|
|
for (let y in this.snap_lines_.y) {
|
|
if (current_snap_y[y] === undefined) {
|
|
this.snap_lines_.y[y].parentNode.removeChild(this.snap_lines_.y[y]);
|
|
delete this.snap_lines_.y[y];
|
|
}
|
|
}
|
|
|
|
current_snap_y = Object.keys(current_snap_y).map(function(key) {
|
|
return Number(key);
|
|
});
|
|
|
|
const intersected_snap_y = this.snap_y_.filter(function(y) {
|
|
return current_snap_y.includes(y) === true;
|
|
});
|
|
|
|
// Add any new snap lines.
|
|
intersected_snap_y.forEach(function(y) {
|
|
if (self.snap_lines_.y[y] === undefined) {
|
|
self.snap_lines_.y[y] = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
self.snap_lines_.y[y].style.strokeDasharray = '7, 3';
|
|
self.snap_lines_.y[y].style.stroke = beestat.style.color.yellow.base;
|
|
self.snap_lines_.y[y].setAttribute('y1', y);
|
|
self.snap_lines_.y[y].setAttribute('y2', y);
|
|
self.snap_lines_.y[y].setAttribute('x1', self.floor_plan_.get_grid_pixels() / -2);
|
|
self.snap_lines_.y[y].setAttribute('x2', self.floor_plan_.get_grid_pixels() / 2);
|
|
self.floor_plan_.get_g().appendChild(self.snap_lines_.y[y]);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Clear all existing snap lines.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.clear_snap_lines_ = function() {
|
|
for (var x in this.snap_lines_.x) {
|
|
this.snap_lines_.x[x].parentNode.removeChild(this.snap_lines_.x[x]);
|
|
delete this.snap_lines_.x[x];
|
|
}
|
|
for (var y in this.snap_lines_.y) {
|
|
this.snap_lines_.y[y].parentNode.removeChild(this.snap_lines_.y[y]);
|
|
delete this.snap_lines_.y[y];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Snap X
|
|
*
|
|
* @return {array} Snap X
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.get_snap_x = function() {
|
|
return this.snap_x_;
|
|
};
|
|
|
|
/**
|
|
* Get Snap Y
|
|
*
|
|
* @return {array} Snap Y
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.get_snap_y = function() {
|
|
return this.snap_y_;
|
|
};
|
|
|
|
/**
|
|
* Set enabled (different than active)
|
|
*
|
|
* @param {boolean} enabled
|
|
*
|
|
* @return {beestat.component.floor_plan_entity} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.room.prototype.set_enabled = function(enabled) {
|
|
this.enabled_ = enabled;
|
|
|
|
return this;
|
|
};
|
|
|
|
// Keeping this around as I may use it.
|
|
// https://stackoverflow.com/a/16283349
|
|
/*beestat.component.floor_plan_entity.room.prototype.get_centroid_ = function () {
|
|
var x = 0,
|
|
y = 0,
|
|
i,
|
|
j,
|
|
f,
|
|
point1,
|
|
point2;
|
|
|
|
for (i = 0, j = this.room_.points.length - 1; i < this.room_.points.length; j=i,i++) {
|
|
point1 = this.room_.points[i];
|
|
point2 = this.room_.points[j];
|
|
f = point1.x * point2.y - point2.x * point1.y;
|
|
x += (point1.x + point2.x) * f;
|
|
y += (point1.y + point2.y) * f;
|
|
}
|
|
|
|
const area = Math.abs(
|
|
ClipperLib.Clipper.Area(this.room_.points)
|
|
);
|
|
|
|
f = area * 6;
|
|
|
|
return {'x': x / f, 'y': y / f};
|
|
};*/
|