diff --git a/src/components/assessment/AssessmentStepper.vue b/src/components/assessment/AssessmentStepper.vue index 1c415838..dd4eea3e 100644 --- a/src/components/assessment/AssessmentStepper.vue +++ b/src/components/assessment/AssessmentStepper.vue @@ -50,7 +50,7 @@ @@ -82,7 +83,7 @@ @@ -103,7 +104,6 @@ @@ -203,22 +203,24 @@ import { ReportPassdown, GroupedMaterial, SelectedMaterialEmit, + SelectedBuildupEmit, } from "@/models/newAssessment"; import { MaterialFull } from "@/store/utilities/material-carbon-factors"; +import { ReportFullGroup, ReportFullTransportGroup } from "@/models/report"; @Component({ components: { Menu1b, Menu2, Menu3, Menu4, Menu5, Menu6, Menu7 }, }) export default class AssessmentStepper extends Vue { @Prop() streams!: any; - @Prop() types!: MaterialGrouping[]; + @Prop() fullGroups!: ReportFullGroup[]; @Prop() materials!: MaterialFull[]; @Prop() transportTypes!: TransportType[]; @Prop() totalVolume!: number; @Prop() emptyProps!: EmptyPropsPassdown; @Prop() report!: ReportPassdown; @Prop() becs!: string; - @Prop() groupedMaterials!: GroupedMaterial[]; + @Prop() transportGroups!: ReportFullTransportGroup[]; @Prop() speckleVol!: boolean; @Prop() update!: boolean; @@ -296,6 +298,11 @@ export default class AssessmentStepper extends Vue { return this.report ? true : false; } + @Emit("selectBuildup") + selectBuildup(selectedBuildup: SelectedBuildupEmit) { + return selectedBuildup; + } + @Emit("createNewGroup") createNewGroup(groupName: string) { return groupName; @@ -357,10 +364,6 @@ export default class AssessmentStepper extends Vue { return objectGroup; } - @Emit("calcVol") - calcVol() { - return; - } @Watch("form.notes") notesUpdate() { this.uploadData({ diff --git a/src/components/assessment/ExpandedMaterialType.vue b/src/components/assessment/ExpandedMaterialType.vue new file mode 100644 index 00000000..afc42a73 --- /dev/null +++ b/src/components/assessment/ExpandedMaterialType.vue @@ -0,0 +1,187 @@ + + diff --git a/src/components/assessment/MaterialType.vue b/src/components/assessment/MaterialType.vue index 5855aa89..fb1ff373 100644 --- a/src/components/assessment/MaterialType.vue +++ b/src/components/assessment/MaterialType.vue @@ -9,17 +9,17 @@ ]" > - + - Objects: {{ type.ids.length }} + Objects: {{ type.objects.length }} - + {{ item.name }} + + + mdi-plus + +
diff --git a/src/components/assessment/Menu6.vue b/src/components/assessment/Menu6.vue index 7b63498e..acf2aac2 100644 --- a/src/components/assessment/Menu6.vue +++ b/src/components/assessment/Menu6.vue @@ -23,7 +23,7 @@ export default class Menu6 extends Vue { getReport(val: ReportPassdown) { if (val) { - const tonnesCO2 = Math.round(val.totals.totalCO2 * 0.001); + const tonnesCO2 = Math.round(val.totalCO2 * 0.001); return `${tonnesCO2} tonnes`; } } @@ -34,30 +34,30 @@ export default class Menu6 extends Vue { levels: [ { name: "A1-A3", - kgCO2e: this.report.totals.productStageCarbonA1A3, + kgCO2e: this.report.totalA1A3, tCO2e: Math.round( - this.report.totals.productStageCarbonA1A3 * 0.001 + this.report.totalA1A3 * 0.001 ), kgCO2eperm2: Math.round( - this.report.totals.productStageCarbonA1A3 / this.floorArea + this.report.totalA1A3 / this.floorArea ), }, { name: "A4", - kgCO2e: this.report.totals.transportCarbonA4, - tCO2e: Math.round(this.report.totals.transportCarbonA4 * 0.001), + kgCO2e: this.report.totalA4, + tCO2e: Math.round(this.report.totalA4 * 0.001), kgCO2eperm2: Math.round( - this.report.totals.transportCarbonA4 / this.floorArea + this.report.totalA4 / this.floorArea ), }, { name: "A5", - kgCO2e: this.report.totals.constructionCarbonA5.value, + kgCO2e: this.report.totalA5, tCO2e: Math.round( - this.report.totals.constructionCarbonA5.value * 0.001 + this.report.totalA5 * 0.001 ), kgCO2eperm2: Math.round( - this.report.totals.constructionCarbonA5.value / this.floorArea + this.report.totalA5 / this.floorArea ), }, ], diff --git a/src/components/assessment/TransportItem.vue b/src/components/assessment/TransportItem.vue index 8cc74390..bfa9ce8c 100644 --- a/src/components/assessment/TransportItem.vue +++ b/src/components/assessment/TransportItem.vue @@ -38,6 +38,7 @@ import { TransportSelected, TransportType, } from "@/models/newAssessment"; +import { ReportFullTransportGroup } from "@/models/report"; import { Vue, Prop, Watch, Component, Emit } from "vue-property-decorator"; type Selected = null | TransportType; @@ -45,11 +46,13 @@ type Selected = null | TransportType; @Component export default class Item extends Vue { @Prop() transportTypes!: TransportType[]; - @Prop() groupedMaterial!: GroupedMaterial; + @Prop() groupedMaterial!: ReportFullTransportGroup; selected: Selected = - this.groupedMaterial && this.groupedMaterial.transportType - ? this.groupedMaterial.transportType + this.groupedMaterial && + this.groupedMaterial.objects[0].hasTransport && + this.groupedMaterial.objects[0].transport + ? this.groupedMaterial.objects[0].transport : null; road = 0; rail = 0; @@ -58,8 +61,10 @@ export default class Item extends Vue { mounted() { this.selected = - this.groupedMaterial && this.groupedMaterial.transportType - ? this.groupedMaterial.transportType + this.groupedMaterial && + this.groupedMaterial.objects[0].hasTransport && + this.groupedMaterial.objects[0].transport + ? this.groupedMaterial.objects[0].transport : null; } @@ -116,7 +121,7 @@ export default class Item extends Vue { } get materialName() { - return this.groupedMaterial.material; + return this.groupedMaterial.name; } get saveDisabled() { diff --git a/src/components/core/VersionUpdateDialog.vue b/src/components/core/VersionUpdateDialog.vue index bb2b998c..cc5d392e 100644 --- a/src/components/core/VersionUpdateDialog.vue +++ b/src/components/core/VersionUpdateDialog.vue @@ -56,15 +56,24 @@ export default class VersionUpdateDialog extends Vue { @Prop() dialog!: boolean; @Prop() version!: string; - features: { title: string; subtitle: string }[] = [{ - title: "Updated Speckle Viewer", - subtitle: "Updated the version of the Speckle Viewer to get the latest features" - }]; + features: { title: string; subtitle: string }[] = [ + { + title: "Material buildup", + subtitle: + "Objects in assessments can now have multiple materials assigned to them, allowing for material buildups to be used instead of single materials", + }, + { + title: "Grouping filters update", + subtitle: + "Grouping filters that require it now have an 'undefined' field, meaning that a filter will not need to appear on every object to allow it to be used for object grouping", + }, + ]; fixes: { title: string; subtitle: string }[] = [ { - title: "Fixed bug in report models", - subtitle: "Fixed a bug that would cause some models to not be saved properly in reports", + title: "Large models bug", + subtitle: + "Fixed a bug causing an 'out of memory' error to sometimes occur when loading large models", }, ]; diff --git a/src/models/newAssessment/index.ts b/src/models/newAssessment/index.ts index 4929f3ff..f563554d 100644 --- a/src/models/newAssessment/index.ts +++ b/src/models/newAssessment/index.ts @@ -9,5 +9,5 @@ export { ProjectDataTemp, ProjectDataComplete } from "./projectData.interface"; export { EmptyProps, EmptyPropsPassdown } from "./emptyProps.interface"; export { ObjectDetails } from "./objectDetails.interface"; export { GroupedMaterial } from "./groupedMaterial.interface"; -export { SelectedMaterialEmit } from "./selectedMaterialEmit.interface"; +export * from "./selectedMaterialEmit.interface"; export * from "./stringPropertyGroups.interface"; diff --git a/src/models/newAssessment/materialUpdateOut.interface.ts b/src/models/newAssessment/materialUpdateOut.interface.ts index d6ca2bbd..750fafa9 100644 --- a/src/models/newAssessment/materialUpdateOut.interface.ts +++ b/src/models/newAssessment/materialUpdateOut.interface.ts @@ -1,7 +1,8 @@ import { MaterialFull } from "@/store/utilities/material-carbon-factors"; -import { MaterialGrouping } from "."; +import { ReportFullGroup } from "../report"; export interface MaterialUpdateOut { material: MaterialFull; - type: MaterialGrouping; + oldMaterial?: MaterialFull; + type: ReportFullGroup; } diff --git a/src/models/newAssessment/selectedMaterialEmit.interface.ts b/src/models/newAssessment/selectedMaterialEmit.interface.ts index 7d19c0c6..8348bbdd 100644 --- a/src/models/newAssessment/selectedMaterialEmit.interface.ts +++ b/src/models/newAssessment/selectedMaterialEmit.interface.ts @@ -1,4 +1,17 @@ +import { MaterialFull } from "@/store/utilities/material-carbon-factors"; + export interface SelectedMaterialEmit { - ids: string[]; - type: string; -}; + ids: string[]; + type: string; +} + +export interface PartMaterial { + id: number; + material?: MaterialFull; + percentage?: string; // will always be a number, but the input gives it as a string, so it's a string here to remind to do the conversion +} + +export interface SelectedBuildupEmit { + ids: string[]; + materials: PartMaterial[]; +} diff --git a/src/models/newAssessment/speckleObject.interface.ts b/src/models/newAssessment/speckleObject.interface.ts index 281090ff..a2484e55 100644 --- a/src/models/newAssessment/speckleObject.interface.ts +++ b/src/models/newAssessment/speckleObject.interface.ts @@ -1,35 +1,11 @@ import { MaterialFull } from "@/store/utilities/material-carbon-factors"; import { TransportType } from "."; import { ChartData } from "../chart"; -import { IABreakdown } from "@/views/utils/process-report-object"; import { Color } from "../renderer"; -export type ObjectsObj = { [id: string]: SpeckleObject }; - -export interface SpeckleObject { - id: string; - speckle_type: string; - formData?: ObjectFormData; - reportData?: ReportDataChild; -} - -export interface ObjectFormData { - transport?: TransportType; - material?: MaterialFull; - volume?: number; -} - -// repeat of SpeckleObject, but forces formData to be present -export interface SpeckleObjectFormComplete { - id: string; - speckle_type: string; - formData: ObjectFormDataComplete; - reportData?: ReportDataChild; -} - export interface ObjectFormDataComplete { - transport: TransportType; - material: MaterialFull; + transport: TransportType | TransportType[]; // single object only used in legacy reports + material: MaterialFull | MaterialFull[];// single object only used in legacy reports volume: number; } @@ -42,8 +18,10 @@ export interface SpeckleObjectComplete { } export interface ReportProp { - reportObjs: SpeckleObjectComplete[]; - totals: ReportTotals; + totalCO2: number; + totalA1A3: number; + totalA4: number; + totalA5: number; } export type ReportPassdown = ReportProp | false; diff --git a/src/models/newAssessment/transportSelected.interface.ts b/src/models/newAssessment/transportSelected.interface.ts index e12be49b..9390ca29 100644 --- a/src/models/newAssessment/transportSelected.interface.ts +++ b/src/models/newAssessment/transportSelected.interface.ts @@ -1,6 +1,7 @@ -import { GroupedMaterial, TransportType } from "."; +import { TransportType } from "."; +import { ReportFullTransportGroup } from "../report"; export interface TransportSelected { - material: GroupedMaterial; + material: ReportFullTransportGroup; transportType: TransportType; } diff --git a/src/models/report/index.ts b/src/models/report/index.ts new file mode 100644 index 00000000..784ea193 --- /dev/null +++ b/src/models/report/index.ts @@ -0,0 +1,588 @@ +import { MaterialFull } from "@/store/utilities/material-carbon-factors"; +import { + EmptyProps, + ProjectDataComplete, + ReportProp, + ReportTotals, + SpeckleObjectComplete, + StringPropertyGroups, + TransportType, +} from "../newAssessment"; +import { IChildObject, Param } from "@/views/utils/add-params/addParams"; +import * as AddParams from "@/views/utils/add-params/addParams"; +import { Color } from "../renderer"; +import { ChartData } from "../chart"; +import { ChildSpeckleObjectData } from "../graphql"; + +interface ObjectMaterials { + [materialName: string]: ReportMaterial; +} +interface ReportObjects { + [id: string]: ReportObject; +} +export interface ReportFullGroup { + name: string; + objects: ReportObject[]; +} +export interface ReportFullTransportGroup { + name: string; + objects: ReportMaterial[]; +} + +export interface ChildObjects { + speckleObject: IChildObject; + volume: number; +} + +interface ReportToUpload { + reportObjs: SpeckleObjectComplete[]; + totals: ReportTotals; +} +interface ReportControllerGroups { + [groupValue: string]: string[]; +} + +export class ReportController { + objects: ReportObjects = {}; + groups: ReportControllerGroups = {}; + projectInfo: ProjectDataComplete = {} as ProjectDataComplete; + + totalCarbon = 0; + totalA1A3 = 0; + totalA4 = 0; + totalA5w = 0; + totalA5a = 0; // A5a only has one component (sys cost), but keeping "total" naming scheme for consistency + totalA5 = 0; + + addParams: AddParams.ParamAdd[] = []; + + materials: ChartData[] = []; + reportObjs: SpeckleObjectComplete[] = []; + + convToUpload( + volume: number, + materialsColors: Color[], + transportColors: Color[] + ): ReportToUpload { + const totals: ReportTotals = { + totalCO2: this.totalCarbon, + volume, + materials: this.materials, + materialsColors, + transportColors, + transportCarbonA4: this.totalA4, + productStageCarbonA1A3: this.totalA1A3, + constructionCarbonA5: { + value: this.totalA5, + waste: this.totalA5w, + site: this.totalA5a, + }, + }; + + return { + totals, + reportObjs: this.reportObjs, + }; + } + + get materialsColors(): Color[] { + return Object.entries(this.objects).map(([k, v]) => ({ + id: v.id, + color: v.objectMaterialColor, + })); + } + get transportColors(): Color[] { + return Object.entries(this.objects).map(([k, v]) => ({ + id: v.id, + color: v.objectTransportColor, + })); + } + + getObjectsByIds(ids: string[]) { + return ids.map(i => this.objects[i]); + } + + calcCarbon(): ReportProp { + this.totalA5a = this.calcA5a(); + + this.totalCarbon = 0; + this.totalA1A3 = 0; + this.totalA4 = 0; + this.totalA5w = 0; + + const materialsObj: { + [key: string]: { value: number; color: string }; + } = {}; + this.reportObjs = []; + + Object.entries(this.objects).forEach(([k, v]) => { + const { A1A3, A4, A5w } = v.calcCarbon(); + + this.totalA1A3 += A1A3; + this.totalA4 += A4; + this.totalA5w += A5w; + + // update materialsObj + Object.entries(v.materials).forEach(([k1, v1]) => { + if (materialsObj[k1]) materialsObj[k1].value += v1.totalCarbon; + else + materialsObj[k1] = { + color: v1.material.color, + value: v1.totalCarbon, + }; + }); + + // update reportObjs + this.reportObjs.push(v.convToSpeckleObjectComplete()); + + // make parameter update object + this.addParams.push({ + parentid: v.id, + name: "Total Carbon", + param: { + id: Math.floor(Math.random() * 10000000).toString(), + name: "Total Carbon", + units: "kgCO2e/kg", + value: A4 + A1A3 + A5w, + isShared: false, + isReadOnly: false, + speckle_type: "Objects.BuiltElements.Revit.Parameter", + applicationId: null, + applicationUnit: null, + isTypeParameter: false, + totalChildrenCount: 0, + applicationUnitType: "string", + applicationInternalName: "string", + }, + }); + this.addParams.push({ + parentid: v.id, + name: "Product Stage Carbon A1-A3", + param: { + id: Math.floor(Math.random() * 10000000).toString(), + name: "Product Stage Carbon A1-A3", + units: "kgCO2e/kg", + value: A1A3, + isShared: false, + isReadOnly: false, + speckle_type: "Objects.BuiltElements.Revit.Parameter", + applicationId: null, + applicationUnit: null, + isTypeParameter: false, + totalChildrenCount: 0, + applicationUnitType: "string", + applicationInternalName: "string", + }, + }); + this.addParams.push({ + parentid: v.id, + name: "Transport Carbon A4", + param: { + id: Math.floor(Math.random() * 10000000).toString(), + name: "Transport Carbon A4", + units: "kgCO2e/kg", + value: A4, + isShared: false, + isReadOnly: false, + speckle_type: "Objects.BuiltElements.Revit.Parameter", + applicationId: null, + applicationUnit: null, + isTypeParameter: false, + totalChildrenCount: 0, + applicationUnitType: "string", + applicationInternalName: "string", + }, + }); + this.addParams.push({ + parentid: v.id, + name: "Construction Carbon A5", + param: { + id: Math.floor(Math.random() * 10000000).toString(), + name: "Construction Carbon A5", + units: "kgCO2e/kg", + value: A5w, + isShared: false, + isReadOnly: false, + speckle_type: "Objects.BuiltElements.Revit.Parameter", + applicationId: null, + applicationUnit: null, + isTypeParameter: false, + totalChildrenCount: 0, + applicationUnitType: "string", + applicationInternalName: "string", + }, + }); + // end parameter update + }); + this.materials = Object.entries(materialsObj).map(([k, v]) => ({ + value: v.value, + label: k, + color: v.color, + })); + + this.totalA5 = this.totalA5w + this.totalA5a; + this.totalCarbon = this.totalA1A3 + this.totalA4 + this.totalA5; + + return { + totalCO2: this.totalCarbon, + totalA1A3: this.totalA1A3, + totalA4: this.totalA4, + totalA5: this.totalA5, + }; + } + + calcA5a() { + return (this.projectInfo.cost * 1400) / 100000; + } + + isReportComplete(): EmptyProps { + const projectEmpty: boolean = + this.projectInfo && this.projectInfo.name !== undefined; // all project info gets filled in at the same time, so if one contains a value, then they all will + const materialsEmpty = Object.entries(this.objects) + .filter(([k, v]) => !v.hasMaterials) + .map(([k, v]) => v.id); + const transportsEmpty: string[] = []; + + Object.entries(this.objects).forEach(([k, v]) => { + if (v.hasMaterials) { + Object.entries(v.materials).forEach(([k1, v1]) => { + if (!v1.hasTransport) transportsEmpty.push(v.id); + }); + } + }); + const volumesEmpty: string[] = []; + + return { + projectEmpty, + materialsEmpty, + transportsEmpty, + volumesEmpty, + }; + } + + get fullGroups(): ReportFullGroup[] { + let res = Object.keys(this.groups).map((g) => ({ + name: g, + objects: this.groups[g].map((id) => this.objects[id]), + })); + return res; + } + + get transportGroups(): ReportFullTransportGroup[] { + const materialGrouping: { [materialName: string]: ReportMaterial[] } = {}; + Object.keys(this.objects).forEach((k) => { + const obj = this.objects[k]; + if (obj.hasMaterials) { + Object.entries(obj.materials).forEach(([k, v]) => { + if (materialGrouping[k]) materialGrouping[k].push(v); + else materialGrouping[k] = [v]; + }); + } + }); + return Object.entries(materialGrouping).map(([k, v]) => ({ + name: k, + objects: v, + })); + } + + get hasProjectInfo() { + return this.projectInfo && this.projectInfo.cost; + } + + // method used on new assessment page when updating an existing report + setObjectsUpdate(childObjects: ChildSpeckleObjectData[]) { + childObjects.forEach((c) => { + const newObj = new ReportObject( + c.act.id, + c.speckleType, + c.act.formData.volume + ); + newObj.updateUsingReport(c); + + this.objects[c.act.id] = newObj; + }); + } + + setObjects(childObjects: ChildObjects[]) { + childObjects.forEach((c) => { + const speckleObject = c.speckleObject; + this.objects[speckleObject.id] = new ReportObject( + speckleObject.id, + speckleObject.speckle_type, + c.volume + ); + }); + } + + addNewGroup(name: string, selectedObjects: string[]) { + // remove selected objects from their current group + const oldGroups: ReportControllerGroups = JSON.parse( + JSON.stringify(this.groups) + ); + + this.groups = {}; + + Object.entries(oldGroups).forEach(([k, v]) => { + this.groups[k] = v.filter((v) => !selectedObjects.includes(v)); + }); + + // add new group with the selected object id's + this.groups[name] = selectedObjects; + } + + groupObjects(propertyGroups: StringPropertyGroups[], selectedGroup: string) { + this.groups = {}; + const group = propertyGroups.find((pg) => pg.name === selectedGroup); + if (group) { + group.data.valueGroups.forEach((vg) => { + if (this.groups[vg.value]) this.groups[vg.value].push(...vg.ids); + else this.groups[vg.value] = [...vg.ids]; + }); + } + } +} + +interface CarbonResults { + A1A3: number; + A4: number; + A5w: number; +} + +export class ReportObject { + constructor( + public id: string, + public speckle_type: string, + public volume: number + ) {} + + materials: ObjectMaterials = {}; + objectMaterialColor = ""; // object will just have one material colour, even if it has multiple materials assigned to it + objectTransportColor = ""; // object will just have one transport colour, even if it has multiple transports assigned to it + + totalCarbon = 0; + totalA1A3 = 0; + totalA4 = 0; + totalA5w = 0; + + get hasMaterials() { + return Object.keys(this.materials).length > 0; + } + + updateUsingReport(c: ChildSpeckleObjectData) { + // add materials + // add transport to materials + const material = c.act.formData.material; + if (Array.isArray(material)) { + const transport = c.act.formData.transport as TransportType[]; // if material is an array then we know that transport will be too + // just make the default colour the first material/transport colour + this.objectMaterialColor = material[0].color; + this.objectTransportColor = transport[0].color; + material.forEach((m, i) => { + if (m.volume) + // we can be _pretty_ certain that m.volume exists (hopefully) + this.addMaterial(m, m.volume / this.volume); + this.setTransport(m.name, transport[i]); // transport and materials should have been added to their arrays in the same order, so index should link the two + }); + } else { + this.objectMaterialColor = material.color; + this.objectTransportColor = ( + c.act.formData.transport as TransportType + ).color; // if material is not an array then we know that transport won't be either + this.addMaterial(material, 1); + this.setTransport( + material.name, + c.act.formData.transport as TransportType + ); // if material is not an array then we know that transport won't be either + } + // pull in carbon values + const reportData = c.act.reportData; + this.totalCarbon = reportData.totalCarbon; + this.totalA1A3 = reportData.productStageCarbonA1A3; + this.totalA4 = reportData.transportCarbonA4; + this.totalA5w = reportData.constructionCarbonA5.waste; + } + + convToSpeckleObjectComplete(): SpeckleObjectComplete { + return { + id: this.id, + speckle_type: this.speckle_type, + formData: { + transport: Object.entries(this.materials).map(([k, v]) => v.transport), + material: Object.entries(this.materials).map(([k, v]) => v.material), + volume: this.volume, + }, + reportData: { + totalCarbon: this.totalCarbon, + transportCarbonA4: this.totalA4, + productStageCarbonA1A3: this.totalA1A3, + constructionCarbonA5: { + value: this.totalA5w, + site: 0, + waste: this.totalA5w, + }, + }, + }; + } + + setTransport(materialName: string, transportType: TransportType) { + this.materials[materialName].setTransport(transportType); + } + + /** + * + * @param material + * @param percentage should be in decimal format + */ + addMaterial(material: MaterialFull, percentage: number) { + this.materials[material.name] = new ReportMaterial( + this.volume * percentage, + JSON.parse(JSON.stringify(material)), // make sure to do a deep copy of material otherwise problems pop up + this.id + ); + } + updateMaterialVolume(materialName: string, percentage: number) { + this.materials[materialName].volume = this.volume * percentage; + } + changeMaterial(oldName: string | undefined, newMaterial: MaterialFull) { + let percentage = 1; + if (oldName) { + percentage = this.materials[oldName].volume / this.volume; + this.removeMaterial(oldName); + } + this.addMaterial(JSON.parse(JSON.stringify(newMaterial)), percentage); // make sure to do a deep copy of material otherwise problems pop up + } + removeAllMaterials() { + this.materials = {}; + } + removeMaterial(materialName: string) { + const oldMaterials = this.materials; + this.materials = {}; + Object.keys(oldMaterials).forEach((m) => { + if (m !== materialName) this.materials[m] = oldMaterials[m]; + }); + } + + calcCarbon(): CarbonResults { + this.totalCarbon = 0; + this.totalA1A3 = 0; + this.totalA4 = 0; + this.totalA5w = 0; + + Object.entries(this.materials).forEach(([k, v]) => { + const { A1A3, A4, A5w } = v.calcCarbon(); + + this.totalA1A3 += A1A3; + this.totalA4 += A4; + this.totalA5w += A5w; + }); + + this.totalCarbon = this.totalA1A3 + this.totalA4 + this.totalA5w; + + return { + A1A3: this.totalA1A3, + A4: this.totalA4, + A5w: this.totalA5w, + }; + } +} + +export class ReportMaterial { + constructor( + public volume: number, + public material: MaterialFull, + public parentId: string + ) { + this.material.volume = this.volume; + } + transport: TransportType = {} as TransportType; + + totalCarbon = 0; + A1A3 = 0; + A4 = 0; + A5w = 0; + + get hasTransport() { + return this.transport && this.transport.name; + } + + setTransport(transport: TransportType) { + this.transport = transport; + } + setMaterial(material: MaterialFull) { + this.material = material; + this.material.volume = this.volume; + } + + calcCarbon(): CarbonResults { + this.A1A3 = this.calcA1A3(this.volume, this.material); + this.A4 = this.calcA4( + this.volume, + this.material, + this.transport, + transportFactors + ); + this.A5w = this.calcA5w( + this.volume, + this.material, + this.transport, + transportFactors + ); + + this.totalCarbon = this.A1A3 + this.A4 + this.A5w; + + return { + A1A3: this.A1A3, + A4: this.A4, + A5w: this.A5w, + }; + } + + calcA1A3(volume: number, material: MaterialFull) { + const mass = volume * material.density; + const A1A3 = mass * material.productStageCarbonA1A3; + + return A1A3; + } + calcA4( + volume: number, + material: MaterialFull, + transport: TransportType, + factors: TransportFactors + ) { + const A4 = + (material.density * + volume * + (transport.values.road * factors.road + + transport.values.sea * factors.sea)) / + 1000; + + return A4; + } + // TODO: check whether we should be including C2-C4 (estimates/assumptions) in this calc? + // How to Calculate Embodied Carbon does say to use it (and gives assumptions to use given a lack of data), however DesignCheck does not use them + calcA5w( + volume: number, + material: MaterialFull, + transport: TransportType, + factors: TransportFactors + ) { + const wasteVolume = volume * (1 / (1 - material.wastage) - 1); + + const A1A3 = this.calcA1A3(wasteVolume, material); + const A4 = this.calcA4(wasteVolume, material, transport, factors); + + const A5w = A1A3 + A4; + + return A5w; + } +} + +interface TransportFactors { + road: number; + sea: number; +} +const transportFactors: TransportFactors = { + // values taken from RICS guidance + road: 0.1136, // gCO2/kg/km + sea: 0.016143, // gCO2/kg/km +}; diff --git a/src/store/index.ts b/src/store/index.ts index 3f508d23..a6b61067 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -53,7 +53,7 @@ Vue.use(Vuex); export default new Vuex.Store({ state: { - version: "0.11.5 \u00DF", + version: "0.12.2 \u00DF", speckleFolderName: "actcarbonreport", speckleViewer: { viewer: undefined, @@ -133,27 +133,6 @@ export default new Vuex.Store({ backgroundColor: "#82c7f1", }, ], - materialCategories: [ - "Aluminium", - "Brick", - "Blockwork", - "Cement", - "Coating", - "Concrete", - "Copper", - "Fire", - "Fill Materials", - "Glass", - "Gypsum", - "Insulation", - "Natural materials", - "Plasterboard", - "Plastic", - "Soil", - "Steel", - "Stone", - "Timber", - ], transportTypes: [ { name: "local", diff --git a/src/store/utilities/carbonCalculator.ts b/src/store/utilities/carbonCalculator.ts deleted file mode 100644 index 835d35b6..00000000 --- a/src/store/utilities/carbonCalculator.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { SpeckleObjectFormComplete } from "@/models/newAssessment"; - -// TODO: MOVE THESE INTO A MIXIN? - -const transportFactors = { - // values taken from RICS guidance - road: 0.1136, // gCO2/kg/km - sea: 0.016143, // gCO2/kg/km -}; - -// CALCULATIONS -// calculate the a1a3 carbon for any speckle object against a specified material -export function productStageCarbonA1A3(obj: SpeckleObjectFormComplete) { - // calculate mass of object - const mass = obj.formData.volume * obj.formData.material.density; - // calculate a1a3 carbon of object - const a1a3 = mass * obj.formData.material.productStageCarbonA1A3; - return a1a3; -} - -// calculate the carbon associated with transport -export function transportCarbonA4(obj: SpeckleObjectFormComplete) { - const factors = transportFactors; - - const trans = obj.formData.transport.values; - - const transCarb = - (obj.formData.material.density * - obj.formData.volume * - (trans.road * factors.road + trans.sea * factors.sea)) / - 1000; - return transCarb; -} - -// calculate the carbon associated with site activities -export function constructionCarbonA5Site(sysCost: number) { - const a5Site = (sysCost * 1400) / 100000; - return a5Site; -} - -// calculate the carbon associated with material wastage -export function constructionCarbonA5Waste(obj: SpeckleObjectFormComplete) { - const wasteVolume = - obj.formData.volume * (1 / (1 - obj.formData.material.wastage) - 1); - - // create new object with waste volume - // const wasteObj = obj; - const wasteObj: SpeckleObjectFormComplete = { - ...obj, - formData: { - ...obj.formData, - volume: wasteVolume, - }, - }; - wasteObj.formData.volume = wasteVolume; - - // compute a1-a4 for waste materials - const a1a3 = productStageCarbonA1A3(wasteObj); - const a4 = transportCarbonA4(wasteObj); - - const a5waste = a1a3 + a4; - - return a5waste; -} - -type FullA5Calc = { - sysCost: number; - obj: SpeckleObjectFormComplete; -}; -type PartialA5Calc = { - site: number; - waste: number; -}; - -function instanceOfFullA5Calc(object: any): object is FullA5Calc { - return object && "sysCost" in object; -} - -export function constructionCarbonA5(ops: FullA5Calc | PartialA5Calc) { - let a5: number; - if (instanceOfFullA5Calc(ops)) { - a5 = - constructionCarbonA5Site(ops.sysCost) + - constructionCarbonA5Waste(ops.obj); - } else { - a5 = ops.site + ops.waste; - } - - return a5; -} diff --git a/src/store/utilities/material-carbon-factors.ts b/src/store/utilities/material-carbon-factors.ts index 52c7f99c..ebde0d76 100644 --- a/src/store/utilities/material-carbon-factors.ts +++ b/src/store/utilities/material-carbon-factors.ts @@ -1,6 +1,11 @@ +export function instanceOfMaterialFull(object: any): object is MaterialFull { + return "name" in object && "color" in object && "productStageCarbonA1A3" in object; +} + export interface MaterialFull extends Material { name: string; color: string; // will be a color hex code + volume?: number; // will only be used once uploaded to report } export interface Material { diff --git a/src/views/Assessment.vue b/src/views/Assessment.vue index ed2d79b9..6a1c873d 100644 --- a/src/views/Assessment.vue +++ b/src/views/Assessment.vue @@ -17,21 +17,21 @@ @uploadData="uploadData" @selectMaterial="selectMaterial" @checkSave="checkSave" - @calcVol="calcVol" @close="close" @openFullView="openFullView" @createNewGroup="createNewObjectGroup" @groupSelected="materialGroupSelected" + @selectBuildup="selectBuildup" :modal="modal" :streams="availableStreams" - :types="types" + :fullGroups="fullGroups" :materials="materials" :transportTypes="transportTypes" :totalVolume="totalVolume" :emptyProps="emptyProps" :report="report" :becs="becs" - :groupedMaterials="groupedMaterials" + :transportGroups="reportController.transportGroups" :speckleVol="speckleVol" :update="update" :streamId="streamId" @@ -104,32 +104,18 @@ import { import { ProjectDataComplete, MaterialUpdateOut, - MaterialGrouping, Step, TransportSelected, TransportType, - EmptyProps, EmptyPropsPassdown, - SpeckleObjectFormComplete, - SpeckleObjectComplete, - ReportTotals, ReportPassdown, ObjectDetails, - GroupedMaterial, SelectedMaterialEmit, - ObjectsObj, StringPropertyGroups, + SelectedBuildupEmit, } from "@/models/newAssessment"; import { MaterialFull } from "@/store/utilities/material-carbon-factors"; -import * as THREE from "three"; -import { - constructionCarbonA5, - constructionCarbonA5Site, - constructionCarbonA5Waste, - productStageCarbonA1A3, - transportCarbonA4, -} from "@/store/utilities/carbonCalculator"; import { CheckContainsChlidReportInput, GetAllReportBranchesOutput, @@ -137,10 +123,10 @@ import { UploadReportInput, GetObjectDetailsOut, } from "@/store"; -import { VolCalculator } from "./utils/VolCalculator"; +import { BECName } from "@/models/shared"; import { LoadStreamOut } from "./utils/process-report-object"; import LoadingSpinner from "@/components/shared/LoadingSpinner.vue"; -import { ChartData } from "@/models/chart"; +import { ChildObjects, ReportController } from "@/models/report"; interface AvailableStream { label: string; @@ -161,6 +147,10 @@ export default class Assessment extends Vue { @Prop() modalStreamid!: string; @Prop() modalBranchName!: string; + reportController = new ReportController(); + + allStreamObjs: GetObjectDetailsOut; + loading = false; loadingSpinnerText = ""; loadingModel = false; @@ -170,14 +160,17 @@ export default class Assessment extends Vue { availableStreams: AvailableStream[] = []; objectURLs: string[] = []; token = ""; - types: MaterialGrouping[] = []; - objectsObj: ObjectsObj = {}; + get fullGroups() { + return this.reportController.fullGroups; + } materials: MaterialFull[] = this.$store.getters.materialsArr; transportTypes: TransportType[] = []; - becs: TransportType[] = []; + becs: { + name: BECName; + color: string; + backgroundColor: string; + }[] = []; totalVolume = -1; - allMesh: THREE.Mesh[] = []; - projectData!: ProjectDataComplete; projectDataPassdown: ProjectDataComplete | null = null; allIds: string[] = []; @@ -186,9 +179,6 @@ export default class Assessment extends Vue { emptyProps: EmptyPropsPassdown = false; // setting to false initially to get vue to detect changes report: ReportPassdown = false; - addParams: AddParams.ParamAdd[] = []; - allChildObjs: AddParams.IChildObject[] = []; - parentObj: AddParams.IParamsParent | null = null; streamId = ""; colors: Color[] = []; @@ -208,7 +198,9 @@ export default class Assessment extends Vue { groupingProps: StringPropertyGroups[] = []; objectGroups: string[] = []; - groupedMaterials: GroupedMaterial[] = []; + get transportGroups() { + return this.reportController.transportGroups; + } selectedObjectGroup = "Object Type"; update = false; @@ -258,7 +250,7 @@ export default class Assessment extends Vue { } materialGroupSelected(objectGroup: string) { - this.types = this.findTypes(this.groupingProps, objectGroup); + this.reportController.groupObjects(this.groupingProps, objectGroup); this.selectedObjectGroup = objectGroup; this.resetColors(); } @@ -299,28 +291,13 @@ export default class Assessment extends Vue { const objGroup = assessmentViewData.data.selectedObjectGroup; this.selectedObjectGroup = objGroup ? objGroup : "Object Type"; - assessmentViewData.data.children.forEach((c) => { - this.objectsObj[c.act.id] = { - id: c.act.id, - speckle_type: c.act.speckle_type, - formData: c.act.formData, - reportData: c.act.reportData, - }; - }); - - this.materialsColors = Object.values(this.objectsObj).map((o) => ({ - id: o.id, - color: o.formData?.material?.color as string, - })); - this.transportColors = Object.values(this.objectsObj).map((o) => ({ - id: o.id, - color: o.formData?.transport?.color as string, - })); + this.reportController.setObjectsUpdate(assessmentViewData.data.children); - this.projectData = assessmentViewData.data.projectInfo; - this.projectDataPassdown = assessmentViewData.data.projectInfo; + this.materialsColors = this.reportController.materialsColors; + this.transportColors = this.reportController.transportColors; - this.groupMaterials(); + this.reportController.projectInfo = assessmentViewData.data.projectInfo; + this.projectDataPassdown = this.reportController.projectInfo; this.totalVolume = assessmentViewData.data.projectInfo.volume; this.speckleVol = true; @@ -330,59 +307,51 @@ export default class Assessment extends Vue { async checkSave() { this.loading = true; - if (this.report) { - if (this.report.reportObjs.length > 0) { - const containsReport: boolean = await this.$store.dispatch( - "checkContainsReport", - this.streamId - ); - if (containsReport) { - const getAllReportBranchesOut: GetAllReportBranchesOutput = - await this.$store.dispatch("getAllReportBranches", this.streamId); - this.branchNames = getAllReportBranchesOut.map((b) => b.name); - this.reportName = this.projectData.name; - this.newBranchDialog = true; - } else { - this.uploadReport("main"); - } - } else { - this.saveSnack = true; - this.saveSuccess = false; - this.loading = false; - } + const containsReport: boolean = await this.$store.dispatch( + "checkContainsReport", + this.streamId + ); + if (containsReport) { + const getAllReportBranchesOut: GetAllReportBranchesOutput = + await this.$store.dispatch("getAllReportBranches", this.streamId); + this.branchNames = getAllReportBranchesOut.map((b) => b.name); + this.reportName = this.reportController.projectInfo.name; + this.newBranchDialog = true; + } else { + this.uploadReport("main"); } } async uploadReport(branchName: string) { - if (this.report && this.report.reportObjs.length > 0) { - this.loadingSpinnerText = "DO NOT REFRESH. Saving report"; - let newModel: AddParams.AddParamsModel | undefined; - if (this.parentObj) { - newModel = await AddParams.addParams( - this.parentObj, - this.addParams, - this.$store.state.selectedServer.url, - this.token, - this.streamId, - this.allChildObjs - ); - } - - const uploadReportInput: UploadReportInput = { - streamid: this.streamId, - objects: this.report.reportObjs, - reportTotals: this.report.totals, - projectData: this.projectData, - branchName, - newModel, - selectedObjectGroup: this.selectedObjectGroup, - }; - this.loading = true; - await this.$store.dispatch("uploadReport", uploadReportInput); - this.loading = false; - this.saveSnack = true; - this.finished = true; - this.$router.push(`/assessment/view/${this.streamId}/${branchName}`); + this.loadingSpinnerText = "DO NOT REFRESH. Saving report"; + let newModel: AddParams.AddParamsModel | undefined; + if (this.allStreamObjs.parent) { + newModel = await AddParams.addParams( + this.allStreamObjs.parent, + this.reportController.addParams, + this.$store.state.selectedServer.url, + this.token, + this.streamId, + this.allStreamObjs.children + ); } + + const upload = this.reportController.convToUpload( + this.totalVolume, + this.materialsColors, + this.transportColors + ); + const uploadReportInput: UploadReportInput = { + streamid: this.streamId, + objects: upload.reportObjs, + reportTotals: upload.totals, + projectData: this.reportController.projectInfo, + branchName, + newModel, + selectedObjectGroup: this.selectedObjectGroup, + }; + this.loading = true; + await this.$store.dispatch("uploadReport", uploadReportInput); + this.$router.push(`/assessment/view/${this.streamId}/${branchName}`); } saveSnackClose() { this.saveSnack = false; @@ -399,9 +368,8 @@ export default class Assessment extends Vue { this.filteredType = material.type; } - async rendererLoaded({ properties, allMesh }: RendererLoaded) { + async rendererLoaded({ properties }: RendererLoaded) { this.loadingModelText = "Loading data from model..."; - this.allMesh = allMesh; const volumeFilter = properties.find( (p) => p.name.toLowerCase() === "volume" @@ -416,36 +384,29 @@ export default class Assessment extends Vue { } ); - this.allChildObjs = res.children; - this.parentObj = res.parent; + this.allStreamObjs = res; let totalVol = 0; - const filteredRes = this.allChildObjs.filter( + const filteredRes = this.allStreamObjs.children.filter( (r) => r.speckle_type !== "Speckle.Core.Models.DataChunk" && r.speckle_type !== "Objects.Geometry.Mesh" ); const speckleObjsPropsSearch: any[] = []; + const childObjs: ChildObjects[] = []; if (volumeFilter) { - const childObjects: ObjectDetails[] = []; this.speckleVol = true; filteredRes.forEach((r) => { const volume = this.findVolume(r, volumeFilter); if (volume) { speckleObjsPropsSearch.push(r); if (!this.update) { - this.objectsObj[r.id] = { - id: r.id, - speckle_type: r.speckle_type, - formData: { - volume: volume, - }, - }; - childObjects.push(r); - // also find total volume here to avoid needing to loop through objects again - totalVol += volume; + childObjs.push({ + volume, + speckleObject: r, + }); } // also find total volume here to avoid needing to loop through objects again totalVol += volume; @@ -453,28 +414,27 @@ export default class Assessment extends Vue { }); } else { this.speckleVol = false; - filteredRes.forEach((r) => { - if (!this.update) { - this.objectsObj[r.id] = { - id: r.id, - speckle_type: r.speckle_type, - }; - } - }); } + // if childObjs.length === 0 then the report is being updated, so no need to set objects here + if (childObjs.length !== 0) this.reportController.setObjects(childObjs); this.groupingProps = findStringProps( speckleObjsPropsSearch, - this.objectsObj + this.reportController.objects // technically not the right type, but it works so... ); this.objectGroups = this.groupingProps.map((gp) => gp.name); + this.reportController.groupObjects( + this.groupingProps, + this.selectedObjectGroup + ); - this.types = this.findTypes(this.groupingProps, this.selectedObjectGroup); + this.allIds = Object.keys(this.reportController.objects); if (!this.update) { - this.allIds = this.types.map((t) => t.ids).flat(); this.totalVolume = totalVol; } - this.updateVolumeGradient(); + this.volumeGradient = { + property: this.volProp, + }; this.resetColors(); this.loadingModel = false; } @@ -493,30 +453,6 @@ export default class Assessment extends Vue { return curr; } - calcVol() { - let totalVol = 0; - this.allMesh.forEach((m) => { - const volume = VolCalculator.getMeshVolume(m); - if (volume) { - try { - const id: string = m.userData.id; - this.objectsObj[id] = { - ...this.objectsObj[id], - formData: { - ...this.objectsObj[id].formData, - volume, - }, - }; - } catch (err) { - console.warn(err); - } - totalVol += volume; - } - }); - this.totalVolume = totalVol; - this.speckleVol = true; - } - stepperUpdate(step: Step) { this.step = step; switch (step) { @@ -531,7 +467,6 @@ export default class Assessment extends Vue { this.beenToTransport = true; this.resetColors(); this.colors = this.transportColors; - this.groupMaterials(); break; case Step.QUANTITIES: this.resetColors(); @@ -540,10 +475,10 @@ export default class Assessment extends Vue { break; case Step.REVIEW: this.resetColors(); - this.review(); + this.emptyProps = this.reportController.isReportComplete(); break; case Step.PREVIEW: - this.carbonCalc(); + this.report = this.reportController.calcCarbon(); this.resetColors(); break; case Step.SAVE: @@ -557,7 +492,7 @@ export default class Assessment extends Vue { objectsSelected(objects: UserData[]) { if (this.step === Step.MATERIALS) { - const keys = Object.keys(this.objectsObj); + const keys = Object.keys(this.reportController.objects); this.selectedObjects = objects .filter((o) => keys.includes(o.id)) .map((o) => o.id); @@ -567,69 +502,7 @@ export default class Assessment extends Vue { } createNewObjectGroup(name: string) { - this.types = this.types.map((t) => ({ - ...t, - ids: t.ids.filter((i) => !this.selectedObjects.includes(i)), - })); - this.types.push({ - ids: this.selectedObjects, - material: undefined, - transport: undefined, - type: name, - }); - } - - groupMaterials() { - let materialsObj: { - [material: string]: { - transportType?: TransportType; - speckle_types: { - [speckle_type: string]: string[] /* array should be object id's */; - }; - }; - } = {}; - - // assuming that the materials section has been filled out already - Object.values(this.objectsObj).forEach((o) => { - const material = o.formData?.material?.name; - const speckle_type = o.speckle_type; - if (material) { - if ( - materialsObj[material] && - materialsObj[material].speckle_types[speckle_type] - ) { - materialsObj[material].speckle_types[speckle_type].push(o.id); - } else if (materialsObj[material]) { - materialsObj[material].speckle_types[speckle_type] = [o.id]; - } else { - materialsObj[material] = { - speckle_types: {}, - transportType: o.formData?.transport, - }; - materialsObj[material].speckle_types[speckle_type] = [o.id]; - } - } - }); - - const materialsArr: GroupedMaterial[] = Object.keys(materialsObj).map( - (m) => { - const ids: string[] = []; - const speckle_types = Object.keys(materialsObj[m].speckle_types).map( - (s) => { - ids.push(...materialsObj[m].speckle_types[s]); - return s; - } - ); - return { - material: m, - objects: ids, - speckle_types, - transportType: materialsObj[m].transportType, - }; - } - ); - - this.groupedMaterials = materialsArr; + this.reportController.addNewGroup(name, this.selectedObjects); } resetColors() { @@ -637,236 +510,6 @@ export default class Assessment extends Vue { this.volumeGradientPassdown = null; } - updateVolumeGradient() { - let minVol = -1; - let maxVol = -1; - - Object.values(this.objectsObj).forEach((o, i) => { - if (o.formData?.volume) { - let volume = o.formData.volume; - if (i === 0) { - // if on the first index, then min and max have not yet been set, so set them to the first vol value - minVol = volume; - maxVol = volume; - } else { - if (minVol > volume) minVol = volume; - if (maxVol < volume) maxVol = volume; - } - } - }); - - this.volumeGradient = { - property: this.volProp, - }; - } - - // for now we're just assuming that all data is filled in if the user reaches this step TODO: ONLY ALLOW USER ON THIS PAGE IF REVIEW IS SUCCESSFUL - carbonCalc() { - // convert objects from SpeckleObject to SpeckleObjectFormComplete - const objs = this.convertToFormComplete(); - this.addParams = []; - - const reportObjs = objs.map((o): SpeckleObjectComplete => { - const A1A3 = productStageCarbonA1A3(o); - const A4 = transportCarbonA4(o); - const A5Waste = constructionCarbonA5Waste(o); - - // make parameter update object - this.addParams.push({ - parentid: o.id, - name: "Total Carbon", - param: { - id: Math.floor(Math.random() * 10000000).toString(), - name: "Total Carbon", - units: "kgCO2e/kg", - value: A4 + A1A3 + A5Waste, - isShared: false, - isReadOnly: false, - speckle_type: "Objects.BuiltElements.Revit.Parameter", - applicationId: null, - applicationUnit: null, - isTypeParameter: false, - totalChildrenCount: 0, - applicationUnitType: "string", - applicationInternalName: "string", - }, - }); - this.addParams.push({ - parentid: o.id, - name: "Product Stage Carbon A1-A3", - param: { - id: Math.floor(Math.random() * 10000000).toString(), - name: "Product Stage Carbon A1-A3", - units: "kgCO2e/kg", - value: A1A3, - isShared: false, - isReadOnly: false, - speckle_type: "Objects.BuiltElements.Revit.Parameter", - applicationId: null, - applicationUnit: null, - isTypeParameter: false, - totalChildrenCount: 0, - applicationUnitType: "string", - applicationInternalName: "string", - }, - }); - this.addParams.push({ - parentid: o.id, - name: "Transport Carbon A4", - param: { - id: Math.floor(Math.random() * 10000000).toString(), - name: "Transport Carbon A4", - units: "kgCO2e/kg", - value: A4, - isShared: false, - isReadOnly: false, - speckle_type: "Objects.BuiltElements.Revit.Parameter", - applicationId: null, - applicationUnit: null, - isTypeParameter: false, - totalChildrenCount: 0, - applicationUnitType: "string", - applicationInternalName: "string", - }, - }); - this.addParams.push({ - parentid: o.id, - name: "Construction Carbon A5", - param: { - id: Math.floor(Math.random() * 10000000).toString(), - name: "Construction Carbon A5", - units: "kgCO2e/kg", - value: A5Waste, - isShared: false, - isReadOnly: false, - speckle_type: "Objects.BuiltElements.Revit.Parameter", - applicationId: null, - applicationUnit: null, - isTypeParameter: false, - totalChildrenCount: 0, - applicationUnitType: "string", - applicationInternalName: "string", - }, - }); - // end parameter update - - return { - ...o, - reportData: { - transportCarbonA4: A4, - productStageCarbonA1A3: A1A3, - constructionCarbonA5: { - // site only kept for consistency with old versions of report. Value as refactoring to not include would be a pain - value: A5Waste, - waste: A5Waste, - site: 0, - }, - // only add A5Waste to total carbon for individual child objects as A5Site is applied model-wide - totalCarbon: A4 + A1A3 + A5Waste, - }, - }; - }); - - const totals = this.calcTotals(reportObjs); - - this.report = { - reportObjs, - totals, - }; - } - - calcTotals(reportObjs: SpeckleObjectComplete[]): ReportTotals { - let A1A3 = 0; - let A4 = 0; - let A5Waste = 0; - const materialsObj: { - [key: string]: { value: number; color: string }; - } = {}; - reportObjs.forEach((o) => { - const rd = o.reportData; - A1A3 += rd.productStageCarbonA1A3; - A4 += rd.transportCarbonA4; - A5Waste += rd.constructionCarbonA5.waste; - const objTotal = rd.productStageCarbonA1A3 + rd.transportCarbonA4 + rd.constructionCarbonA5.waste; - const materialName = o.formData.material.name; - if (materialName in materialsObj) { - materialsObj[materialName].value += objTotal; - } else { - materialsObj[materialName] = { - value: objTotal, - color: o.formData.material.color, - }; - } - }); - let A5Site = constructionCarbonA5Site(this.projectData.cost); - let A5Value = A5Site + A5Waste - - let totalCO2 = A1A3 + A4 + A5Value; - const materials: ChartData[] = Object.entries(materialsObj).map((m) => ({ - value: m[1].value, - label: m[0], - color: m[1].color, - })); - - return { - transportCarbonA4: A4, - productStageCarbonA1A3: A1A3, - constructionCarbonA5: { - value: A5Value, - waste: A5Waste, - site: A5Site, - }, - totalCO2, - volume: this.totalVolume, - materials, - materialsColors: this.materialsColors, - transportColors: this.transportColors, - }; - } - - convertToFormComplete() { - const objs: SpeckleObjectFormComplete[] = []; - Object.values(this.objectsObj).forEach((o) => { - if ( - o.formData && - o.formData.transport && - o.formData.material && - o.formData.volume - ) { - objs.push({ - ...o, - formData: { - transport: o.formData.transport, - material: o.formData.material, - volume: o.formData.volume, - }, - }); - } - }); - return objs; - } - - review() { - const emptyProps: EmptyProps = { - projectEmpty: this.projectData ? true : false, - materialsEmpty: [] as string[], - transportsEmpty: [] as string[], - volumesEmpty: [] as string[], - }; - - Object.values(this.objectsObj).forEach((o) => { - const formData = o.formData; - - if (formData?.material === undefined) - emptyProps.materialsEmpty.push(o.id); - if (formData?.transport === undefined) - emptyProps.transportsEmpty.push(o.id); - if (formData?.volume === undefined) emptyProps.volumesEmpty.push(o.id); - }); - - this.emptyProps = emptyProps; - } - async loadStream(id: string) { this.loadingModel = true; this.loadingModelText = "Loading model..."; @@ -877,7 +520,11 @@ export default class Assessment extends Vue { transportSelected(selected: TransportSelected) { if (this.beenToTransport) { - const ids = selected.material.objects; + selected.material.objects.forEach((o) => { + o.setTransport(selected.transportType); + }); + + const ids = selected.material.objects.map((o) => o.parentId); this.colors = this.colors.filter((c) => !ids.includes(c.id)); ids.forEach((id) => { this.colors.push({ @@ -886,22 +533,54 @@ export default class Assessment extends Vue { }); }); this.transportColors = this.colors; + } + } + + selectBuildup(selectedBuildup: SelectedBuildupEmit) { + const objects = this.reportController.getObjectsByIds(selectedBuildup.ids); + + objects.forEach((o) => { + // get transport of any materials that were already present + const oldTransports: { [material: string]: TransportType } = {}; + Object.entries(o.materials).forEach( + ([k, v]) => (oldTransports[k] = v.transport) + ); - selected.material.objects.forEach((i) => { - const oldObj = this.objectsObj[i]; - this.objectsObj[i] = { - ...oldObj, - formData: { - ...oldObj.formData, - transport: selected.transportType, - }, - }; + o.removeAllMaterials(); + selectedBuildup.materials.forEach((b) => { + if (b.material && b.percentage) { + o.addMaterial(b.material, +b.percentage / 100); + const oldTransport = oldTransports[b.material.name]; + if (oldTransport) o.setTransport(b.material.name, oldTransport); + } }); - } + }); + + this.colors = this.colors.filter( + (c) => !selectedBuildup.ids.includes(c.id) + ); + + selectedBuildup.ids.forEach((id) => { + try { + // colour by the first material as that's easiest + if (selectedBuildup.materials[0].material) + this.colors.push({ + color: selectedBuildup.materials[0].material.color, + id, + }); + } catch (err) { + console.error("err:", id); + } + }); + this.materialsColors = this.colors; } materialUpdated(material: MaterialUpdateOut) { - const ids = material.type.ids; + material.type.objects.forEach((o) => { + o.removeAllMaterials(); + o.changeMaterial(material.oldMaterial?.name, material.material); + }); + const ids = material.type.objects.map((o) => o.id); this.colors = this.colors.filter((c) => !ids.includes(c.id)); ids.forEach((id) => { try { @@ -914,48 +593,11 @@ export default class Assessment extends Vue { } }); this.materialsColors = this.colors; - - // update the objects to include this new material - material.type.ids.forEach((i) => { - const oldObj = this.objectsObj[i]; - this.objectsObj[i] = { - ...oldObj, - formData: { - ...oldObj.formData, - material: material.material, - }, - }; - }); - } - - // types = material groups, cuz legacy - findTypes( - propertyGroups: StringPropertyGroups[], - selectedGroup: string - ): MaterialGrouping[] { - const group = propertyGroups.find((pg) => pg.name === selectedGroup); - if (group) { - // we're assuming that if this.update=true then objectsObj will already be filled by this point - const materialGrouping: MaterialGrouping[] = group.data.valueGroups.map( - (vg) => ({ - type: vg.value, - ids: vg.ids, - material: this.update - ? this.objectsObj[vg.ids[0]].formData?.material - : undefined, - transport: this.update - ? this.objectsObj[vg.ids[0]].formData?.transport - : undefined, - }) - ); - return materialGrouping; - } - return []; } uploadData(data: ProjectDataComplete) { // form data from step 1 - this.projectData = data; + this.reportController.projectInfo = data; this.$store.dispatch("changeRegion", data.region).then((res) => { this.materials = this.$store.getters.materialsArr; }); diff --git a/src/views/utils/add-params/addParams.ts b/src/views/utils/add-params/addParams.ts index dbbc585b..d6eabc1d 100644 --- a/src/views/utils/add-params/addParams.ts +++ b/src/views/utils/add-params/addParams.ts @@ -69,7 +69,7 @@ export interface ParamAdd { param: Param; } -interface Param { +export interface Param { id: string; name: string; units: string | null; diff --git a/src/views/utils/process-report-object/loadReportObjectV1.ts b/src/views/utils/process-report-object/loadReportObjectV1.ts index f97787e8..12fc50a3 100644 --- a/src/views/utils/process-report-object/loadReportObjectV1.ts +++ b/src/views/utils/process-report-object/loadReportObjectV1.ts @@ -13,6 +13,7 @@ import { import { getChildren } from "./utils"; import * as speckleUtil from "../../../store/speckle/speckleUtil"; import { IdMapper } from "../add-params/addParams"; +import { MaterialFull } from "@/store/utilities/material-carbon-factors"; export async function calcV1( branchData: HTTPStreamDataParentV1, @@ -69,7 +70,8 @@ export async function calcV1( Math.ceil(levelsUpdated.levels[2].tCO2e * 100) / 100; const objectIds = await speckleUtil.getStreamObjects(context, streamId); - const modelId = objectIds.data.stream.branch.commits.items[0].referencedObject; + const modelId = + objectIds.data.stream.branch.commits.items[0].referencedObject; return { ready: true, @@ -84,7 +86,7 @@ export async function calcV1( aBreakdown: levelsUpdated, children: childrenData, idMapper: {} as IdMapper, - selectedObjectGroup: branchData.selectedObjectGroup + selectedObjectGroup: branchData.selectedObjectGroup, }, }; } @@ -137,8 +139,9 @@ export function extractCo2Data( object.act.reportData.transportCarbonA4 + object.act.reportData.constructionCarbonA5.value; - const materialKey = object.act.formData.material.name; - const materialColor = object.act.formData.material.color; + const material = object.act.formData.material as MaterialFull; // for v1 reports we know that only one material will be used + const materialKey = material.name; + const materialColor = material.color; if (Object.prototype.hasOwnProperty.call(co2Obj, materialKey)) { co2Obj[materialKey] = { value: co2Obj[materialKey].value + totalObjectCarbon, @@ -154,7 +157,7 @@ export function extractCo2Data( } colorsArr.push({ id: object.act.id, - color: object.act.formData.material.color, + color: material.color, }); }); const co2Arr = Object.entries(co2Obj); diff --git a/src/views/utils/propertyFiltering/findStringProps.ts b/src/views/utils/propertyFiltering/findStringProps.ts index 0aecd82c..23396787 100644 --- a/src/views/utils/propertyFiltering/findStringProps.ts +++ b/src/views/utils/propertyFiltering/findStringProps.ts @@ -1,11 +1,11 @@ import { PropertyInfo } from "@speckle/viewer"; import { flattenObject } from "./"; -import { ObjectsObj, StringPropertyGroups } from "@/models/newAssessment"; +import { StringPropertyGroups } from "@/models/newAssessment"; // source: https://github.com/specklesystems/speckle-server export function findStringProps( speckleObjsPropsSearch: any[], - objectsObj: ObjectsObj + objectsObj: { [key: string]: any } // we can probably figure out a way to no longer need this... ): StringPropertyGroups[] { const propValues: any = {}; speckleObjsPropsSearch.forEach((obj) => { @@ -119,11 +119,32 @@ export function findStringProps( filter.targetKey = key; filters.push(filter); } - const stringFilters = filters.filter( - (f) => - f.data.type === "string" && - f.data.objectCount === Object.keys(objectsObj).length - ); + + // filter out number filters and add in an "undefined" option for those that need it + const stringFilters = filters.filter((f) => f.data.type === "string"); + stringFilters.forEach((f) => { + if (f.data.objectCount !== Object.keys(objectsObj).length) { + const ids2d = ( + f.data.valueGroups as { value: string; ids: string[] }[] + ).map((v: { value: string; ids: string[] }) => v.ids); + const ids: string[] = []; + ids2d.forEach((i) => ids.push(...i)); + + let allIds = Object.keys(objectsObj); + + ids.forEach((i) => { + const index = allIds.indexOf(i); + if (index > -1) { + allIds.splice(index, 1); + } + }); + + f.data.valueGroups.push({ + value: "undefined", + ids: [...allIds], + }); + } + }); return stringFilters; }