MEGAINFRA-WEBAPP/src/lib/site-map-v2/quantities.ts
DEls 8e1cbc8b75 feat(site-map): Munyaka 2 stormwater and village network, Progress section highlight, layer switches
Munyaka model: 27 stormwater structures and 26 pipes from the issued structure table
(MH/KI/outlet by CAD block), water node points, and the information-stage village network
(services, ponds, boundary, lots, hydrants) aligned from the SDP Rev C PDF.

Map 3: selected Progress section is outlined and its row highlighted; Show-on-map gains
ground surface, contours, kerbs, markings, toes, reserve, signs, studs and HOLD switches;
the ground fades when underground services are ticked; pipes and manholes are chunkier.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 12:13:38 +02:00

77 lines
4.8 KiB
TypeScript

import type { SiteModelData } from "./model-types";
import type { ProgressState } from "./progress";
import { valueOf } from "./progress";
import { earthKey, fitKey, kerbKey, PAINT_KEY, roadKey, signKey, srKey, srPipeKey, studKey, swKey, swPipeKey } from "./keys";
export interface BoqRow { group: string; name: string; unit: string; color: number; qty: number; done: number }
export const KERB_ITEM = "Kerbs (mountable / Fig. 7)";
const KIND: Record<string, string> = { KI: "Kerb inlet", MH: "Manhole", JB: "Junction box", OUT: "Outlet / headwall", DL: "Outfall point" };
export const structureKind = (name: string) => KIND[name.match(/^[A-Z]+/)?.[0] ?? ""] ?? "Structure";
export function trenchVolume(L: number, dia: number, depthA: number, depthB: number): number {
return L * (dia / 1000 + 0.6) * Math.max(0.6, (depthA + depthB) / 2 + 0.15);
}
export function boqRows(m: SiteModelData, s: ProgressState): BoqRow[] {
const rows = new Map<string, BoqRow>();
const add = (group: string, name: string, unit: string, color: number, qty: number, done: number) => {
const k = `${group}|${name}`, r = rows.get(k) ?? { group, name, unit, color, qty: 0, done: 0 };
r.qty += qty; r.done += done; rows.set(k, r);
};
// Roadworks: per area piece, per layer
for (const a of m.road.areas) for (const l of m.road.builds[a.kind]?.layers ?? []) {
const q = l.unit === "m³" ? a.area * l.thk : a.area;
add("Roadworks", l.name, l.unit, l.color, q, valueOf(s, roadKey(a.section, a.kind, l.id)) ? q : 0);
}
const kerbLen = new Map<string, number>();
for (const k of m.road.kerbs) kerbLen.set(k.section, (kerbLen.get(k.section) ?? 0) + k.length * (m.road.kerbQuantityFactor ?? 0.5)); // model declares single or paired linework
for (const [sec, L] of kerbLen) add("Roadworks", KERB_ITEM, "m", 0xd8d3c6, L, (L * valueOf(s, kerbKey(sec))) / 100);
// Stormwater
const sw = m.services.stormwater, byName = new Map(sw.structures.map((x) => [x.name, x]));
const top = (name: string) => { const x = byName.get(name)!; return x.gl ?? x.il + 1.5; };
let tq = 0, td = 0;
for (const p of sw.pipes) {
const laid = valueOf(s, swPipeKey(p.from, p.to)) > 0;
add("Stormwater", `${p.dia} mm concrete pipe${p.dia === 450 ? " (Class 100D)" : ""}`, "m", 0x8d99a6, p.length, laid ? p.length : 0);
const t = trenchVolume(p.length, p.dia, top(p.from) - p.ilFrom, top(p.to) - p.ilTo); tq += t; td += laid ? t : 0;
}
for (const x of sw.structures) {
const k = structureKind(x.kind ?? x.name);
add("Stormwater", k.endsWith("box") ? `${k}es` : `${k}s`, "No.", 0x6b7a88, 1, valueOf(s, swKey(x.name)) === 2 ? 1 : 0);
}
add("Stormwater", "Trench excavation (estimate)", "m³", 0x9b7f57, tq, td);
// Sewer
const sr = m.services.sewer, srName = new Map(sr.manholes.map((x) => [x.id, x]));
let sq = 0, sd = 0;
for (const p of sr.pipes) {
const a = srName.get(p.from)!, b = srName.get(p.to)!, laid = valueOf(s, srPipeKey(a.name, b.name)) > 0;
add("Sewer", `${p.dia} mm uPVC sewer pipe`, "m", 0xb8703a, p.length, laid ? p.length : 0);
const t = trenchVolume(p.length, p.dia, 1.5, 1.5); sq += t; sd += laid ? t : 0;
}
for (const x of sr.manholes) add("Sewer", "Sewer manholes", "No.", 0x7a5a48, 1, valueOf(s, srKey(x.name)) === 2 ? 1 : 0);
add("Sewer", "Trench excavation (estimate)", "m³", 0x9b7f57, sq, sd);
// Earthworks
for (const v of m.earthworks.volumes) {
const f = valueOf(s, earthKey(v.from, v.to)) / 100;
add("Earthworks", "Bulk excavation — cut to formation", "m³", 0xd64545, v.cut, v.cut * f);
add("Earthworks", "Embankment fill to formation", "m³", 0x3f9a5a, v.fill, v.fill * f);
}
// Signs, studs, markings, water
const sc = m.schedules, sum = (o: Record<string, number>) => Object.values(o).reduce((a, b) => a + b, 0);
add("Signs, studs & markings", "Road signs (all types)", "No.", 0xd1242f, sum(sc.signCounts), Object.keys(sc.signCounts).reduce((t, c) => t + valueOf(s, signKey(c)), 0));
add("Signs, studs & markings", "Road studs", "No.", 0xf2c200, sum(sc.studCounts), Object.keys(sc.studCounts).reduce((t, c) => t + valueOf(s, studKey(c)), 0));
const paint = sum(sc.paintAreas);
add("Signs, studs & markings", "Painted markings (from hatches)", "m²", 0xf4f4f4, paint, valueOf(s, PAINT_KEY) ? paint : 0);
add("Water", "Water fittings (all)", "No.", 0x18a0c9, sum(sc.waterFittings), Object.keys(sc.waterFittings).reduce((t, c) => t + valueOf(s, fitKey(c)), 0));
return [...rows.values()];
}
export function sectionProgress(m: SiteModelData, s: ProgressState, section: string): number {
let tot = 0, done = 0;
for (const a of m.road.areas.filter((x) => x.section === section)) {
const layers = m.road.builds[a.kind]?.layers ?? [];
tot += a.area;
done += (a.area * layers.filter((l) => valueOf(s, roadKey(section, a.kind, l.id))).length) / (layers.length || 1);
}
return tot ? done / tot : 0;
}