- Bouton "Supprimer" sur chaque run (liste + detail), DELETE deja expose cote API. - Tous les parametres CloudCompare mirroires dans l'UI (mirroir de la boite "Compute Volume" du GUI) : strategie de remplissage (leave_empty/min/max/custom/interpolate/kriging), hauteur de cellule (moyenne/min/max/mediane, -PROJ), direction de projection (X/Y/Z, -VERT_DIR), multiplicateur de grille. Nuage comble desormais exporte en ASCII (pas .bin) : -VOLUME le lit nativement, evite un aller-retour inutile puisqu'il faut de toute facon un export ASCII pour la carte de comparaison. - Tableau : volume/surface/volume ajoute/volume retire avec % absolu et % relatif vs niveau 0. Matching cells retire du tableau et des graphiques (mais toujours calcule/stocke, juste plus affiche). - Carte de comparaison unique par niveau (bleu->rouge façon CloudCompare, colormap jet) a la place des heightmaps separees par nuage : diff = ceil - ground (ou ceil - plan constant), calculee nous-memes par binning numpy sur une grille commune. - README + CLAUDE.md : avertissement explicite sur la persistance Coolify quand le build pack est "Application"/Dockerfile plutot que "Docker Compose" (docker-compose.yml et son volume nomme sont alors completement ignores par Coolify, il faut un volume "Storages" configure manuellement). - Valide en local avec les 2 vrais nuages utilisateur (mode comparaison) : resultats identiques a avant (14 244 -> 14 722 m3 selon le niveau), cartes de comparaison generees a chaque niveau, bouton suppression teste via l'UI.
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { reportToCsv, sanitizeBaseName } from './pipeline.service';
|
|
import { RunReport } from './types';
|
|
|
|
describe('sanitizeBaseName', () => {
|
|
it('retire l\'extension et les caracteres non surs', () => {
|
|
expect(sanitizeBaseName('mon nuage (2026) v2.las')).toBe('mon_nuage_2026_v2');
|
|
});
|
|
|
|
it('gere le double-extension .copc.laz (ne retire que le dernier ".laz")', () => {
|
|
expect(sanitizeBaseName('zone_chantier.copc.laz')).toBe('zone_chantier_copc');
|
|
});
|
|
|
|
it('retombe sur un nom par defaut si le nom de base est vide apres extraction de l\'extension', () => {
|
|
expect(sanitizeBaseName('.las')).toBe('nuage');
|
|
});
|
|
|
|
it('tronque a 80 caracteres', () => {
|
|
const long = 'a'.repeat(200) + '.las';
|
|
expect(sanitizeBaseName(long).length).toBe(80);
|
|
});
|
|
});
|
|
|
|
describe('reportToCsv', () => {
|
|
const constHeightReport: RunReport = {
|
|
runId: 'run-1',
|
|
mode: 'const_height',
|
|
originalFilename: 'test.las',
|
|
originalFilenameBottom: null,
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
params: {
|
|
zref: 100,
|
|
zrefSource: 'auto',
|
|
initialStep: 0.02,
|
|
initialStepSource: 'auto',
|
|
gridStepMultiplier: 2,
|
|
gridStepMultiplierSource: 'auto',
|
|
maxEdgeLengthMultiplier: 3,
|
|
maxEdgeLengthSource: 'auto',
|
|
emptyCellStrategy: 'interpolate',
|
|
customHeightValue: null,
|
|
krigingKnn: null,
|
|
projectionType: 'avg',
|
|
vertDir: 2,
|
|
factor: 2,
|
|
steps: 1,
|
|
densityMatch: null,
|
|
sampleStats: { sampleCount: 100, meanNn: 0.02, medianNn: 0.02, bboxApprox: { xmin: 0, xmax: 1, ymin: 0, ymax: 1, zmin: 100, zmax: 101 } },
|
|
},
|
|
levels: [
|
|
{
|
|
level: 0,
|
|
gridStep: 0.04,
|
|
maxEdgeLength: 0.12,
|
|
clouds: [{ role: 'unique', spatialStep: null, pointCount: 1000, pointRatio: 1, binFile: 'levels/L0_full.bin' }],
|
|
volume: 12.5,
|
|
surface: 100,
|
|
addedVolume: 12.5,
|
|
removedVolume: 0,
|
|
matchingCellsPct: 99,
|
|
groundNonMatchingPct: 1,
|
|
ceilNonMatchingPct: 0,
|
|
diffMapImage: 'images/L0_diffmap.png',
|
|
warn: false,
|
|
status: 'ok',
|
|
computeTimeMs: 500,
|
|
},
|
|
{
|
|
level: 1,
|
|
gridStep: 0.08,
|
|
maxEdgeLength: 0.24,
|
|
clouds: [{ role: 'unique', spatialStep: 0.04, pointCount: 0, pointRatio: 0, binFile: null }],
|
|
volume: null,
|
|
surface: null,
|
|
addedVolume: null,
|
|
removedVolume: null,
|
|
matchingCellsPct: null,
|
|
groundNonMatchingPct: null,
|
|
ceilNonMatchingPct: null,
|
|
diffMapImage: null,
|
|
warn: true,
|
|
status: 'skipped',
|
|
message: 'decimation, pas assez de points\navec une virgule, ici',
|
|
computeTimeMs: 0,
|
|
},
|
|
],
|
|
mergedBinFile: 'test_all_levels.bin',
|
|
};
|
|
|
|
it('genere un en-tete et une ligne par (niveau, role)', () => {
|
|
const csv = reportToCsv(constHeightReport);
|
|
const lines = csv.trim().split('\n');
|
|
expect(lines).toHaveLength(3); // header + 2 niveaux (1 nuage chacun en mode const_height)
|
|
expect(lines[0]).toBe(
|
|
'level,role,spatialStep,gridStep,maxEdgeLength,pointCount,pointRatio,volume,surface,addedVolume,removedVolume,matchingCellsPct,groundNonMatchingPct,ceilNonMatchingPct,status,warn,computeTimeMs,message',
|
|
);
|
|
});
|
|
|
|
it('genere une ligne par nuage (top ET bottom) en mode cloud_compare', () => {
|
|
const compareReport: RunReport = {
|
|
...constHeightReport,
|
|
mode: 'cloud_compare',
|
|
levels: [
|
|
{
|
|
level: 0,
|
|
gridStep: 0.04,
|
|
maxEdgeLength: 0.12,
|
|
clouds: [
|
|
{ role: 'top', spatialStep: 0.02, pointCount: 500, pointRatio: 1, binFile: 'levels/L0_top_full.bin' },
|
|
{ role: 'bottom', spatialStep: 0.02, pointCount: 480, pointRatio: 1, binFile: 'levels/L0_bottom_full.bin' },
|
|
],
|
|
volume: 5,
|
|
surface: 50,
|
|
addedVolume: 5,
|
|
removedVolume: 0,
|
|
matchingCellsPct: 97,
|
|
groundNonMatchingPct: 3,
|
|
ceilNonMatchingPct: 0,
|
|
diffMapImage: 'images/L0_diffmap.png',
|
|
warn: false,
|
|
status: 'ok',
|
|
computeTimeMs: 300,
|
|
},
|
|
],
|
|
};
|
|
const csv = reportToCsv(compareReport);
|
|
const lines = csv.trim().split('\n');
|
|
expect(lines).toHaveLength(3); // header + 2 lignes (top, bottom) pour le seul niveau
|
|
expect(lines[1]).toMatch(/^0,top,/);
|
|
expect(lines[2]).toMatch(/^0,bottom,/);
|
|
});
|
|
|
|
it('remplace les sauts de ligne et virgules dans les messages pour ne pas casser le CSV', () => {
|
|
const csv = reportToCsv(constHeightReport);
|
|
const lines = csv.trim().split('\n');
|
|
expect(lines[2]).not.toContain('\n');
|
|
const messageField = lines[2].split(',').slice(17).join(',');
|
|
expect(messageField).not.toMatch(/,/);
|
|
});
|
|
|
|
it('laisse les champs numeriques null en cellule vide plutot que "null"', () => {
|
|
const csv = reportToCsv(constHeightReport);
|
|
const lines = csv.trim().split('\n');
|
|
expect(lines[2]).not.toContain('null');
|
|
});
|
|
});
|