@geotechcli/core 0.4.75 → 0.4.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { buildGroundModelMap } from '../ground-model/index.js';
2
2
  import { buildFemDraftCandidatesFromGroundModel, } from '../fem/index.js';
3
+ import { buildBoreholeLocation } from '../geo/coordinates.js';
3
4
  import { buildIntegratedSourcePagesFromLayout } from './integrated-review-model.js';
4
5
  function uniqueStrings(values) {
5
6
  return [...new Set(values.filter((value) => typeof value === 'string' && value.trim().length > 0))];
@@ -1341,6 +1342,128 @@ function numericParameterValue(parameter) {
1341
1342
  function parameterBoreholeId(parameter) {
1342
1343
  return inferBoreholeIdsFromText(parameter.material, parameter.context, parameter.name)[0];
1343
1344
  }
1345
+ function firstRegexGroup(text, pattern) {
1346
+ return text.match(pattern)?.[1]?.trim();
1347
+ }
1348
+ function labelledCoordinateValue(text, pattern, allowedHemisphere) {
1349
+ const match = text.match(pattern);
1350
+ if (!match?.[2]) {
1351
+ return undefined;
1352
+ }
1353
+ const hemisphere = match[1]?.trim().toUpperCase();
1354
+ const value = match[2].trim();
1355
+ if (!hemisphere || /[NSEW]$/i.test(value) || !allowedHemisphere.test(hemisphere)) {
1356
+ return value;
1357
+ }
1358
+ return `${value} ${hemisphere}`;
1359
+ }
1360
+ function reportBoreholeCoordinateSegments(text) {
1361
+ const normalized = text.replace(/\r\n/g, '\n');
1362
+ const boreholeMarker = /\b(?:B\.?\s*H\.?|BH|BORE\s*HOLE|BOREHOLE|BOREHOLENO)\s*(?:NO\.?)?\s*[:#-]?\s*0*\d{1,3}\b/gi;
1363
+ const matches = [...normalized.matchAll(boreholeMarker)]
1364
+ .map((match) => match.index)
1365
+ .filter((index) => index != null);
1366
+ if (matches.length === 0) {
1367
+ return normalized
1368
+ .split(/\r?\n/)
1369
+ .map((line) => line.replace(/\s+/g, ' ').trim())
1370
+ .filter(Boolean);
1371
+ }
1372
+ return matches
1373
+ .map((start, index) => normalized.slice(start, matches[index + 1] ?? normalized.length))
1374
+ .flatMap((segment) => segment.split(/(?<=\.)\s+(?=(?:BH|B\.?\s*H|Bore\s*Hole|Borehole)\b)/i))
1375
+ .map((segment) => segment.replace(/\s+/g, ' ').trim())
1376
+ .filter(Boolean);
1377
+ }
1378
+ function reportCoordinateTextEntries(result) {
1379
+ return [
1380
+ ...(result.contentChunks ?? []).map((chunk) => ({
1381
+ text: [chunk.headingAncestry.join(' '), chunk.text].filter(Boolean).join('\n'),
1382
+ pageNumber: firstSourcePage([chunk.sourcePages]),
1383
+ })),
1384
+ ...(result.inspection?.pages ?? []).map((page) => ({
1385
+ text: [
1386
+ page.normalizedText,
1387
+ page.extractedText,
1388
+ page.normalizedArtifact?.nativeText,
1389
+ ].filter(Boolean).join('\n'),
1390
+ pageNumber: page.pageNumber,
1391
+ })),
1392
+ ...result.parameters.map((parameter) => ({
1393
+ text: [parameter.name, parameter.valueText, parameter.unit, parameter.material, parameter.context].filter(Boolean).join(' '),
1394
+ pageNumber: firstSourcePage([parameter.sourcePages]),
1395
+ })),
1396
+ ].filter((entry) => entry.text.trim().length > 0);
1397
+ }
1398
+ function extractReportCoordinateCandidateFromLine(rawLine, pageNumber) {
1399
+ const line = rawLine.replace(/\s+/g, ' ').trim();
1400
+ if (!line || !/\b(?:coordinate|location|latitude|longitude|easting|northing|bore\s*hole|borehole|bh)\b/i.test(line)) {
1401
+ return undefined;
1402
+ }
1403
+ const boreholeIds = inferBoreholeIdsFromText(line);
1404
+ if (boreholeIds.length !== 1) {
1405
+ return undefined;
1406
+ }
1407
+ const easting = firstRegexGroup(line, /\b(?:easting|east)\s*(?:\([ex]\))?\s*[:=\-]?\s*([+-]?\d[\d,]*(?:\.\d+)?)/i);
1408
+ const northing = firstRegexGroup(line, /\b(?:northing|north)\s*(?:\([ny]\))?\s*[:=\-]?\s*([+-]?\d[\d,]*(?:\.\d+)?)/i);
1409
+ const latitude = labelledCoordinateValue(line, /\b(?:latitude|lat)\s*(?:\(([ns])\))?\s*[:=\-]?\s*([+-]?\d{1,2}(?:[.,]\d+)?\s*[NS]?)/i, /^[NS]$/i);
1410
+ const longitude = labelledCoordinateValue(line, /\b(?:longitude|long|lon)\s*(?:\(([ew])\))?\s*[:=\-]?\s*([+-]?\d{1,3}(?:[.,]\d+)?\s*[EW]?)/i, /^[EW]$/i);
1411
+ if (!((easting && northing) || (latitude && longitude))) {
1412
+ return undefined;
1413
+ }
1414
+ const epsg = firstRegexGroup(line, /\bEPSG\s*[:#-]?\s*(\d{3,5})\b/i);
1415
+ return {
1416
+ boreholeId: boreholeIds[0],
1417
+ rawText: line,
1418
+ ...(pageNumber != null ? { pageNumber } : {}),
1419
+ ...(easting ? { easting } : {}),
1420
+ ...(northing ? { northing } : {}),
1421
+ ...(latitude ? { latitude } : {}),
1422
+ ...(longitude ? { longitude } : {}),
1423
+ ...(epsg ? { crs: `EPSG:${epsg}` } : {}),
1424
+ };
1425
+ }
1426
+ function extractReportBoreholeCoordinateCandidates(result) {
1427
+ const candidates = [];
1428
+ const seen = new Set();
1429
+ for (const entry of reportCoordinateTextEntries(result)) {
1430
+ for (const segment of reportBoreholeCoordinateSegments(entry.text)) {
1431
+ const candidate = extractReportCoordinateCandidateFromLine(segment, entry.pageNumber);
1432
+ if (!candidate) {
1433
+ continue;
1434
+ }
1435
+ const key = `${candidate.boreholeId}:${candidate.easting ?? ''}:${candidate.northing ?? ''}:${candidate.latitude ?? ''}:${candidate.longitude ?? ''}`;
1436
+ if (seen.has(key)) {
1437
+ continue;
1438
+ }
1439
+ seen.add(key);
1440
+ candidates.push(candidate);
1441
+ }
1442
+ }
1443
+ return candidates;
1444
+ }
1445
+ function coordinateSystemFromReportBoreholes(boreholes) {
1446
+ const coordinates = boreholes.map((borehole) => borehole.coordinates).filter(Boolean);
1447
+ const hasProjected = coordinates.some((coordinate) => coordinate?.easting != null && coordinate.northing != null);
1448
+ const hasGeographic = coordinates.some((coordinate) => coordinate?.latitude != null && coordinate.longitude != null);
1449
+ if (hasProjected) {
1450
+ return {
1451
+ kind: 'local-grid',
1452
+ warnings: ['PDF report coordinate evidence requires CRS/unit verification before design or GIS overlay use.'],
1453
+ };
1454
+ }
1455
+ if (hasGeographic) {
1456
+ return {
1457
+ kind: 'geographic',
1458
+ crs: 'EPSG:4326',
1459
+ warnings: ['Geographic borehole coordinates were extracted from report text; verify against the source PDF before design use.'],
1460
+ };
1461
+ }
1462
+ return {
1463
+ kind: 'unknown',
1464
+ warnings: ['PDF report evidence did not include plottable coordinate data.'],
1465
+ };
1466
+ }
1344
1467
  function looksLikeBearingTableSptFalsePositive(parameter) {
1345
1468
  const normalizedName = parameter.name.toLowerCase().replace(/\s+/g, '');
1346
1469
  if (!/spt|nvalue|n-value|standardpenetration/.test(normalizedName)) {
@@ -1361,6 +1484,7 @@ function buildGroundModelFromGeotechReport(result, profile, sourceLabel) {
1361
1484
  const strata = [];
1362
1485
  const groundwater = [];
1363
1486
  const parameters = [];
1487
+ let promotedCoordinateSystem;
1364
1488
  const ensureBorehole = (id, evidenceIds = []) => {
1365
1489
  const normalizedId = normalizeBoreholeId(id);
1366
1490
  const existing = boreholes.get(normalizedId);
@@ -1485,6 +1609,67 @@ function buildGroundModelFromGeotechReport(result, profile, sourceLabel) {
1485
1609
  };
1486
1610
  parameters.push(visualParameter);
1487
1611
  }
1612
+ for (const candidate of extractReportBoreholeCoordinateCandidates(result)) {
1613
+ const location = buildBoreholeLocation({
1614
+ boreholeId: candidate.boreholeId,
1615
+ source: 'pdf-report',
1616
+ description: candidate.rawText,
1617
+ crs: candidate.crs,
1618
+ easting: candidate.easting,
1619
+ northing: candidate.northing,
1620
+ latitude: candidate.latitude,
1621
+ longitude: candidate.longitude,
1622
+ raw: { rawCoordinateText: candidate.rawText },
1623
+ });
1624
+ const hasPlottableCoordinates = Boolean(location?.projected || location?.wgs84);
1625
+ if (!location || !hasPlottableCoordinates) {
1626
+ continue;
1627
+ }
1628
+ const locationCrs = location.crs?.code ?? (location.crs?.epsg != null ? `EPSG:${location.crs.epsg}` : location.crs?.name);
1629
+ if (!promotedCoordinateSystem) {
1630
+ promotedCoordinateSystem = location.projected
1631
+ ? {
1632
+ kind: 'local-grid',
1633
+ ...(locationCrs ? { crs: locationCrs } : {}),
1634
+ warnings: locationCrs
1635
+ ? ['PDF report coordinate evidence requires source-page verification before design or GIS overlay use.']
1636
+ : ['Projected borehole coordinates were extracted from report text without an explicit CRS.'],
1637
+ }
1638
+ : {
1639
+ kind: 'geographic',
1640
+ crs: locationCrs ?? 'EPSG:4326',
1641
+ warnings: ['Geographic borehole coordinates were extracted from report text; verify against the source PDF before design use.'],
1642
+ };
1643
+ }
1644
+ const evidenceId = addReportEvidenceRef(evidenceState, {
1645
+ pageNumber: candidate.pageNumber,
1646
+ rawValue: candidate.rawText,
1647
+ normalizedValue: location.projected
1648
+ ? `E ${location.projected.easting}, N ${location.projected.northing}`
1649
+ : location.wgs84
1650
+ ? `${location.wgs84.latitude}, ${location.wgs84.longitude}`
1651
+ : null,
1652
+ warnings: location.crs?.kind === 'unknown' ? ['Coordinate CRS is unknown.'] : [],
1653
+ });
1654
+ const borehole = ensureBorehole(candidate.boreholeId, [evidenceId]);
1655
+ borehole.coordinates = {
1656
+ ...(location.projected
1657
+ ? {
1658
+ easting: location.projected.easting,
1659
+ northing: location.projected.northing,
1660
+ }
1661
+ : {}),
1662
+ ...(location.wgs84
1663
+ ? {
1664
+ latitude: location.wgs84.latitude,
1665
+ longitude: location.wgs84.longitude,
1666
+ }
1667
+ : {}),
1668
+ evidenceIds: [evidenceId],
1669
+ confidence: confidenceRatio(result.confidence),
1670
+ };
1671
+ borehole.evidenceIds = uniqueStrings([...borehole.evidenceIds, evidenceId]);
1672
+ }
1488
1673
  const boreholeList = [...boreholes.values()].sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
1489
1674
  const sptTestCount = boreholeList.reduce((count, borehole) => count + borehole.sptTests.length, 0);
1490
1675
  if (strata.length === 0 && groundwater.length === 0 && parameters.length === 0 && sptTestCount === 0) {
@@ -1507,10 +1692,7 @@ function buildGroundModelFromGeotechReport(result, profile, sourceLabel) {
1507
1692
  project: {
1508
1693
  rootPath: result.source.filePath ?? result.source.fileName ?? sourceLabel,
1509
1694
  },
1510
- coordinateSystem: {
1511
- kind: 'unknown',
1512
- warnings: ['PDF report evidence did not include plottable coordinate data.'],
1513
- },
1695
+ coordinateSystem: promotedCoordinateSystem ?? coordinateSystemFromReportBoreholes(boreholeList),
1514
1696
  boreholes: boreholeList,
1515
1697
  strata,
1516
1698
  groundwater,