/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { describe, it } from 'node:test'; import assert from 'node:assert'; import { detectScaleUnitMismatch, getEffectiveHorizontalScale, hasStandardGeoreferencing, inferMapUnitScale, mergeMapConversion, mergeProjectedCRS, supportsStandardGeoreferencing, } from './effective-georef.js'; import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser'; describe('effective georeferencing', () => { it('recomputes map unit scale when the edited MapUnit changes', () => { const original: ProjectedCRS = { id: 1, name: 'EPSG:28992', mapUnit: 'METRE', mapUnitScale: 1, }; const merged = mergeProjectedCRS(original, { mapUnit: 'US SURVEY FOOT' }, 1); assert.strictEqual(merged?.mapUnit, 'US SURVEY FOOT'); assert.strictEqual(merged?.mapUnitScale, 0.3048006096); }); it('preserves the extracted map unit scale when MapUnit was not edited', () => { const original: ProjectedCRS = { id: 1, name: 'EPSG:1234', mapUnit: 'CUSTOM', mapUnitScale: 2.5, }; const merged = mergeProjectedCRS(original, { description: 'Edited CRS' }, 1); assert.strictEqual(merged?.description, 'Edited CRS'); assert.strictEqual(merged?.mapUnitScale, 2.5); }); it('treats IFC2X3 files with IfcMapConversion and IfcProjectedCRS as standard georeferencing', () => { assert.strictEqual( supportsStandardGeoreferencing('IFC2X3', { source: 'mapConversion', projectedCRS: { id: 1, name: 'EPSG:2056', }, mapConversion: { id: 2, sourceCRS: 10, targetCRS: 11, eastings: 2681750, northings: 1225750, orthogonalHeight: 0, }, }), true, ); }); it('treats IFC2X3 files with only IfcMapConversion (no IfcProjectedCRS name yet) as editable', () => { // Extension-bearing IFC2X3 files sometimes carry one half of the // georef pair; once we've parsed it, the editor should surface the // data instead of hiding behind a schema notice. See issue #683. assert.strictEqual( supportsStandardGeoreferencing('IFC2X3', { source: 'mapConversion', mapConversion: { id: 2, sourceCRS: 10, targetCRS: 11, eastings: 2681750, northings: 1225750, orthogonalHeight: 0, }, }), true, ); }); it('treats IFC2X3 files with only IfcProjectedCRS as editable so users can add IfcMapConversion', () => { assert.strictEqual( supportsStandardGeoreferencing('IFC2X3', { projectedCRS: { id: 1, name: 'EPSG:2056', }, }), true, ); }); it('keeps pure IfcSite IFC2X3 geolocation in read-only mode', () => { assert.strictEqual( supportsStandardGeoreferencing('IFC2X3', { source: 'siteLocation', projectedCRS: { id: 226, name: 'EPSG:4326', }, }), false, ); }); it('overlays edited IfcMapConversion fields without dropping original rotation and scale', () => { const original: MapConversion = { id: 2, sourceCRS: 10, targetCRS: 11, eastings: 100, northings: 200, orthogonalHeight: 5, xAxisAbscissa: 0, xAxisOrdinate: 1, scale: 0.9999, }; const merged = mergeMapConversion(original, { eastings: 150, orthogonalHeight: 9 }); assert.deepStrictEqual(merged, { id: 2, sourceCRS: 10, targetCRS: 11, eastings: 150, northings: 200, orthogonalHeight: 9, xAxisAbscissa: 0, xAxisOrdinate: 1, scale: 0.9999, }); }); it('infers common IFC map unit names', () => { assert.strictEqual(inferMapUnitScale('FOOT'), 0.3048); assert.strictEqual(inferMapUnitScale('METRE'), 1); assert.strictEqual(inferMapUnitScale('MILLIMETRE'), 0.001); }); describe('getEffectiveHorizontalScale (issue #595)', () => { it('returns 1 when project mm and map m, with Scale=0.001 (Bonsai-style)', () => { // mm project (lengthUnitScale=0.001), m map (mapUnitScale=1), Scale=0.001 assert.strictEqual(getEffectiveHorizontalScale(0.001, 1, 0.001), 1); }); it('returns 1 when project m and map m, with Scale=1', () => { assert.strictEqual(getEffectiveHorizontalScale(1, 1, 1), 1); }); it('returns 1 when project ft and map m, with Scale=0.3048', () => { assert.strictEqual(getEffectiveHorizontalScale(0.3048, 1, 0.3048), 1); }); it('returns 1 when project mm and map mm, with Scale=1 (consistent units)', () => { assert.strictEqual(getEffectiveHorizontalScale(1, 0.001, 0.001), 1); }); it('preserves a deliberate non-unit scaling (Scale=2 with metres throughout)', () => { assert.strictEqual(getEffectiveHorizontalScale(2, 1, 1), 2); }); it('treats Scale=undefined + unit mismatch as author-meant-Scale=1/lus (Bonsai heuristic)', () => { // Project mm, map m, Scale undefined. Spec-strict effective = 1 * 1 / // 0.001 = 1000 would inflate metre-converted geometry 1000x, pushing // proj4 inputs miles outside the CRS valid range — the model lands at // the projection's antipode (Hans's IXAS_KW 018_georeffed.ifc bug). // Bonsai/IfcOpenShell/Revit exports routinely omit Scale; the author's // intent is "geometry and offsets share the same metric unit", which // corresponds to Scale=lengthUnitScale/mapUnitScale per spec → effective 1. assert.strictEqual(getEffectiveHorizontalScale(undefined, 1, 0.001), 1); }); it('treats Scale=1 + unit mismatch the same as Scale=undefined (heuristic also fires)', () => { // Same situation as above, but the file wrote Scale=1 explicitly (also // common — Revit's IFC exporter does this). The heuristic must catch // both the missing-Scale and the wrong-Scale=1 cases. assert.strictEqual(getEffectiveHorizontalScale(1, 1, 0.001), 1); }); it('preserves spec-strict math when Scale ≠ 1 (the author opted in to a real scaling)', () => { // Scale=2 with mm project + m map says "multiply X_local by 2 when // adding to map offsets" — deliberate, not a unit-bridging mistake. // Effective = (2 * 1) / 0.001 = 2000. assert.strictEqual(getEffectiveHorizontalScale(2, 1, 0.001), 2000); }); it('falls back to 1 for non-positive lengthUnitScale or mapUnitScale', () => { assert.strictEqual(getEffectiveHorizontalScale(1, 0, 1), 1); assert.strictEqual(getEffectiveHorizontalScale(1, 1, 0), 1); assert.strictEqual(getEffectiveHorizontalScale(1, -1, 1), 1); }); }); describe('detectScaleUnitMismatch', () => { it('returns null for spec-compliant Scale (mm/m with Scale=0.001)', () => { assert.strictEqual(detectScaleUnitMismatch(0.001, 1, 0.001), null); }); it('returns null when project=map=metres and Scale=1', () => { assert.strictEqual(detectScaleUnitMismatch(1, 1, 1), null); }); it('returns null when project=map=metres and Scale is undefined', () => { assert.strictEqual(detectScaleUnitMismatch(undefined, 1, 1), null); }); it('flags the common Scale=1 + mm-project + m-map error', () => { const m = detectScaleUnitMismatch(1, 1, 0.001); assert.ok(m, 'expected a mismatch report'); assert.strictEqual(m!.rawScale, 1); assert.strictEqual(m!.effectiveScale, 1000); assert.strictEqual(m!.expectedScale, 0.001); }); it('flags Scale omitted when units differ', () => { const m = detectScaleUnitMismatch(undefined, 1, 0.001); assert.ok(m); assert.strictEqual(m!.effectiveScale, 1000); }); it('tolerates tiny floating-point noise around 1.0', () => { // Scale = 1.0 ± 0.4% should still be considered consistent. assert.strictEqual(detectScaleUnitMismatch(1.004, 1, 1), null); assert.strictEqual(detectScaleUnitMismatch(0.996, 1, 1), null); }); it('flags a deliberate non-unit scaling (Scale=2 with metres)', () => { const m = detectScaleUnitMismatch(2, 1, 1); assert.ok(m); assert.strictEqual(m!.effectiveScale, 2); }); }); describe('hasStandardGeoreferencing (federation alignment gate)', () => { // Federation affine alignment (extractModelGeoref → buildGeorefAlignmentTransform) // gates on this predicate. A site-location-only georef must NOT qualify: it is // EPSG:4326 lat/long degrees + a raw, un-unit-scaled IfcSite RefElevation, which // the projected-CRS transform misreads as metres and flings the second federated // model kilometres away. These tests lock that invariant. const mapConversion: MapConversion = { id: 1, sourceCRS: 0, targetCRS: 0, eastings: 100, northings: 200, orthogonalHeight: 5, }; it('rejects synthesised site-location georef even with a CRS name + map conversion', () => { assert.strictEqual( hasStandardGeoreferencing({ source: 'siteLocation', projectedCRS: { id: 1, name: 'EPSG:4326' }, mapConversion, }), false, ); }); it('accepts true IfcMapConversion + IfcProjectedCRS georef', () => { assert.strictEqual( hasStandardGeoreferencing({ source: 'mapConversion', projectedCRS: { id: 1, name: 'EPSG:28992' }, mapConversion, }), true, ); }); it('rejects georef missing a map conversion', () => { assert.strictEqual( hasStandardGeoreferencing({ source: 'mapConversion', projectedCRS: { id: 1, name: 'EPSG:28992' }, mapConversion: undefined, }), false, ); }); it('rejects georef missing a projected CRS name', () => { assert.strictEqual( hasStandardGeoreferencing({ source: 'mapConversion', projectedCRS: { id: 1, name: '' }, mapConversion, }), false, ); }); it('rejects null / undefined', () => { assert.strictEqual(hasStandardGeoreferencing(null), false); assert.strictEqual(hasStandardGeoreferencing(undefined), false); }); }); });