import type { ParsedCase } from "../types"; import { EMPTY_CASE_STATE, type CaseState, type RawFieldValue, type SurveyRow, type TaperRow } from "./caseModel"; /** Flatten xml2js node to its text content (preserves attr bag in '$' if present). */ function textOf(value: RawFieldValue): string { if (value === undefined || value === null) return ""; if (typeof value === "string") return value; if (typeof value === "object") { const obj = value as Record; if (typeof obj._ === "string") return obj._; if (Object.keys(obj).length === 1 && "$" in obj) return ""; } return ""; } function numberOf(value: RawFieldValue, fallback = 0): number { const text = textOf(value).trim(); if (!text) return fallback; const n = Number(text); return Number.isFinite(n) ? n : fallback; } function stringOf(value: RawFieldValue, fallback = ""): string { const text = textOf(value).trim(); return text || fallback; } function parseColonArray(value: RawFieldValue): number[] { const text = textOf(value); if (!text) return []; return text .split(":") .map((piece) => piece.trim()) .filter((piece) => piece.length > 0) .map((piece) => Number(piece)) .filter((n) => Number.isFinite(n)); } /** * Hydrate a CaseState from the `parsed` block returned by the solver-api. * Values come primarily from `rawFields` (pre-normalization) so the GUI * edits the XML-native units directly. `parsed.model` is used only as a * fallback when `rawFields` lacks an entry. */ export function hydrateFromParsed(parsed: ParsedCase): CaseState { const raw = (parsed.rawFields ?? {}) as Record; const model = parsed.model ?? ({} as ParsedCase["model"]); const rawFieldOrder = Object.keys(raw); const md = parseColonArray(raw.MeasuredDepthArray); const inc = parseColonArray(raw.InclinationFromVerticalArray); const azi = parseColonArray(raw.AzimuthFromNorthArray); const surveyLen = Math.max(md.length, inc.length, azi.length); const survey: SurveyRow[] = []; for (let i = 0; i < surveyLen; i += 1) { survey.push({ md: md[i] ?? 0, inc: inc[i] ?? 0, azi: azi[i] ?? 0 }); } const diam = parseColonArray(raw.TaperDiameterArray); const length = parseColonArray(raw.TaperLengthArray); const modulus = parseColonArray(raw.TaperModulusArray); const rodType = parseColonArray(raw.RodTypeArray); const weight = parseColonArray(raw.TaperWeightArray); const mts = parseColonArray(raw.TaperMTSArray); const guideCounts = parseColonArray(raw.TaperGuidesCountArray); const rgWeights = parseColonArray(raw.RodGuideWeightArray); const rgTypePieces = textOf(raw.RodGuideTypeArray).split(":"); const rodGuideSlotCount = Math.max(rgTypePieces.length, rgWeights.length, 1); /** Core rod-string stations (commercial XML often pads these together). */ const coreTaperLen = Math.max(diam.length, length.length, modulus.length, rodType.length); const taperParallelSuffix = { weights: weight.length > coreTaperLen ? weight.slice(coreTaperLen) : [], mts: mts.length > coreTaperLen ? mts.slice(coreTaperLen) : [], guidesCounts: guideCounts.length > coreTaperLen ? guideCounts.slice(coreTaperLen) : [] }; const taper: TaperRow[] = []; for (let i = 0; i < coreTaperLen; i += 1) { const gc = guideCounts[i]; const guidesEnabled = Number.isFinite(gc) && gc >= 0; const tok = (rgTypePieces[i] ?? "").trim(); taper.push({ diameter: diam[i] ?? 0, length: length[i] ?? 0, modulus: modulus[i] ?? 0, rodType: rodType[i] ?? 0, weightLbfPerFt: weight[i] ?? 0, mtsLbf: mts[i] ?? 0, guidesEnabled, guideCount: guidesEnabled ? Math.max(0, Math.round(gc)) : 0, guideTypeToken: tok, rodGuideWeightLbfPerFt: rgWeights[i] ?? 0 }); } return { ...EMPTY_CASE_STATE, wellName: stringOf(raw.WellName, model.wellName ?? ""), company: stringOf(raw.Company, model.company ?? ""), pumpDepth: numberOf(raw.PumpDepth, model.pumpDepth ?? 0), tubingAnchorLocation: numberOf(raw.TubingAnchorLocation, model.tubingAnchorLocation ?? 0), tubingSize: numberOf(raw.TubingSize, model.tubingSize ?? 0), pumpingSpeed: numberOf(raw.PumpingSpeed, model.pumpingSpeed ?? 0), pumpingSpeedOption: numberOf(raw.PumpingSpeedOption, model.pumpingSpeedOption ?? 0), pumpingUnitId: stringOf(raw.PumpingUnitID, model.pumpingUnitId ?? ""), survey, taper, pumpDiameter: numberOf(raw.PumpDiameter, model.pumpDiameter ?? 0), pumpFriction: numberOf(raw.PumpFriction, model.pumpFriction ?? 0), pumpIntakePressure: numberOf(raw.PumpIntakePressure, model.pumpIntakePressure ?? 0), pumpFillageOption: numberOf(raw.PumpFillageOption, model.pumpFillageOption ?? 0), percentPumpFillage: numberOf(raw.PercentPumpFillage, model.percentPumpFillage ?? 0), percentUpstrokeTime: numberOf(raw.PercentageUpstrokeTime, model.percentUpstrokeTime ?? 50), percentDownstrokeTime: numberOf(raw.PercentageDownstrokeTime, model.percentDownstrokeTime ?? 50), waterCut: numberOf(raw.WaterCut, model.waterCut ?? 0), waterSpecGravity: numberOf(raw.WaterSpecGravity, model.waterSpecGravity ?? 1), fluidLevelOilGravity: numberOf(raw.FluidLevelOilGravity, model.fluidLevelOilGravity ?? 35), tubingGradient: numberOf(raw.TubingGradient, model.tubingGradient ?? 0), rodFrictionCoefficient: numberOf( raw.RodFrictionCoefficient, model.rodFrictionCoefficient ?? 0 ), stuffingBoxFriction: numberOf(raw.StuffingBoxFriction, model.stuffingBoxFriction ?? 0), moldedGuideFrictionRatio: numberOf( raw.MoldedGuideFrictionRatio, model.moldedGuideFrictionRatio ?? 1 ), wheeledGuideFrictionRatio: numberOf( raw.WheeledGuideFrictionRatio, model.wheeledGuideFrictionRatio ?? 1 ), otherGuideFrictionRatio: numberOf( raw.OtherGuideFrictionRatio, model.otherGuideFrictionRatio ?? 1 ), sinkerBarDiameter: numberOf(raw.SinkerBarDiameter, 0), sinkerBarLength: numberOf(raw.SinkerBarLength, 0), rodGuideSlotCount, taperParallelSuffix, upStrokeDamping: numberOf(raw.UpStrokeDampingFactor, model.upStrokeDamping ?? 0), downStrokeDamping: numberOf(raw.DownStrokeDampingFactor, model.downStrokeDamping ?? 0), nonDimensionalFluidDamping: numberOf( raw.NonDimensionalFluidDamping, model.nonDimensionalFluidDamping ?? 0 ), unitsSelection: numberOf(raw.UnitsSelection, model.unitsSelection ?? 0), rawFields: { ...raw }, rawFieldOrder }; }