148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import type { ExternalCsvConfiguration } from "../csv/external-csv-contracts.js";
|
|
import type {
|
|
ExternalModelObjectPlanningValues,
|
|
ExternalModelObjectSourceValues,
|
|
} from "./external-model-contracts.js";
|
|
|
|
export type InitialExternalObjectIssue =
|
|
| "unknown-family-and-type"
|
|
| "invalid-power"
|
|
| "invalid-quantity"
|
|
| "missing-room";
|
|
|
|
export interface InitialExternalObjectProjection {
|
|
ifcGuid: string;
|
|
sourceValues: ExternalModelObjectSourceValues;
|
|
planningValues: ExternalModelObjectPlanningValues;
|
|
issues: InitialExternalObjectIssue[];
|
|
}
|
|
|
|
export interface ExternalObjectImportCandidate {
|
|
rowNumber: number;
|
|
ifcGuid: string;
|
|
roomNumber: string;
|
|
roomName: string;
|
|
familyAndType: string;
|
|
selectionMarker: string;
|
|
circuitIdentifier: string;
|
|
power: string;
|
|
quantity: string | null;
|
|
additionalSourceValues: Record<string, string>;
|
|
}
|
|
|
|
export function normalizeExternalRoomPart(value: string): string {
|
|
return value.trim().normalize("NFKC").toLocaleUpperCase("de-DE");
|
|
}
|
|
|
|
export function createExternalRoomKey(
|
|
roomNumber: string,
|
|
roomName: string
|
|
): string | null {
|
|
const normalizedNumber = normalizeExternalRoomPart(roomNumber);
|
|
if (normalizedNumber) return `number:${normalizedNumber}`;
|
|
const normalizedName = normalizeExternalRoomPart(roomName);
|
|
return normalizedName ? `name:${normalizedName}` : null;
|
|
}
|
|
|
|
export function indexExternalObjectsByIfcGuid<T extends { ifcGuid: string }>(
|
|
objects: readonly T[]
|
|
): Map<string, T> {
|
|
const index = new Map<string, T>();
|
|
for (const object of objects) {
|
|
if (!object.ifcGuid || object.ifcGuid !== object.ifcGuid.trim()) {
|
|
throw new Error("IfcGUID must be non-empty and must not contain outer whitespace.");
|
|
}
|
|
if (index.has(object.ifcGuid)) {
|
|
throw new Error(`Duplicate IfcGUID: ${object.ifcGuid}`);
|
|
}
|
|
index.set(object.ifcGuid, object);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
export function projectInitialExternalObject(
|
|
object: ExternalObjectImportCandidate,
|
|
configuration: ExternalCsvConfiguration
|
|
): InitialExternalObjectProjection {
|
|
const rule = configuration.familyTypeRules.find(
|
|
(candidate) => candidate.exactFamilyAndType === object.familyAndType
|
|
);
|
|
const issues: InitialExternalObjectIssue[] = [];
|
|
if (!rule) issues.push("unknown-family-and-type");
|
|
if (!createExternalRoomKey(object.roomNumber, object.roomName)) {
|
|
issues.push("missing-room");
|
|
}
|
|
|
|
const parsedPower = parseConfiguredNumber(object.power, configuration);
|
|
if (object.power.trim() && parsedPower === null) issues.push("invalid-power");
|
|
|
|
let effectiveQuantity = 1;
|
|
if (rule?.quantityRule.kind === "fixed") {
|
|
effectiveQuantity = rule.quantityRule.quantity;
|
|
} else if (rule?.quantityRule.kind === "mapped-column") {
|
|
const parsedQuantity = parseConfiguredNumber(object.quantity ?? "", configuration);
|
|
if (parsedQuantity === null || !Number.isInteger(parsedQuantity) || parsedQuantity <= 0) {
|
|
issues.push("invalid-quantity");
|
|
} else {
|
|
effectiveQuantity = parsedQuantity;
|
|
}
|
|
}
|
|
|
|
return {
|
|
ifcGuid: object.ifcGuid,
|
|
sourceValues: {
|
|
rowNumber: object.rowNumber,
|
|
roomNumber: object.roomNumber,
|
|
roomName: object.roomName,
|
|
familyAndType: object.familyAndType,
|
|
selectionMarker: object.selectionMarker,
|
|
circuitIdentifier: object.circuitIdentifier,
|
|
power: object.power,
|
|
quantity: object.quantity,
|
|
additionalSourceValues: { ...object.additionalSourceValues },
|
|
},
|
|
planningValues: {
|
|
displayName: rule
|
|
? resolveDisplayName(rule.displayNameSuggestion, object)
|
|
: null,
|
|
internalDeviceType: rule?.internalDeviceType ?? null,
|
|
category: rule?.category ?? null,
|
|
connectionKind: rule?.connectionKind ?? null,
|
|
effectiveQuantity,
|
|
powerPerUnitW:
|
|
parsedPower === null ? null : parsedPower * configuration.wattsPerSourceUnit,
|
|
simultaneityFactor: 1,
|
|
cosPhi: null,
|
|
costGroup: null,
|
|
remark: null,
|
|
},
|
|
issues,
|
|
};
|
|
}
|
|
|
|
function parseConfiguredNumber(
|
|
value: string,
|
|
configuration: ExternalCsvConfiguration
|
|
): number | null {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return null;
|
|
const normalized = configuration.decimalSeparator === ","
|
|
? trimmed.replace(",", ".")
|
|
: trimmed;
|
|
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null;
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
}
|
|
|
|
function resolveDisplayName(
|
|
suggestion: ExternalCsvConfiguration["familyTypeRules"][number]["displayNameSuggestion"],
|
|
object: ExternalObjectImportCandidate
|
|
): string | null {
|
|
if (suggestion === null) return null;
|
|
if (suggestion.kind === "fixed") return suggestion.value;
|
|
if (suggestion.kind === "selection-marker") {
|
|
return object.selectionMarker.trim() || null;
|
|
}
|
|
return object.familyAndType.trim() || null;
|
|
}
|