From e0e9892ff0d1e55a6bc56157328ca216b9f55449 Mon Sep 17 00:00:00 2001 From: myoualid Date: Fri, 20 Sep 2024 16:26:40 +0100 Subject: [PATCH] cost module web-ui: - remove Blender operators in favor of ifc.api functions - replace BonsaiBIM Data class dependency with new ifc5D2json script - enable viewing multiple cost schedules - add Identifaction column ref #5369 --- .../bonsai/bim/data/webui/static/css/cost.css | 40 ++- .../bim/data/webui/static/css/shared.css | 2 +- .../bonsai/bim/data/webui/static/js/cost.js | 161 +++++----- .../data/webui/static/js/utilities/costui.js | 299 +++++++++--------- src/bonsai/bonsai/tool/cost.py | 7 +- src/bonsai/bonsai/tool/web.py | 82 +++-- src/ifc5d/ifc5d/ifc2json.py | 160 ++++++++++ 7 files changed, 455 insertions(+), 296 deletions(-) create mode 100644 src/ifc5d/ifc5d/ifc2json.py diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css index 039ad8e4c1..38b8d028b0 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -19,6 +19,7 @@ body { margin: 0; padding: 0; box-sizing: border-box; + height: 100vh; } .floating-form { @@ -39,15 +40,15 @@ body { .form-header { display: flex; - justify-content: flex-start; + justify-content: center; align-items: center; - margin-bottom: 10px; background-color: #323936; padding: var(--padding-medium); font-size: large; border-bottom: 1px solid #ddd; border-radius: 6px 6px 0 0; height: 5%; + position: relative; } .form-container { @@ -58,9 +59,7 @@ body { .form-section { margin-bottom: 1em; - height: 65%; - overflow-y: auto; - overflow-x: hidden; + height: 70%; } .form-section h3 { @@ -185,14 +184,21 @@ body { .table-container { margin-top: 1em; - overflow-x: auto; - overflow-y: auto; - position: relative; - border: 1px solid #ddd; + display: flex; + flex-direction: row; +} + +.table-wrapper { + flex: 1; + overflow: auto; + border: 2px solid #254e3c; + /* shadow to the top */ + box-shadow: 0 -5px 5px -0 #254e3c; } .floating-form .table-container { max-height: 80%; + overflow: auto; } #cost-items { @@ -208,9 +214,12 @@ body { height: 50vh; } +tr, td, th { + height: 100%; +} + table { width: 100%; - height: 100%; border-collapse: collapse; user-select: none; font-size: var(--fontSize); @@ -247,8 +256,9 @@ td { align-items: center; } -#cost-items td:first-child { +.flex-container { display: flex; + justify-content: space-between; align-items: center; } @@ -375,10 +385,6 @@ form table { [id^="cost-items"] td:nth-child(1) { width: auto; } -[id^="cost-items"] th:not(:nth-child(1)), -[id^="cost-items"] td:not(:nth-child(1)) { - width: 10%; -} [id^="cost-items"] th:last-child, [id^="cost-items"] td:last-child { @@ -411,3 +417,7 @@ td { position: absolute; transform: translateY(-50%); } + +i { + margin-right: 5px; +} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css index 92fd10bda0..5082d95230 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css @@ -222,7 +222,7 @@ button:hover { border-collapse: collapse; display: flex; justify-content: flex-start; - flex-wrap: wrap; + flex-wrap: nowrap; } .column-container { diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js index e1c4ed376a..ff469dedb2 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -160,36 +160,6 @@ function handleConnectedClients(data) { }); } -function handleCostValuesData(data) { - CostUI.createCostValuesForm({ - costValues: data.data["cost_values"]["cost_values"], - costItemId: data.data["cost_values"]["cost_item_id"], - callbacks: { - editCostValues: editCostValues, - addCostValue: addCostValue, - deleteCostValue: deleteCostValue, - }, - }); - - CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]); -} - -function addCostValue(costItemId) { - executeOperator({ type: "addCostValue", costItemId: costItemId }); -} - -function editCostValues(costItemId, costValues) { - executeOperator({ - type: "editCostValues", - costItemId: costItemId, - costValues: costValues, - }); -} - -function deleteCostItem(costItemId) { - executeOperator({ type: "deleteCostItem", costItemId: costItemId }); -} - function handleThemeData(themeData) { function arrayToRgbString(arr) { const [r, g, b, a] = arr.map((num) => Math.round(num * 255)); @@ -291,34 +261,6 @@ function toggleClientList() { clientList.addClass("show"); } -function addCostItem(costItemId) { - executeOperator({ type: "addCostItem", costItemId: costItemId }); -} - -function editCostItemName(costItemId, name) { - executeOperator({ - type: "editCostItemName", - costItemId: costItemId, - name: name, - }); -} - -function selectAssignedElements(costItemId) { - executeOperator({ type: "selectAssignedElements", costItemId: costItemId }); -} - -function handleWebConnect() { - executeOperator({ type: "getPredefinedTypes" }); - getCostSchedules(); -} - -function addSummaryCostItem(costScheduleId) { - executeOperator({ - type: "addSummaryCostItem", - costScheduleId: costScheduleId, - }); -} - function handleCostSchedulesData(data) { const blenderId = data.blenderId; const loadedSchedule = loadedSchedules[blenderId] @@ -358,28 +300,20 @@ function handleCostSchedulesData(data) { } function handleCostItemsData(data) { + const costScheduleId = data.data["cost_items"]["schedule_data"][0]["id"]; const cards = document.querySelectorAll("[id^='schedule-']"); cards.forEach((card) => { - if (card.id === "schedule-" + data.data["cost_items"]["cost_schedule_id"]) { - card.classList.add("highlighted"); - } else { - card.classList.remove("highlighted"); - } + card.classList.remove("highlighted"); }); - - const costScheduleId = data.data["cost_items"]["cost_schedule_id"]; - loadedSchedules[data.blenderId] = costScheduleId; CostUI.highlightElement("schedule-" + costScheduleId); - + loadedSchedules[data.blenderId] = costScheduleId; const currency = data.data["cost_items"]["currency"] ? data.data["cost_items"]["currency"]["name"] : "Undefined"; - + const costSchedule = data.data["cost_items"]["schedule_data"][0]; CostUI.createCostSchedule({ - costItems: data.data["cost_items"]["cost_items"], + costSchedule: costSchedule, currency: currency, - costScheduleId: costScheduleId, - blenderID: data.blenderId, callbacks: { addCostItem: addCostItem, deleteCostItem: deleteCostItem, @@ -394,23 +328,32 @@ function handleCostItemsData(data) { }); } -function enableEditingQuantities(costItemId) { - executeOperator({ type: "enableEditingQuantities", costItemId: costItemId }); +function addCostItem(costItemId) { + executeOperator({ type: "addCostItem", costItemId: costItemId }); } -function duplicateCostItem(costItemId) { - executeOperator({ type: "duplicateCostItem", costItemId: costItemId }); +function editCostItemName(costItemId, name) { + executeOperator({ + type: "editCostItemName", + costItemId: costItemId, + name: name, + }); } -function executeOperator(operator, blenderId) { - const msg = { - sourcePage: "cost", - operator: operator, - }; - if (blenderId !== undefined) { - msg.BlenderId = blenderId; - } - socket.emit("web_operator", msg); +function selectAssignedElements(costItemId) { + executeOperator({ type: "selectAssignedElements", costItemId: costItemId }); +} + +function handleWebConnect() { + executeOperator({ type: "getPredefinedTypes" }); + getCostSchedules(); +} + +function addSummaryCostItem(costScheduleId) { + executeOperator({ + type: "addSummaryCostItem", + costScheduleId: costScheduleId, + }); } function loadCostSchedule(costScheduleId, blenderId) { @@ -427,3 +370,53 @@ function getCostSchedules(blenderId) { function enableEditingCostValues(costItemId) { executeOperator({ type: "enableEditingCostValues", costItemId: costItemId }); } + +function enableEditingQuantities(costItemId) { + executeOperator({ type: "enableEditingQuantities", costItemId: costItemId }); +} + +function duplicateCostItem(costItemId) { + executeOperator({ type: "duplicateCostItem", costItemId: costItemId }); +} + +function handleCostValuesData(data) { + CostUI.createCostValuesForm({ + costValues: data.data["cost_values"]["cost_values"], + costItemId: data.data["cost_values"]["cost_item_id"], + callbacks: { + editCostValues: editCostValues, + addCostValue: addCostValue, + deleteCostValue: deleteCostValue, + }, + }); + + CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]); +} + +function addCostValue(costItemId) { + executeOperator({ type: "addCostValue", costItemId: costItemId }); +} + +function editCostValues(costItemId, costValues) { + executeOperator({ + type: "editCostValues", + costItemId: costItemId, + costValues: costValues, + }); +} + +function deleteCostItem(costItemId) { + CostUI.deleteCostItem(costItemId); + executeOperator({ type: "deleteCostItem", costItemId: costItemId }); +} + +function executeOperator(operator, blenderId) { + const msg = { + sourcePage: "cost", + operator: operator, + }; + if (blenderId !== undefined) { + msg.BlenderId = blenderId; + } + socket.emit("web_operator", msg); +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js index 53055f2e65..468932bb99 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -7,17 +7,42 @@ export class CostUI { } static removeCostSchedule(id) { - document.getElementById("cost-items-" + id).remove(); + const table = document.getElementById("cost-items-" + id); + table ? table.parentElement.remove() : null; } - static createCostTable(id, currency, callbacks) { - CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; + static createCostTable({ costSchedule, currency, callbacks }) { + let tableWrapper; + const id = costSchedule.id; + if (CostUI.isCostScheduleLoaded(id)) { + const table = document.getElementById("cost-items-" + id); + tableWrapper = table.parentElement; + table.remove(); + } else { + tableWrapper = document.createElement("div"); + tableWrapper.classList.add("table-wrapper"); + document.getElementById("cost-items").appendChild(tableWrapper); + const tableHeader = document.createElement("div"); + tableHeader.classList.add("form-header"); + const text = CostUI.Text( + costSchedule.Name, + "fa-solid fa-money-bill-wave", + "x-large" + ); + const callback = () => { + tableWrapper.remove(); + CostUI.unhighlightElement("schedule-" + id); + }; + let closeButton = CostUI.createCloseButton(callback); + tableHeader.appendChild(text); + tableHeader.appendChild(closeButton); + tableWrapper.appendChild(tableHeader); + } const table = document.createElement("table"); table.id = "cost-items-" + id; const tbody = document.createElement("tbody"); - tbody.setAttribute("id", "cost-items"); - const columnHeaders = [ + "ID", "Name", "Quantity", "Unit", @@ -44,7 +69,7 @@ export class CostUI { table.appendChild(thead); table.appendChild(tbody); - document.getElementById("cost-items").appendChild(table); + tableWrapper.appendChild(table); CostUI.createContextMenu(callbacks); table.get_blender_id = function () { @@ -54,12 +79,11 @@ export class CostUI { return [table, tbody]; } - static deleteCostItem(costItemId, callback) { + static deleteCostItem(costItemId) { const costItemRow = document.getElementById(costItemId); let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {}; expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState); localStorage.setItem("expandedState", JSON.stringify(expandedState)); - callback(costItemId); } static createContextMenu(callbacks) { @@ -79,16 +103,20 @@ export class CostUI { event.preventDefault(); const targetRow = event.target.closest("tr"); const targetCell = event.target.closest("td"); - if (targetRow && targetRow.parentElement.id === "cost-items") { - const contextMenu = document.getElementById("context-menu"); - contextMenu.style.display = "block"; - contextMenu.style.left = `${event.pageX}px`; - contextMenu.style.top = `${event.pageY}px`; - - contextMenu.targetRow = targetRow; - contextMenu.targetCell = targetCell; - } else { - document.getElementById("context-menu").style.display = "none"; + if (targetRow) { + const isCostTable = targetRow.parentElement.parentElement + ? targetRow.parentElement.parentElement.id.includes("cost-items") + : false; + if (isCostTable) { + const contextMenu = document.getElementById("context-menu"); + contextMenu.style.display = "block"; + contextMenu.style.left = `${event.pageX}px`; + contextMenu.style.top = `${event.pageY}px`; + contextMenu.targetRow = targetRow; + contextMenu.targetCell = targetCell; + } else { + document.getElementById("context-menu").style.display = "none"; + } } }); @@ -102,104 +130,79 @@ export class CostUI { const editCostValuesButton = document.getElementById( "edit-cost-values-button" ); - editCostValuesButton.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - const targetCell = document.getElementById("context-menu").targetCell; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.enableEditingCostValues - ? callbacks.enableEditingCostValues(costItemId) - : null; - } - document.getElementById("context-menu").style.display = "none"; - }); - const addButton = document.getElementById("add-cost-item-button"); - if (!addButton.dataset.listenerAdded) { - function addCostItemHandler(e) { - e.stopPropagation(); - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null; - } - document.getElementById("context-menu").style.display = "none"; - } - addButton.addEventListener("click", addCostItemHandler); - addButton.dataset.listenerAdded = "true"; - } - const deleteCostItemButton = document.getElementById("delete-cost-item"); - if (deleteCostItemButton && !deleteCostItemButton.hasListener) { - deleteCostItemButton.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem); - } - document.getElementById("context-menu").style.display = "none"; - }); - deleteCostItemButton.hasListener = true; - } - const duplicateButton = document.getElementById("duplicate-button"); - if (duplicateButton && !duplicateButton.hasListener) { - duplicateButton.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.duplicateCostItem(costItemId); - } - document.getElementById("context-menu").style.display = "none"; - }); - duplicateButton.hasListener = true; - } - const getSelectedProducts = document.getElementById("assign-selection"); - if (getSelectedProducts && !getSelectedProducts.hasListener) { - getSelectedProducts.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.enableEditingQuantities(costItemId); - } - document.getElementById("context-menu").style.display = "none"; - }); - getSelectedProducts.hasListener = true; + + function handleButtonClick(button, callback) { + if (button && !button.hasListener) { + button.addEventListener("click", function () { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + callback(costItemId); + } + document.getElementById("context-menu").style.display = "none"; + }); + button.hasListener = true; + } } - function getColumnNames(tableId) { - const table = document.getElementById(tableId); - const headerRow = table.querySelector("thead tr"); - return Array.from(headerRow.children).map((headerCell) => - headerCell.textContent.trim() - ); + // if (deleteCostItemButton && !deleteCostItemButton.hasListener) { + // deleteCostItemButton.addEventListener("click", function () { + // const targetRow = document.getElementById("context-menu").targetRow; + // if (targetRow) { + // const costItemId = parseInt(targetRow.getAttribute("id")); + // CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem); + // } + // document.getElementById("context-menu").style.display = "none"; + // }); + // deleteCostItemButton.hasListener = true; + // } + + handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem); + handleButtonClick(duplicateButton, callbacks.duplicateCostItem); + handleButtonClick(getSelectedProducts, callbacks.enableEditingQuantities); + handleButtonClick(editCostValuesButton, callbacks.enableEditingCostValues); + handleButtonClick(addButton, callbacks.addCostItem); + handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem); + + addTableListeners(callbacks); + + function addTableListeners(callbacks) { + function getColumnNames(tableId) { + const table = document.getElementById(tableId); + const headerRow = table.querySelector("thead tr"); + return Array.from(headerRow.children).map((headerCell) => + headerCell.textContent.trim() + ); + } + document + .getElementById("cost-items") + .addEventListener("click", function (event) { + const targetRow = event.target.closest("tr"); + const targetCell = event.target.closest("td"); + if (targetRow && targetCell) { + const columnIndex = Array.from(targetRow.children).indexOf( + targetCell + ); + const costItemId = parseInt(targetRow.getAttribute("id")); + const columnName = getColumnNames("cost-items")[columnIndex]; + + if (columnName.includes("Cost") && !columnName.includes("Total")) { + callbacks.enableEditingCostValues + ? callbacks.enableEditingCostValues(costItemId) + : null; + } + if (columnName === "Quantity") { + callbacks.enableEditingQuantities + ? callbacks.enableEditingQuantities(costItemId) + : null; + } + } + }); } - - document - .getElementById("cost-items") - .addEventListener("click", function (event) { - const targetRow = event.target.closest("tr"); - const targetCell = event.target.closest("td"); - if (targetRow && targetCell) { - const columnIndex = Array.from(targetRow.children).indexOf( - targetCell - ); - const costItemId = parseInt(targetRow.getAttribute("id")); - const columnName = getColumnNames("cost-items")[columnIndex]; - - if (columnName.includes("Cost") && !columnName.includes("Total")) { - callbacks.enableEditingCostValues - ? callbacks.enableEditingCostValues(costItemId) - : null; - } - if (columnName === "Quantity") { - callbacks.enableEditingQuantities - ? callbacks.enableEditingQuantities(costItemId) - : null; - } - } - }); } static deleteCostItemRow(targetRow, expandedState) { @@ -353,19 +356,13 @@ export class CostUI { return div; } - static createCostSchedule({ - costItems, - currency, - costScheduleId, - blenderID, - callbacks = {}, - }) { - const [table, tbody] = CostUI.createCostTable( - blenderID, + static createCostSchedule({ costSchedule, currency, callbacks = {} }) { + const [table, tbody] = CostUI.createCostTable({ + costSchedule, currency, - callbacks - ); - if (costItems.length === 0) { + callbacks, + }); + if (costSchedule["cost_items"].length === 0) { const tr = document.createElement("tr"); const td = document.createElement("td"); td.colSpan = 6; @@ -383,7 +380,13 @@ export class CostUI { td.appendChild(addSummaryCostItemButton); addSummaryCostItemButton.classList.add("action-button"); } else { - CostUI.createCostTree(costItems, tbody, 0, null, callbacks); + CostUI.createCostTree( + costSchedule["cost_items"], + tbody, + 0, + null, + callbacks + ); CostUI.applyExpandedState(); } } @@ -404,9 +407,9 @@ export class CostUI { ); container.appendChild(row); - if (costItem.is_nested_by && costItem.is_nested_by.length > 0) { + if (costItem.IsNestedBy && costItem.IsNestedBy.length > 0) { CostUI.createCostTree( - costItem.is_nested_by, + costItem.IsNestedBy, container, nestingLevel + 1, costItem.id, @@ -434,6 +437,10 @@ export class CostUI { ? CostUI.format_number(costItem.TotalAppliedValue) : "-"; const row = CostUI.createCostItemRow(costItem, nestingLevel, parentID); + const identification = costItem.Identification + ? costItem.Identification + : "XXX"; + const idCell = CostUI.createTableCell(identification); const expandButton = CostUI.createExpandButton(costItem); const nameCell = CostUI.createNameCell( costItem, @@ -456,6 +463,7 @@ export class CostUI { actionsCell.classList.add("actions-column"); + row.appendChild(idCell); row.appendChild(nameCell); row.appendChild(totalCostQuantityCell); row.appendChild(unitSymbolCell); @@ -491,7 +499,7 @@ export class CostUI { expandButton.classList.add("fa-solid"); expandButton.classList.add("fa-angle-right"); - if (row.is_nested_by && row.is_nested_by.length > 0) { + if (row.IsNestedBy && row.IsNestedBy.length > 0) { expandButton.classList.add("fa-angle-right"); } else { expandButton.style.visibility = "hidden"; @@ -614,7 +622,7 @@ export class CostUI { static createNameCell(costItem, nestingLevel, expandButton, callbacks) { const nameCell = document.createElement("td"); const nameInput = document.createElement("input"); - nameInput.value = costItem.name ? costItem.name : "Unnamed"; + nameInput.value = costItem.Name ? costItem.Name : "Unnamed"; nameInput.addEventListener("change", function () { callbacks.editCostItemName @@ -631,6 +639,7 @@ export class CostUI { }); nameCell.style.paddingLeft = nestingLevel * 20 + "px"; + nameCell.classList.add("row-container"); nameCell.appendChild(expandButton); nameCell.appendChild(nameInput); const widthButton = 20; @@ -652,7 +661,7 @@ export class CostUI { const totalCostCell = document.createElement("td"); const totalCost = CostUI.format_number(costItem.TotalCost); totalCostCell.textContent = totalCost; - if (costItem.is_sum) { + if (costItem.IsSum) { totalCostCell.textContent = totalCost + " *"; } else if (!costItem.TotalCost) { const button = CostUI.createButton("Sum", "fa-solid fa-calculator"); @@ -746,7 +755,7 @@ export class CostUI { const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); deleteButton.addEventListener( "click", - CostUI.deleteCostItem.bind(null, costItemId, deleteCostItem) + deleteCostItem.bind(null, costItemId) ); return deleteButton; } @@ -1089,6 +1098,13 @@ export class CostUI { } static Form({ id, name, icon = "fa-solid fa-file", shouldHide = false }) { + function style(form) { + form.classList.add("floating-form"); + form.style.position = "absolute"; + form.style.top = "50%"; + form.style.left = "50%"; + form.style.transform = "translate(-50%, -50%)"; + } if (document.getElementById(id)) { const formContainer = document .getElementById(id) @@ -1098,17 +1114,19 @@ export class CostUI { } const form = document.createElement("form"); form.id = id; - form.classList.add("floating-form"); - form.style.position = "absolute"; - form.style.top = "50%"; - form.style.left = "50%"; - form.style.transform = "translate(-50%, -50%)"; + style(form); const header = CostUI.createHeader(name, icon); let closeButton; + if (shouldHide) { closeButton = CostUI.createHideButton(form); } else { - closeButton = CostUI.createCloseButton(form); + const callback = () => { + form.remove(); + const costItemId = form.id.split("-").pop(); + CostUI.unhighlightElement(costItemId); + }; + closeButton = CostUI.createCloseButton(callback); // Pass the function reference without invoking it } form.appendChild(header); form.appendChild(closeButton); @@ -1200,16 +1218,11 @@ export class CostUI { return hideButton; } - static createCloseButton(form) { + static createCloseButton(callback) { const closeButton = document.createElement("span"); closeButton.classList.add("close-button"); closeButton.innerHTML = "×"; - closeButton.addEventListener("click", function () { - form.remove(); - const costItemId = form.id.split("-").pop(); - CostUI.unhighlightElement(costItemId); - }); - + closeButton.addEventListener("click", callback); return closeButton; } @@ -2114,9 +2127,7 @@ export class CostUI { }); } }); - } - - else if (key.toLowerCase().includes("value")) { + } else if (key.toLowerCase().includes("value")) { const cell = document.createElement("td"); const input = document.createElement("input"); input.type = "number"; diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 8b38502c14..c3c0bf59d2 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -865,12 +865,7 @@ class Cost(bonsai.core.tool.Cost): def generate_cost_schedule_browser(cls, cost_chedule) -> None: if not bpy.context.scene.WebProperties.is_connected: bpy.ops.bim.connect_websocket_server(page="costing") - cost_schedule_data = cls.create_cost_schedule_json(cost_chedule) - tool.Web.send_webui_data( - data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id(), "currency": cls.currency()}, - data_key="cost_items", - event="cost_items", - ) + tool.Web.load_cost_schedule_web_ui(cost_chedule) @classmethod def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict: diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 2564c0fb6b..53e7763ffd 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -41,6 +41,7 @@ from time import sleep from pathlib import Path import bonsai.core.sequence import bonsai.core.cost +from ifc5d.ifc2json import ifc5D2json sio = None ws_process = None @@ -346,9 +347,11 @@ class Web(bonsai.core.tool.Web): ifc_file = tool.Ifc.get() if operator_data["type"] == "getPredefinedTypes": - print("getting predefined types") - predefined_types = ifcopenshell.util.cost.get_cost_schedule_types(ifc_file) - cls.send_webui_data(data=predefined_types, data_key="predefined_types", event="predefined_types") + cls.send_webui_data( + data=ifcopenshell.util.cost.get_cost_schedule_types(ifc_file), + data_key="predefined_types", + event="predefined_types" + ) if operator_data["type"] == "addCostSchedule": bonsai.core.cost.add_cost_schedule( tool.Ifc, name=operator_data["name"], predefined_type=operator_data["predefinedType"] @@ -361,7 +364,6 @@ class Web(bonsai.core.tool.Web): event="cost_schedules", ) if operator_data["type"] == "getCostSchedules": - print("getting cost schedules") cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")] currency = tool.Cost.currency() cls.send_webui_data( @@ -370,38 +372,36 @@ class Web(bonsai.core.tool.Web): event="cost_schedules", ) if operator_data["type"] == "loadCostSchedule": - print("loading cost schedule") cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) - bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "selectAssignedElements": - print("selecting assigned elements") cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) tool.Spatial.select_products(products, unhide=True) if operator_data["type"] == "addSummaryCostItem": cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) - bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule) + tool.Ifc.run("cost.add_cost_item", cost_schedule=cost_schedule) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "addCostItem": if not operator_data["costItemId"]: return - bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"]) - cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) + cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]) + tool.Ifc.run("cost.add_cost_item", cost_item=cost_item) + cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "deleteCostItem": - cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) - bpy.ops.bim.remove_cost_item(cost_item=operator_data["costItemId"]) + cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) + tool.Ifc.run("cost.remove_cost_item", cost_item=cost_item) + cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "duplicateCostItem": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) - new_cost_item = ifcopenshell.api.cost.copy_cost_item(ifc_file, cost_item) + tool.Ifc.run("cost.copy_cost_item", cost_item=cost_item) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "editCostItemName": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) tool.Ifc.run("cost.edit_cost_item", cost_item=cost_item, attributes={"Name": operator_data["name"]}) - tool.Cost.load_cost_schedule_tree() if operator_data["type"] == "enableEditingCostValues": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) cost_values = ifcopenshell.util.cost.get_cost_values(cost_item) @@ -422,18 +422,15 @@ class Web(bonsai.core.tool.Web): ) if operator_data["type"] == "addSumCostValue": cost_item = ifc_file.by_id(operator_data["costItemId"]) - value = ifcopenshell.api.cost.add_cost_value( - ifc_file, - parent=cost_item, - ) - ifcopenshell.api.cost.edit_cost_value(file=ifc_file, cost_value=value, attributes={"Category": "*"}) + value = tool.Ifc.run("cost.add_cost_value", parent=cost_item) + tool.Ifc.run("cost.edit_cost_value", cost_value=value, attributes={"Category": "*"}) cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) - tool.Cost.load_cost_schedule_tree() cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "deleteCostValue": - bpy.ops.bim.remove_cost_value(parent=operator_data["costItemId"], cost_value=operator_data["costValueId"]) cost_item = ifc_file.by_id(operator_data["costItemId"]) + cost_value = ifc_file.by_id(operator_data["costValueId"]) + tool.Ifc.run("cost.remove_cost_value", parent=cost_item, cost_value=cost_value) cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "editCostValues": @@ -441,7 +438,6 @@ class Web(bonsai.core.tool.Web): cost_item = ifc_file.by_id(cost_item_id) cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) for value_data in operator_data["costValues"] or []: - print("editing cost value data", value_data) value = ifc_file.by_id(value_data["id"]) if value_data["costType"] == "FIXED": attributes = {"AppliedValue": value_data["appliedValue"], "Category": None} @@ -453,51 +449,43 @@ class Web(bonsai.core.tool.Web): elif value_data["costType"] == "SUM": attributes = {"Category": "*"} ifcopenshell.api.cost.edit_cost_value(file=ifc_file, cost_value=value, attributes=attributes) - print(value.get_info()) - tool.Cost.load_cost_schedule_tree() cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "addProductAssignments": prop_name = operator_data["propName"] if prop_name == "count": prop_name = "" - bpy.ops.bim.assign_cost_item_quantity( - cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name - ) + bonsai.core.cost.assign_cost_item_quantity(tool.Ifc, tool.Cost, cost_item=ifc_file.by_id(operator_data["costItemId"]), related_object_type="PRODUCT", prop_name=prop_name) cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "enableEditingQuantities": cost_item_id = operator_data["costItemId"] cost_item = ifc_file.by_id(cost_item_id) - cls.enableEditingCostItemQuantities(cost_item) + cls.load_cost_item_quantities_ui(cost_item) if operator_data["type"] == "AddCostItemQuantity": cost_item_id = operator_data["costItemId"] cost_item = ifc_file.by_id(cost_item_id) cost_schedule = tool.Cost.get_cost_schedule(cost_item) - bpy.ops.bim.add_cost_item_quantity(cost_item=cost_item_id, ifc_class=operator_data["ifcClass"]) + tool.Ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=operator_data["ifcClass"]) cls.load_cost_schedule_web_ui(cost_schedule) - cls.enableEditingCostItemQuantities(cost_item) + cls.load_cost_item_quantities_ui(cost_item) if operator_data["type"] == "editCostItemQuantity": cost_item_id = operator_data["costItemId"] cost_item = ifc_file.by_id(cost_item_id) cost_schedule = tool.Cost.get_cost_schedule(cost_item) physical_quantity = tool.Ifc.get().by_id(operator_data["quantityId"]) - attributes = operator_data["attributes"] - tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes) - tool.Cost.load_cost_item_quantities(ifc_file.by_id(operator_data["costItemId"])) + tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=operator_data["attributes"]) cls.load_cost_schedule_web_ui(cost_schedule) - cls.enableEditingCostItemQuantities(cost_item) + cls.load_cost_item_quantities_ui(cost_item) if operator_data["type"] == "deleteCostItemQuantity": - bpy.ops.bim.remove_cost_item_quantity( - cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"] - ) - cost_item_id = operator_data["costItemId"] - cost_item = ifc_file.by_id(cost_item_id) + cost_item = ifc_file.by_id(operator_data["costItemId"]) + physical_quantity = ifc_file.by_id(operator_data["quantityId"]) + tool.Ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity) cost_schedule = tool.Cost.get_cost_schedule(cost_item) cls.load_cost_schedule_web_ui(cost_schedule) - cls.enableEditingCostItemQuantities(cost_item) + cls.load_cost_item_quantities_ui(cost_item) @classmethod - def enableEditingCostItemQuantities(cls, cost_item): + def load_cost_item_quantities_ui(cls, cost_item : ifcopenshell.entity_instance) -> None: if not cost_item: return selected_products = list(tool.Spatial.get_selected_products()) @@ -520,7 +508,7 @@ class Web(bonsai.core.tool.Web): ) @classmethod - def selection_data(cls, cost_item, elements): + def selection_data(cls, cost_item : ifcopenshell.entity_instance, elements : list) -> list: if not elements: return [] if not cost_item: @@ -566,13 +554,15 @@ class Web(bonsai.core.tool.Web): return data @classmethod - def load_cost_schedule_web_ui(cls, cost_schedule): - json_data = tool.Cost.create_cost_schedule_json(cost_schedule) + def load_cost_schedule_web_ui(cls, cost_schedule :ifcopenshell.entity_instance) -> None: + ifc2json = ifc5D2json() + ifc2json.file_path = tool.Ifc.get() + ifc2json.cost_schedule = cost_schedule + schedule_data = ifc2json.convert_ifc_to_json() cls.send_webui_data( data={ - "cost_items": json_data, - "cost_schedule_id": cost_schedule.id(), "currency": tool.Cost.currency(), + "schedule_data": schedule_data, }, data_key="cost_items", event="cost_items", diff --git a/src/ifc5d/ifc5d/ifc2json.py b/src/ifc5d/ifc5d/ifc2json.py new file mode 100644 index 0000000000..be6320597a --- /dev/null +++ b/src/ifc5d/ifc5d/ifc2json.py @@ -0,0 +1,160 @@ +import ifcopenshell +import ifcopenshell.util.unit +from typing import Any +import ifcopenshell.util.cost +import ifcopenshell.util.date + +CostItem = dict[str, Any] + +class ifc5D2json: + def __init__(self): + self.json: str = None + self.file_path: str = None + self.file: ifcopenshell.file = None + self.cost_schedule: ifcopenshell.entity_instance = None + self.units: dict[str, ifcopenshell.entity_instance] = {} + self._cost_values = {} + self.data: list[dict[str, Any]] = [] + + def convert_ifc_to_json(self): + self.data = [] + if not isinstance(self.file_path, str): + self.file = self.file_path + else: + self.file = ifcopenshell.open(self.file_path) + if not self.cost_schedule: + self.extract_cost_schedules() + else: + self.extract_cost_schedule() + return self.data + + def extract_cost_schedule(self): + data = self.cost_schedule.get_info(recursive=True) + data["cost_items"] = self.build_schedule_tree(self.cost_schedule) + self.data.append(data) + + def extract_cost_schedules(self): + for cost_schedule in self.file.by_type("IfcCostSchedule"): + data = cost_schedule.get_info(recursive=True) + data["cost_items"] = self.build_schedule_tree(cost_schedule) + self.data.append(data) + + def build_schedule_tree(self, cost_schedule: ifcopenshell.entity_instance) -> dict[str, Any]: + schedule_data = [] + self._cost_values = {} + for rel in cost_schedule.Controls or []: + for cost_item in rel.RelatedObjects: + self.extract_cost_item_data(cost_item, schedule_data) + return schedule_data + + def extract_cost_item_data(self , cost_item: ifcopenshell.entity_instance, json_data): + data = {} + self.extract_cost_item_quantities(cost_item, data) + self.populate_cost_values(cost_item, data) + self.populate_nesting_index(cost_item, data) + data = {**data, **cost_item.get_info(recursive=True)} + data["id"] = cost_item.id() + data["IsNestedBy"] = [] + data["IsSum"] = self.check_if_cost_item_is_sum(cost_item) + json_data.append(data) + for rel in cost_item.IsNestedBy or []: + for sub_cost in rel.RelatedObjects: + self.extract_cost_item_data(sub_cost, data["IsNestedBy"]) + + def check_if_cost_item_is_sum(self, cost_item: ifcopenshell.entity_instance) -> bool: + cost_values = [] + if cost_item.is_a("IfcCostItem"): + cost_values = cost_item.CostValues + elif cost_item.is_a("IfcCostValue"): + cost_values = cost_item.Components + for cost_value in cost_values or []: + if cost_value.Category == "*": + return True + return False + + def populate_nesting_index(self, cost_item, data): + data["NestingIndex"] = None + for rel in cost_item.Nests or []: + data["NestingIndex"] = rel.RelatedObjects.index(cost_item) + + def populate_cost_values(self, root_element, data): + # data["CostValues"] = [] + data["CategoryValues"] = {} + data["UnitBasisValueComponent"] = None + data["UnitBasisUnitSymbol"] = None + data["TotalAppliedValue"] = 0.0 + data["TotalCost"] = 0.0 + has_unit_basis = False + is_sum = False + if root_element.is_a("IfcCostItem"): + values = root_element.CostValues + elif root_element.is_a("IfcConstructionResource"): + values = root_element.BaseCosts + for cost_value in values or []: + self.extract_cost_value(root_element, data, cost_value) + # data["CostValues"].append(cost_value.id()) + data["TotalAppliedValue"] += self._cost_values[cost_value.id()]["AppliedValue"] + if cost_value.UnitBasis: + cost_value_data = self._cost_values[cost_value.id()] + data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] + data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] + has_unit_basis = True + else: + data["UnitBasisValueComponent"] = 1 + data["UnitBasisUnitSymbol"] = "U" + if cost_value.Category == "*": + is_sum = True + cost_quantity = 1 if data["TotalCostQuantity"] is None else data["TotalCostQuantity"] + if has_unit_basis: + data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"] + else: + data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity + if is_sum: + data["TotalAppliedValue"] = None + + def extract_cost_value(self, root_element, root_element_data, cost_value): + value_data = cost_value.get_info() + del value_data["AppliedValue"] + if value_data["UnitBasis"]: + data = cost_value.UnitBasis.get_info() + data["ValueComponent"] = data["ValueComponent"].wrappedValue + data["UnitComponent"] = data["UnitComponent"].id() + data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(cost_value.UnitBasis.UnitComponent) + value_data["UnitBasis"] = data + if value_data["ApplicableDate"]: + value_data["ApplicableDate"] = ifcopenshell.util.date.ifc2datetime(value_data["ApplicableDate"]) + if value_data["FixedUntilDate"]: + value_data["FixedUntilDate"] = ifcopenshell.util.date.ifc2datetime(value_data["FixedUntilDate"]) + value_data["Components"] = [c.id() for c in value_data["Components"] or []] + value_data["AppliedValue"] = ifcopenshell.util.cost.calculate_applied_value(root_element, cost_value) + + if cost_value.Category not in [None, "*"]: + root_element_data["CategoryValues"].setdefault(cost_value.Category, 0) + root_element_data["CategoryValues"][cost_value.Category] += value_data["AppliedValue"] + + value_data["Formula"] = ifcopenshell.util.cost.serialise_cost_value(cost_value) + + self._cost_values[cost_value.id()] = value_data + for component in cost_value.Components or []: + self.extract_cost_value(root_element, root_element_data, component) + + def extract_cost_item_quantities(self, cost_item, data): + data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item) + data["UnitSymbol"] = "-" + if cost_item.CostQuantities: + quantity = cost_item.CostQuantities[0] + data["QuantityType"] = quantity.is_a() + unit = ifcopenshell.util.unit.get_property_unit(quantity, self.file) + if unit: + data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) + if quantity.is_a("IfcPhysicalSimpleQuantity"): + measure_class = ( + quantity.wrapped_data.declaration() + .as_entity() + .attribute_by_index(3) + .type_of_attribute() + .declared_type() + .name() + ) + if "Count" in measure_class: + data["UnitSymbol"] = "U" \ No newline at end of file