Files
Pier_v1/tests/ground-model.test.ts

223 lines
9.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { groundElFor, parseGroundModel, strataRowsFor } from '../src/core/ground-model';
import { changedKeys, overrideKeys, revertDraft, type PierValues } from '../src/core/changes';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { loadP1 } from './helpers';
/** The shape written by RAG/pierzainer_ground (schema 1). */
const SAMPLE = JSON.stringify({
schema: 1,
generated: '2026-10-23',
coord: '측량 X(북) · Y(동), m. 치수 mm',
piers: [
{ id: 'P1', ground: { center: 29.355, excel: 28.931, used: 29.355, source: '계산' }, footing: { cover: 0.972 } },
{ id: 'P2', ground: { center: null, excel: null, used: null, source: '없음' } },
],
});
describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
it('reads the pier-centre ground EL and the excel value', () => {
const m = parseGroundModel(SAMPLE);
expect(m.piers[0]).toEqual({ id: 'P1', centerEl: 29.355, excelEl: 28.931 });
expect(groundElFor(m, 'P1')).toBe(29.355);
});
it('a pier outside the terrain has no value, and an unknown pier has none either', () => {
const m = parseGroundModel(SAMPLE);
expect(groundElFor(m, 'P2')).toBeUndefined();
expect(groundElFor(m, 'P9')).toBeUndefined();
});
it('refuses another schema number instead of guessing the layout', () => {
expect(() => parseGroundModel(SAMPLE.replace('"schema":1', '"schema":3'))).toThrow('형식 번호 3');
});
it('refuses a file without a pier list or without pier names', () => {
expect(() => parseGroundModel('{"schema": 1}')).toThrow('교각 목록');
expect(() => parseGroundModel('{"schema": 1, "piers": [{"ground": {}}]}')).toThrow('이름(id)');
});
it('refuses text that is not JSON', () => {
expect(() => parseGroundModel('not json')).toThrow('JSON으로 읽히지 않습니다');
});
});
const LEGACY_SAMPLE = JSON.stringify({ schema: 1, piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] });
describe('지형 격자 (schema 2)', () => {
const WITH_GRID = JSON.stringify({
schema: 2,
generated: '2026-10-23',
piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }],
terrain_grid: { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] },
});
it('reads the terrain grid as terrainGrid', () => {
const m = parseGroundModel(WITH_GRID);
expect(m.schema).toBe(2);
expect(m.terrainGrid).toEqual({ origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] });
});
it('reads both embedded map images and their survey-coordinate corners', () => {
const raw = JSON.parse(WITH_GRID) as Record<string, unknown>;
const image = 'data:image/jpeg;base64,/9j/2Q==';
raw.visualization = {
layers: [
{ id: 'satellite', name: '위성사진', mime_type: 'image/jpeg', image, survey_corners: [[1, 2], [1, 3], [4, 2], [4, 3]] },
{ id: 'ground', name: '2D 지도', mime_type: 'image/jpeg', image, survey_corners: [[1, 2], [1, 3], [4, 2], [4, 3]] },
],
};
const model = parseGroundModel(JSON.stringify(raw));
expect(model.mapLayers?.map((layer) => layer.id)).toEqual(['satellite', 'ground']);
expect(model.mapLayers?.[0].survey_corners[0]).toEqual([1, 2]);
});
it('ignores legacy map layers without coordinate-matched image corners', () => {
const raw = JSON.parse(WITH_GRID) as Record<string, unknown>;
raw.visualization = {
layers: [
{ id: 'satellite', mime_type: 'image/jpeg', image: 'data:image/jpeg;base64,/9j/2Q==' },
{ id: 'ground', mime_type: 'image/jpeg', image: 'data:image/jpeg;base64,/9j/2Q==' },
],
};
expect(parseGroundModel(JSON.stringify(raw)).mapLayers).toEqual([]);
});
it('a schema 1 file has no terrain grid', () => {
expect(parseGroundModel(LEGACY_SAMPLE).terrainGrid).toBeUndefined();
});
it('refuses a grid whose value count is not nx × ny', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,29]'))).toThrow('nx × ny');
});
it('refuses a grid value that is not a number or null', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,"x",29,30]'))).toThrow('숫자가 아닌');
});
it('refuses an unknown schema number', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"schema":2', '"schema":3'))).toThrow('형식 번호 3');
});
it('keeps the grid in a saved project', () => {
const m = parseGroundModel(WITH_GRID);
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: loadP1() }],
history: [],
groundModel: m,
});
expect(parseProject(text).groundModel?.terrainGrid).toEqual(m.terrainGrid);
});
});
describe('형식 번호 검사', () => {
it('accepts a legacy file without a schema number when it has the standard pier structure', () => {
const m = parseGroundModel(JSON.stringify({ piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] }));
expect(m.schema).toBe(2);
expect(m.piers[0].id).toBe('P1');
});
it('refuses a null schema number', () => {
expect(() => parseGroundModel('{"schema": null, "piers": []}')).toThrow('형식 번호 null');
});
it('refuses a pier that has no ground object', () => {
expect(() => parseGroundModel(JSON.stringify({ schema: 1, piers: [{ id: 'P2' }] }))).toThrow('P2: 지반 값(ground)이 없습니다');
});
});
describe('지반고 적용 (사용자가 누를 때만, 변경으로 기록)', () => {
const base = loadP1();
const pier: PierValues = { defaults: base, input: base, draft: { ...base, groundEl: 29.355 } };
it('an applied value counts as an override and can be reverted to the excel value', () => {
expect(overrideKeys(pier)).toContain('groundEl');
expect(revertDraft(pier, 'groundEl').groundEl).toBe(base.groundEl);
});
it('the draft and the generated input are compared, so the change waits for regeneration', () => {
expect(changedKeys(pier.input, pier.draft)).toEqual(['groundEl']);
});
});
describe('프로젝트 파일의 지반 모델', () => {
function project(extra: Partial<ProjectFile>): ProjectFile {
const p1 = loadP1();
return {
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: p1 }],
history: [],
...extra,
};
}
it('keeps the imported ground model across save and open', () => {
const m = parseGroundModel(SAMPLE);
const reopened = parseProject(serializeProject(project({ groundModel: m })));
expect(reopened.groundModel).toEqual(m);
});
it('refuses a damaged ground model inside the project file', () => {
const text = serializeProject(project({})).replace('"history": []', '"history": [], "groundModel": {"schema": 1}');
expect(() => parseProject(text)).toThrow('교각 목록');
});
it('refuses a stored model whose pier values are not numbers or null', () => {
const m = parseGroundModel(SAMPLE);
const bad = { ...m, piers: [{ id: 'P1', centerEl: '29.3', excelEl: null }] };
const text = serializeProject(project({})).replace('"history": []', `"history": [], "groundModel": ${JSON.stringify(bad)}`);
expect(() => parseProject(text)).toThrow('P1의 값이 맞지 않습니다');
});
it('opens an older file that has no ground model', () => {
expect(parseProject(serializeProject(project({}))).groundModel).toBeUndefined();
});
});
describe('지반 모델의 지층 기둥 (soil.layers)', () => {
const WITH_SOIL = JSON.stringify({
schema: 2,
piers: [{
id: 'P1',
ground: { center: 29.355, excel: 28.931 },
soil: { layers: [{ name: '매립토', top_el: 29.355, bottom_el: 27.5 }, { name: '연암', top_el: 27.5, bottom_el: 10 }] },
}],
});
it('reads the layers as strata rows (name and bottom EL, top to bottom)', () => {
const m = parseGroundModel(WITH_SOIL);
expect(strataRowsFor(m, 'P1')).toEqual([{ name: '매립토', bottomEl: 27.5 }, { name: '연암', bottomEl: 10 }]);
expect(strataRowsFor(m, 'P9')).toBeUndefined();
});
it('a pier without a soil column has no layers', () => {
expect(strataRowsFor(parseGroundModel(SAMPLE), 'P1')).toBeUndefined();
});
it('refuses a soil layers entry that is not a list', () => {
const data = JSON.parse(WITH_SOIL);
data.piers[0].soil.layers = { x: 1 };
expect(() => parseGroundModel(JSON.stringify(data))).toThrow('지층(soil.layers) 형식');
});
it('keeps the layers in a saved project and refuses a damaged layer', () => {
const m = parseGroundModel(WITH_SOIL);
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: loadP1() }],
history: [],
groundModel: m,
});
expect(strataRowsFor(parseProject(text).groundModel!, 'P1')).toEqual(strataRowsFor(m, 'P1'));
const bad = text.replace('"bottomEl": 10', '"bottomEl": "10"');
expect(() => parseProject(bad)).toThrow('지층 값이 맞지 않습니다');
});
});