gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
773 lines (725 loc) • 38.4 kB
JavaScript
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANN UI CLASS
* Handle GUI manipulation (showing/changing data, populating windows, etc) for Floorplanner.html
*/
/*
* Floorplan UI Constructor
* @param {Floorplan} floorplan A reference to a valid instance of Floorplan
* @param {String} name The name of this FloorplanUI instance known to the DOM
* @param {String} The name of this UI's floorplan known to the DOM
* @param {Object} state A JSON object with string ids for UI HTML elements. Format is as follows:
menuButtons: {
selectionInfoWindowButtonId:
palettesWindowButtonId:
overviewWindowButtonId:
optionsWindowButtonId:
statisticsWindowButtonId:
}
windows: {
diagramHelpDiv: {
id:
}
selectionInfoWindow: {
id:
textDivId:
handleId:
colorPickerId:
heightLabelId:
heightInputId:
widthInputId:
nodeGroupInfoId:
nameInputId:
notesTextareaId:
}
palettesWindow:{
id:
furnitureSearchInputId:
furniturePaletteId:
}
overviewWindow: {
id:
}
optionsWindow: {
id:
gridSizeInputId:
unitsConversionFactorInputId:
unitsFormId:
unitsFormName:
checkboxes: {
showGridCheckboxId:
gridSnapCheckboxId:
wallGuidelinesCheckboxId:
wallLengthsCheckboxId:
wallAnglesCheckboxId:
smallWallAnglesCheckboxId:
}
}
statisticsWindow: {
id:
textDivId:
numsTableId:
totalsTableId:
}
}
scaleDisplayId:
setBehaviorClass:
wallThicknessInputId:
wallThicknessBoxId:
unitsBoxId:
unitsInputId:
*/
function FloorplanUI(floorplan, name, floorplanName, state) {
this._floorplan = floorplan;
this._name = name;
this._floorplanName = floorplanName;
this._state = state;
this._furnitureNodeData = null; // used for searchFurniture function. set only once
this.floorplan.floorplanUI = this;
}
// Get Floorplan associated with this UI
Object.defineProperty(FloorplanUI.prototype, "floorplan", {
get: function () { return this._floorplan; }
});
// Get state object containing many ids of various UI elements
Object.defineProperty(FloorplanUI.prototype, "state", {
get: function () { return this._state; }
});
// Get name of this FloorplanUI instance known to the DOM
Object.defineProperty(FloorplanUI.prototype, "name", {
get: function () { return this._name; }
});
// Get name of the Floorplan associated with this FloorplanUI instance known to the DOM
Object.defineProperty(FloorplanUI.prototype, "floorplanName", {
get: function () { return this._floorplanName; }
});
Object.defineProperty(FloorplanUI.prototype, "furnitureData", {
get: function () { return this._furnitureData; },
set: function (val) { this._furnitureData = val; }
});
/*
* UI manipulation:
* Hide/Show Element, Adjust Scale, ChangeGridSize, Change Units Conversion Factor
* Search Furniture, Checkbox Changed, Change Units, Set Behavior, Update UI
*/
/*
* Hide or show specific help/windows (used mainly with hotkeys)
* @param {String} id The ID of the window to show / hide
*/
FloorplanUI.prototype.hideShow = function(id) {
var element = document.getElementById(id); var str;
var windows = this.state.windows;
switch (id) {
case windows.diagramHelpDiv.id: str = 'Diagram Help'; char = 'H'; break;
case windows.selectionInfoWindow.id: str = 'Selection Help'; char = 'I'; break;
case windows.overviewWindow.id: str = 'Overview'; char = 'E'; break;
case windows.optionsWindow.id: str = 'Options'; char = 'B'; break;
case windows.statisticsWindow.id: str = 'Statistics'; char = 'G'; break;
case windows.palettesWindow.id: str = 'Palettes'; char = 'P'; {
furniturePalette.layoutDiagram(true);
wallPartsPalette.layoutDiagram(true);
break;
}
}
var button = document.getElementById(id + 'Button');
element.style.visibility = element.style.visibility === "visible" ? "hidden" : "visible";
var verb = element.style.visibility === "visible" ? "Hide " : "Show ";
if (button) button.innerHTML = verb + str + "<p class='shortcut'> (Ctrl + " + char + " )</p>";
}
/*
* Set text under Diagram to suggest most common functions user could perform
* @param {String} str The text to display in the Diagram Help div
*/
FloorplanUI.prototype.setDiagramHelper = function(str) {
var helper = document.getElementById(this.state.windows.diagramHelpDiv.id);
if (helper) helper.innerHTML = '<p>' + str + '</p>';
}
/*
* Increase / decrease diagram scale to the nearest 10%
* @param {String} sign Accepted values are "+" and "-"
*/
FloorplanUI.prototype.adjustScale = function(sign) {
var floorplan = this.floorplan;
var el = document.getElementById(this.state.scaleDisplayId);
floorplan.startTransaction('Change Scale');
switch (sign) {
case '-': floorplan.scale -= .1; break;
case '+': floorplan.scale += .1; break;
}
floorplan.scale = parseFloat((Math.round(floorplan.scale / .1) * .1).toFixed(2));
var scale = (floorplan.scale * 100).toFixed(2);
el.innerHTML = 'Scale: ' + scale + '%';
floorplan.commitTransaction('Change Scale');
}
// Change edge length of the grid based on input
FloorplanUI.prototype.changeGridSize = function () {
var floorplan = this.floorplan;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change grid size");
var el = document.getElementById(this.state.windows.optionsWindow.gridSizeInputId); var input;
if (!isNaN(el.value) && el.value != null && el.value != '' && el.value != undefined && el.value > 1) input = parseFloat(el.value);
else {
el.value = floorplan.convertPixelsToUnits(10); // if bad input given, revert to 20cm (10px) or unit equivalent
input = parseFloat(el.value);
}
input = floorplan.convertUnitsToPixels(input);
floorplan.grid.gridCellSize = new go.Size(input, input);
floorplan.toolManager.draggingTool.gridCellSize = new go.Size(input, input);
floorplan.model.setDataProperty(floorplan.model.modelData, "gridSize", input);
floorplan.commitTransaction("change grid size");
floorplan.skipsUndoManager = false;
}
FloorplanUI.prototype.changeUnitsConversionFactor = function () {
var floorplan = this.floorplan;
var val = document.getElementById(this.state.windows.optionsWindow.unitsConversionFactorInputId).value;
if (isNaN(val) || !val || val == undefined) return;
floorplan.skipsUndoManager = true;
floorplan.model.set(floorplan.model.modelData, "unitsConversionFactor", val);
floorplan.skipsUndoManager = false;
}
// Search through all elements in the furniture palette (useful for a palette with many furniture nodes)
FloorplanUI.prototype.searchFurniture = function () {
var ui = this;
var floorplan = this.floorplan;
var furniturePaletteId = ui.state.windows.palettesWindow.furniturePaletteId;
var str = document.getElementById(ui.state.windows.palettesWindow.furnitureSearchInputId).value;
var furniturePalette = window.furniturePalette;
/*var furniturePalette = null;
for (var i = 0; i < floorplan.palettes.length; i++) {
var palette = floorplan.palettes[i];
if (palette.div.id == furniturePaletteId) {
furniturePalette = floorplan.palettes[i];
}
}*/
if (ui.furnitureData == null) ui.furnitureData = furniturePalette.model.nodeDataArray;
var items = furniturePalette.model.nodeDataArray.slice();
if (str !== null && str !== undefined && str !== "") {
for (var i = 0; i < items.length; i += 0) {
var item = items[i];
if (item.type.toLowerCase().indexOf(str.toLowerCase()) === -1) {
items.splice(i, 1);
}
else i++;
}
furniturePalette.model.nodeDataArray = items;
}
else furniturePalette.model.nodeDataArray = ui.furnitureData;
furniturePalette.updateAllRelationshipsFromData();
}
/*
* Change the "checked" value of checkboxes in the Options Menu, and to have those changes reflected in app behavior / model data
* @param {String} id The ID of the changed checkbox
*/
FloorplanUI.prototype.checkboxChanged = function (id) {
var floorplan = this.floorplan;
var checkboxes = this.state.windows.optionsWindow.checkboxes;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change preference");
var element = document.getElementById(id);
switch (id) {
case checkboxes.showGridCheckboxId: {
floorplan.grid.visible = element.checked;
floorplan.model.modelData.preferences.showGrid = element.checked;
break;
}
case checkboxes.gridSnapCheckboxId: {
floorplan.toolManager.draggingTool.isGridSnapEnabled = element.checked;
floorplan.model.modelData.preferences.gridSnap = element.checked;
break;
}
case checkboxes.wallGuidelinesCheckboxId: floorplan.model.modelData.preferences.showWallGuidelines = element.checked; break;
case checkboxes.wallLengthsCheckboxId: floorplan.model.modelData.preferences.showWallLengths = element.checked; floorplan.updateWallDimensions(); break;
case checkboxes.wallAnglesCheckboxId: floorplan.model.modelData.preferences.showWallAngles = element.checked; floorplan.updateWallAngles(); break;
case checkboxes.smallWallAnglesCheckboxId: floorplan.model.modelData.preferences.showOnlySmallWallAngles = element.checked; floorplan.updateWallAngles(); break;
}
floorplan.commitTransaction("change preference");
floorplan.skipsUndoManager = false;
}
// Adjust units based on the selected radio button in the Options Menu
FloorplanUI.prototype.changeUnits = function() {
var floorplan = this.floorplan;
floorplan.startTransaction("set units");
var prevUnits = floorplan.model.modelData.units;
var radios = document.forms[this.state.windows.optionsWindow.unitsFormId].elements[this.state.windows.optionsWindow.unitsFormName];
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
floorplan.model.setDataProperty(floorplan.model.modelData, "units", radios[i].id);
}
}
var units = floorplan.model.modelData.units;
switch (units) {
case 'centimeters': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'cm'); break;
case 'meters': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'm'); break;
case 'feet': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'ft'); break;
case 'inches': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'in'); break;
}
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
// update all units boxes with new units
var unitAbbrevInputs = document.getElementsByClassName(this.state.unitsBoxClass);
for (var i = 0; i < unitAbbrevInputs.length; i++) {
unitAbbrevInputs[i].value = unitsAbbreviation;
}
var unitInputs = document.getElementsByClassName(this.state.unitsInputClass);
for (var i = 0; i < unitInputs.length; i++) {
var input = unitInputs[i];
floorplan.model.setDataProperty(floorplan.model.modelData, "units", prevUnits);
var value = floorplan.convertUnitsToPixels(input.value);
floorplan.model.setDataProperty(floorplan.model.modelData, "units", units)
value = floorplan.convertPixelsToUnits(value);
input.value = value;
}
if (floorplan.selection.count === 1) this.setSelectionInfo(floorplan.selection.first()); // reload node info measurements according to new units
floorplan.commitTransaction("set units");
}
/*
* Set current tool (selecting/dragging or wallbuilding/reshaping)
* @param {String} string Informs what behavior to switch to. Accepted values: "dragging", "wallbuilding"
*/
FloorplanUI.prototype.setBehavior = function (string) {
var floorplan = this.floorplan;
var ui = this;
var wallBuildingTool = floorplan.toolManager.mouseDownTools.elt(0);
var wallReshapingTool = floorplan.toolManager.mouseDownTools.elt(3);
// style the current tool HTML button accordingly
var elements = document.getElementsByClassName(this.state.setBehaviorClass);
for (var i = 0; i < elements.length; i++) {
var el = elements[i];
if (el.id === string + "Button") el.style.backgroundColor = '#4b545f';
else el.style.backgroundColor = '#bbbbbb';
}
var wallThicknessBox = document.getElementById(this.state.wallThicknessBoxId)
if (string === 'wallBuilding') {
wallBuildingTool.isEnabled = true;
wallReshapingTool.isEnabled = false;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change wallThickness");
// create walls with wallThickness in input box
floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', parseFloat(document.getElementById(ui.state.wallThicknessInputId).value));
var wallThickness = floorplan.model.modelData.wallThickness;
if (isNaN(wallThickness)) floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', 5);
else {
var width = floorplan.convertUnitsToPixels(wallThickness);
floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', width);
}
floorplan.commitTransaction("change wallThickness");
floorplan.skipsUndoManager = false;
wallThicknessBox.style.visibility = 'visible';
wallThicknessBox.style.display = 'inline-block';
ui.setDiagramHelper("Click and drag on the diagram to draw a wall (hold SHIFT for 45 degree angles)");
}
if (string === 'dragging') {
wallBuildingTool.isEnabled = false;
wallReshapingTool.isEnabled = true;
wallThicknessBox.style.visibility = 'hidden';
wallThicknessBox.style.display = 'none';
}
// clear resize adornments on walls/windows, if there are any
floorplan.nodes.iterator.each(function (n) { n.clearAdornments(); })
floorplan.clearSelection();
}
/*
* Populating UI Windows from Floorplan data:
* Update UI, Update Statistics, Fill Rows With Nodes, Set Selection Info, Set Color, Set Height, Set Width, Apply Selection Changes
*/
// Update the UI properly in accordance with model.modelData (called only when a new floorplan is loaded or created)
FloorplanUI.prototype.updateUI = function () {
var floorplan = this.floorplan;
var modelData = floorplan.model.modelData;
var checkboxes = this.state.windows.optionsWindow.checkboxes;
if (floorplan.floorplanUI) floorplan.floorplanUI.changeUnits();
document.getElementById(this.state.wallThicknessInputId).value = floorplan.convertPixelsToUnits(modelData.wallThickness);
// update options GUI based on floorplan.model.modelData.preferences
var preferences = modelData.preferences;
document.getElementById(checkboxes.showGridCheckboxId).checked = preferences.showGrid;
document.getElementById(checkboxes.gridSnapCheckboxId).checked = preferences.gridSnap;
document.getElementById(checkboxes.wallGuidelinesCheckboxId).checked = preferences.showWallGuidelines;
document.getElementById(checkboxes.wallLengthsCheckboxId).checked = preferences.showWallLengths;
document.getElementById(checkboxes.wallAnglesCheckboxId).checked = preferences.showWallAngles;
document.getElementById(checkboxes.smallWallAnglesCheckboxId).checked = preferences.showOnlySmallWallAngles;
}
// Update all statistics in Statistics Window - called when a Floorplan's model is changed
FloorplanUI.prototype.updateStatistics = function () {
var floorplan = this.floorplan;
var statsWindow = this.state.windows.statisticsWindow;
var element = document.getElementById(statsWindow.textDivId);
if (element) {
element.innerHTML = "<div class='row'><div class='col-2' style='height: 165px; overflow: auto;'> Item Types <table id='" + statsWindow.numsTableId + "'></table></div><div class='col-2'> Totals <table id='totalsTable'></table></div></div>";
// fill Item Types table with node type/count of all nodes in diagram
var numsTable = document.getElementById(statsWindow.numsTableId);
// get all palette nodes associated with this Floorplan
var palettes = floorplan.palettes;
var allPaletteNodes = [];
for (var i = 0; i < palettes.length; i++) {
allPaletteNodes = allPaletteNodes.concat(palettes[i].model.nodeDataArray);
}
for (var i = 0; i < allPaletteNodes.length; i++) {
var type = allPaletteNodes[i].type;
var num = floorplan.findNodesByExample({ type: type }).count;
if (num > 0) // only display data for nodes that exist on the diagram
numsTable.innerHTML += "<tr class='data'> <td style='float: left;'>" + type + "</td> <td style='float: right;'> " + num + "</td></tr>";
}
// fill Totals table with lengths of all walls
totalsTable = document.getElementById('totalsTable');
var walls = floorplan.findNodesByExample({ category: "WallGroup" });
var totalLength = 0;
walls.iterator.each(function (wall) {
var wallLength = Math.sqrt(wall.data.startpoint.distanceSquaredPoint(wall.data.endpoint));
totalLength += wallLength;
});
totalLength = floorplan.convertPixelsToUnits(totalLength).toFixed(2);
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
totalsTable.innerHTML += "<tr class='data'><td style='float: left;'>Wall Lengths</td><td style='float: right;'>" + totalLength + unitsAbbreviation + "</td></tr>";
}
}
/* Helper function for setSelectionInfo(); displays all nodes in a given set in a given 1 or 2 rows in a given HTML element
* @param {Iterable | Array} iterator A iterable collection of nodes to display in rows
* @param {String} element The ID of the HTML element to fill with this data
* @param {String} selectedKey The key of the currently selected node -- this node's name will be styled differently in the rows
*/
// TODO some repetitive code here
FloorplanUI.prototype.fillRowsWithNodes = function(iterator, element, selectedKey) {
var floorplan = this.floorplan;
var ui = this;
var arr = [];
if (iterator.constructor !== Array) iterator.each(function (p) { arr.push(p); });
else arr = iterator;
// helper
function makeOnClick(key) {
return ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + "'" + key + "'" + '))';
}
for (var i = 0; i < arr.length; i += 2) {
if (arr[i].data === null) { this.setSelectionInfo('Nothing selected'); return; }
var key1 = arr[i].data.key; // keys used to locate the node if clicked on...
var name1 = (arr[i].data.caption !== "MultiPurposeNode") ? arr[i].data.caption : arr[i].data.text; // ... names are editable, so users can distinguish between nodes
// if there are two nodes for this row...
if (arr[i + 1] != undefined && arr[i + 1] != null) {
var key2 = arr[i + 1].data.key;
var name2 = (arr[i + 1].data.caption !== "MultiPurposeNode") ? arr[i + 1].data.caption : arr[i + 1].data.text;
// if there's a non-null selectedKey, highlight the selected node in the list
if (key1 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
else if (key2 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
else element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable"' + 'onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable"' + 'onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
}
// if there's only one node for this row...
else {
if (key1 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key1) + '">' + name1 + '</p></div></div>';
else element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key1) + '">' + name1 + '</p></div></div>';
}
}
}
/*
* Displays dynamic, editable info about selection in Selection Info window (height/length, width, name, group info, color, etc.)
* @param {Node | String} node Can be: Reference to Node / Group, go.Node/go.Group key, "Selection" (indicating multi-selection), "Nothing selected"
*/
FloorplanUI.prototype.setSelectionInfo = function(node) {
var floorplan = this.floorplan;
var ui = this;
if (node instanceof go.GraphObject) node = node.part;
var selectionInfoWindow = this.state.windows.selectionInfoWindow;
var element = document.getElementById(selectionInfoWindow.textDivId);
var state = this.state;
var infoWindow = document.getElementById(selectionInfoWindow.id);
if (element === null || infoWindow === null) return;
if (node === 'Nothing selected' || node.layer === null || node === null) { element.innerHTML = '<p>' + node + '</p>'; return; }
// if there are multiple nodes selected, show all their names, allowing user to click on the node they want
if (node === 'Selection: ') {
var selectionIterator = floorplan.selection.iterator; var arr = [];
element.innerHTML = '<p id="name"> Selection (' + selectionIterator.count + ' items selected): </p>';
this.fillRowsWithNodes(selectionIterator, element, null);
infoWindow.style.height = document.getElementById(selectionInfoWindow.textDivId).offsetHeight + document.getElementById(selectionInfoWindow.handleId).offsetHeight + 5 + 'px';
return;
}
// TODO clean this to be usable by a more general template scheme
// if we have one node selected, gather pertinent information for that node....
floorplan.select(node);
var name = ''; var length; var width; var nodeGroupCount = 0; var notes = node.data.notes;
// get node name
if (node.category === 'MultiPurposeNode') name = node.data.text;
else name = node.data.caption;
// get node height / width / length (dependent on node category)
// Wall Groups
if (node.category === 'WallGroup') {
length = floorplan.convertPixelsToUnits(Math.sqrt(node.data.startpoint.distanceSquared(node.data.endpoint.x, node.data.endpoint.y))).toFixed(2); // wall length
thickness = floorplan.convertPixelsToUnits(node.data.thickness).toFixed(2); // wall thickness
}
// Wall Parts (i.e. Door / Window Nodes)
else if (node.category === 'DoorNode' || node.category === "WindowNode") {
length = floorplan.convertPixelsToUnits(node.data.length).toFixed(2);
}
// Generic Groups
else if (node.data.isGroup && node.category !== "WallGroup") {
height = floorplan.convertPixelsToUnits(node.actualBounds.height).toFixed(2);
width = floorplan.convertPixelsToUnits(node.actualBounds.width).toFixed(2);
}
// Furniture Nodes
else {
height = floorplan.convertPixelsToUnits(node.data.height).toFixed(2);
width = floorplan.convertPixelsToUnits(node.data.width).toFixed(2);
}
// get node group info
if (node.containingGroup != null && node.containingGroup != undefined) {
var nodeGroupParts = node.containingGroup.memberParts;
nodeGroupCount = nodeGroupParts.count;
}
if (node.data.isGroup) {
var nodeGroupParts = node.memberParts;
nodeGroupCount = nodeGroupParts.count;
}
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
// display information in the selection info window
element.innerHTML = '<p id="name" >Name: ' + '<input id="nameInput" class="nameNotesInput" value="' + name + '"/>' + '</p>'; // name
element.innerHTML += "<p>Notes: <textarea id='"+ selectionInfoWindow.notesTextareaId +"' class='nameNotesInput' >" + notes + "</textarea></p>"; // notes
// display color as a color picker element (furniture nodes only)
if (!node.data.isGroup && node.data.category !== "DoorNode" && node.data.category !== "WindowNode") {
element.innerHTML += '<p>Color: <input type="color" id="'+ selectionInfoWindow.colorPickerId +'" value="' + node.data.color + '" name="selectionColor"></input></p>';
}
// display ONLY "Length" input (Door / Window Nodes only) (this is still technically the "width input" as far as IDs are concerned)
if (node.category === "DoorNode" || node.category === "WindowNode") element.innerHTML += '<div class="row"><p id="'+ selectionInfoWindow.widthLabelId +'" class="data">Length: <br/><input id = "' + selectionInfoWindow.widthInputId + '" class = "dimensionsInput" name = "width" value = "' + length + '"/>'
+ '<input id="widthUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p>';
// for walls "width" is displayed as "length", "height" is displayed as "Thickness"
else if (node.category === 'WallGroup') {
element.innerHTML += '<div class="row"><div class="col-2"><p id="' + selectionInfoWindow.heightLabelId + '" class="data">Thickness: <br/><input id ="' + selectionInfoWindow.heightInputId + '" class = "dimensionsInput" name = "height" value = "' + thickness
+ '"/><input id="heightUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p> ' + '</div><div class="col-2"><p class="data">Length: <br/><input id="' + selectionInfoWindow.widthInputId + '" class="dimensionsInput" value = "'
+ length + '"/><input id="widthUnits" class="' + state.unitsBoxClass + '" value="' + unitsAbbreviation + '" disabled/></p>' + '</p></div></div>';
}
// display editable properties height and width (non Door / Window nodes)
else element.innerHTML += '<div class="row"><div class="col-2"><p id="' + selectionInfoWindow.heightLabelId + '" class="data">Height: <br/><input id ="' + selectionInfoWindow.heightInputId + '" class = "dimensionsInput" name = "height" value = "' + height
+ '"/><input id="heightUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p> ' + '</div><div class="col-2"><p class="data">Width: <br/><input id="' + selectionInfoWindow.widthInputId + '" class="dimensionsInput" value = "'
+ width + '"/><input id="widthUnits" class="' + state.unitsBoxClass + '" value="' + unitsAbbreviation + '" disabled/></p>' + '</p></div></div>';
// do not allow height or width adjustment for group info
if (node.data.isGroup && node.category !== "WallGroup") {
document.getElementById(selectionInfoWindow.heightInputId).disabled = true;
document.getElementById(selectionInfoWindow.widthInputId).disabled = true;
}
// "Apply Changes" button
element.innerHTML += '<div class="row"> <button id="applySelectionChanges" onClick="' + this.name + '.applySelectionChanges()">Apply Changes</button></div>';
// display group info for standard groups, wallParts for walls
var groupName = null; var groupKey = null; var selectedKey = "";
if (node.data.isGroup === true) {
groupName = node.data.caption;
groupKey = node.data.key;
selectedKey = "selectedKey"; // the 'group' node is selected; make it blue to show this
}
if (node.containingGroup !== null) {
groupName = node.containingGroup.data.caption;
groupKey = node.containingGroup.data.key;
}
if (groupName !== null) {
groupKey = "'" + groupKey + "'";
element.innerHTML += '<div class="row data" id="' + selectionInfoWindow.nodeGroupInfoId +'"> <span class="clickable ' + selectedKey + '" onclick="' + ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + groupKey + '))">' +
groupName + '</span> Info (' + nodeGroupCount + ' member(s) in <span class="clickable ' + selectedKey + '" onclick="' + ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + groupKey + '))">' + groupName + '</span>) </div>';
if (nodeGroupCount != 0) ui.fillRowsWithNodes(nodeGroupParts, document.getElementById(selectionInfoWindow.nodeGroupInfoId), node.data.key);
}
var nameInput = document.getElementById(selectionInfoWindow.nameInputId);
if (!floorplan.isReadOnly) {
// dynamically adjust name of selected node based on user input
nameInput.addEventListener('input', function (e) {
var value = nameInput.value;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("rename node");
if (value === null || value === "" || value === undefined) { floorplan.commitTransaction("rename node"); return; }
floorplan.model.setDataProperty(node.data, "caption", value);
floorplan.model.setDataProperty(node.data, "text", value); // if node is a multi purpose node, update the text on it
floorplan.commitTransaction("rename node");
floorplan.skipsUndoManager = false;
});
// dynamically adjust notes of selected node based on user input
var notesTextarea = document.getElementById(selectionInfoWindow.notesTextareaId);
notesTextarea.addEventListener('input', function (e) {
var value = notesTextarea.value;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("edit node notes");
if (value === null || value === undefined) return;
floorplan.model.setDataProperty(node.data, "notes", value);
floorplan.commitTransaction("edit node notes");
floorplan.skipsUndoManager = false;
});
infoWindow.style.height = document.getElementById(selectionInfoWindow.textDivId).offsetHeight + document.getElementById(selectionInfoWindow.handleId).offsetHeight + 5 + 'px';
}
}
// Triggered by "Apply Changes"; set model data for fill color of the current selection
FloorplanUI.prototype.setColor = function () {
var floorplan = this.floorplan;
var node = floorplan.selection.first();
var colorPicker = document.getElementById(this.state.windows.selectionInfoWindow.colorPickerId);
if (colorPicker !== null) {
floorplan.startTransaction("recolor node");
floorplan.model.setDataProperty(node.data, "color", colorPicker.value);
floorplan.model.setDataProperty(node.data, "stroke", invertColor(colorPicker.value))
floorplan.commitTransaction("recolor node");
}
}
// Triggered by "Apply Changes"; set model data for height of the currently selected node (also handles door length for doors, wall length for walls)
FloorplanUI.prototype.setHeight = function () {
var heightInput = document.getElementById(this.state.windows.selectionInfoWindow.heightInputId);
if (heightInput) {
var floorplan = this.floorplan;
var ui = this;
var node = floorplan.selection.first();
var value = parseFloat(floorplan.convertUnitsToPixels(heightInput.value));
if (isNaN(value)) {
alert("Please enter a valid number");
ui.setSelectionInfo(node, floorplan);
return;
}
floorplan.skipsUndoManager = true;
floorplan.startTransaction("resize node");
if (!floorplan.isReadOnly) {
// Case: Furniture Nodes and Window Nodes; basic height adjustment
if (node.category !== 'WallGroup' && node.category !== 'DoorNode') {
floorplan.model.setDataProperty(node.data, "height", value);
}
// Case: Wall Groups; set wall's data.strokeWidth
else if (node.category === 'WallGroup') {
floorplan.model.setDataProperty(node.data, "thickness", value);
node.memberParts.iterator.each(function (part) {
if (part.category === 'DoorNode') floorplan.model.setDataProperty(part.data, "doorOpeningHeight", value);
if (part.category === 'WindowNode') floorplan.model.setDataProperty(part.data, "height", value);
});
}
// Note: Door Nodes are purposefully unaccounted for, as width (length) adjustment (in setWidth()) also adjusts node height
}
floorplan.commitTransaction("resize node");
floorplan.updateWallDimensions(floorplan);
floorplan.skipsUndoManager = false;
}
}
// Triggered by "Apply Changes"; set model data for width of the currently selected node
FloorplanUI.prototype.setWidth = function() {
var floorplan = this.floorplan;
var ui = this;
var node = floorplan.selection.first();
var widthInput = document.getElementById(this.state.windows.selectionInfoWindow.widthInputId);
if (widthInput === null) return;
var value = parseFloat(floorplan.convertUnitsToPixels(widthInput.value));
if (isNaN(value)) {
alert("Please enter a valid number");
ui.setSelectionInfo(node, floorplan);
return;
}
floorplan.skipsUndoManager = true;
floorplan.startTransaction("resize node");
if (!floorplan.isReadOnly) {
// Case: Window / Door Nodes (part.width is part.data.length), keeps windows and doors within wall boundaries (and surrounding wall part boundaries)
if (node.category === 'WindowNode' || node.category === "DoorNode") {
var wall = floorplan.findPartForKey(node.data.group);
var loc = node.location.copy();
// constrain max width "value" by the free stretch on the wall "node" is in
if (wall !== null) {
var containingStretch = getWallPartStretch(node);
var stretchLength = Math.sqrt(containingStretch.point1.distanceSquaredPoint(containingStretch.point2));
if (stretchLength < value) {
value = stretchLength;
loc = new go.Point((containingStretch.point1.x + containingStretch.point2.x) / 2,
(containingStretch.point1.y + containingStretch.point2.y) / 2);
}
}
floorplan.model.setDataProperty(node.data, "length", value);
node.location = loc;
floorplan.updateWallDimensions();
}
// Case: Wall Groups; wall length adjustment; do not allow walls to be shorter than the distance between their fathest apart wallParts
else if (node.category === "WallGroup") {
var sPt = node.data.startpoint.copy();
var ePt = node.data.endpoint.copy();
var angle = sPt.directionPoint(ePt);
var midPoint = new go.Point(((sPt.x + ePt.x) / 2), ((sPt.y + ePt.y) / 2));
var newEpt = new go.Point((midPoint.x + (value / 2)), midPoint.y);
var newSpt = new go.Point((midPoint.x - (value / 2)), midPoint.y);
newEpt.offset(-midPoint.x, -midPoint.y).rotate(angle).offset(midPoint.x, midPoint.y);
newSpt.offset(-midPoint.x, -midPoint.y).rotate(angle).offset(midPoint.x, midPoint.y);
// Edge Case 1: The user has input a length shorter than the edge wallPart's endpoints allow
// find the endpoints of the wallparts closest to the endpoints of the wall
var closestPtToSpt = null; var farthestPtFromSpt;
var closestDistToSpt = Number.MAX_VALUE; var farthestDistFromSpt = 0;
node.memberParts.iterator.each(function (wallPart) {
var endpoints = getWallPartEndpoints(wallPart);
var endpoint1 = endpoints[0];
var endpoint2 = endpoints[1];
var distance1 = Math.sqrt(endpoint1.distanceSquaredPoint(sPt));
var distance2 = Math.sqrt(endpoint2.distanceSquaredPoint(sPt));
if (distance1 < closestDistToSpt) {
closestDistToSpt = distance1;
closestPtToSpt = endpoint1;
} if (distance1 > farthestDistFromSpt) {
farthestDistFromSpt = distance1;
farthestPtFromSpt = endpoint1;
} if (distance2 < closestDistToSpt) {
closestDistToSpt = distance2;
closestPtToSpt = endpoint2;
} if (distance2 > farthestDistFromSpt) {
farthestDistFromSpt = distance2;
farthestPtFromSpt = endpoint2;
}
});
if (closestPtToSpt !== null) {
// if the proposed length is smaller than the minDistance, set wall length to minDistance
var proposedDistance = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
var minDistance = Math.sqrt(closestPtToSpt.distanceSquaredPoint(farthestPtFromSpt));
if (proposedDistance < minDistance) {
newSpt = closestPtToSpt;
newEpt = farthestPtFromSpt;
}
}
/*
* Edge Case 2: The new wall endpoints constructed based on user input do not generate a wall too short for the wall's edge wallPart's endpoints;
* however, there is/are a/some wallPart(s) that do not fit along the new wall endpoints (due to midpoint construction)
* if a wallPart endpoint is outside the line created by newSpt and newEpt, adjust the endpoints accordingly
*/
var farthestPtFromWallPt = null; var farthestFromWallPtDist = 0;
node.memberParts.iterator.each(function (part) {
var endpoints = getWallPartEndpoints(part);
// check for endpoints of wallParts not along the line segment made by newSpt and newEpt
for (var i = 0; i < endpoints.length; i++) {
var point = endpoints[i];
var distanceToStartPoint = parseFloat(Math.sqrt(point.distanceSquaredPoint(newSpt)).toFixed(2));
var distanceToEndPoint = parseFloat(Math.sqrt(point.distanceSquaredPoint(newEpt)).toFixed(2));
var wallLength = parseFloat(Math.sqrt(newSpt.distanceSquaredPoint(newEpt)).toFixed(2));
if ((distanceToStartPoint + distanceToEndPoint).toFixed(2) !== wallLength.toFixed(2)) {
var testDistance = Math.sqrt(point.distanceSquaredPoint(newSpt));
if (testDistance > farthestFromWallPtDist) {
farthestFromWallPtDist = testDistance;
farthestPtFromWallPt = point;
}
}
}
});
// if a wallPart endpoint is outside the wall, adjust the endpoints of the wall to accomodate it
if (farthestPtFromWallPt !== null) {
var distance = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
if (farthestPtFromWallPt.distanceSquaredPoint(newSpt) < farthestPtFromWallPt.distanceSquaredPoint(newEpt)) {
newSpt = farthestPtFromWallPt;
var totalLength = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
newEpt = new go.Point(newSpt.x + ((distance / totalLength) * (newEpt.x - newSpt.x)),
newSpt.y + ((distance / totalLength) * (newEpt.y - newSpt.y)));
} else {
newEpt = farthestPtFromWallPt;
var totalLength = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
newSpt = new go.Point(newEpt.x + ((distance / totalLength) * (newSpt.x - newEpt.x)),
newEpt.y + ((distance / totalLength) * (newSpt.y - newEpt.y)));
}
}
floorplan.model.setDataProperty(node.data, "startpoint", newSpt);
floorplan.model.setDataProperty(node.data, "endpoint", newEpt);
floorplan.updateWall(node);
}
// Case: Standard / Multi-Purpose Nodes; basic width ajustment
else floorplan.model.setDataProperty(node.data, "width", value);
}
floorplan.commitTransaction("resize node");
floorplan.skipsUndoManager = false;
}
// Set height, width, and color of the selection based on user input in the Selection Info Window
FloorplanUI.prototype.applySelectionChanges = function() {
var floorplan = this.floorplan;
var ui = this;
this.setHeight();
this.setWidth();
this.setColor();
ui.setSelectionInfo(floorplan.selection.first(), floorplan);
}