Files
SzmediTools/Szmedi.CADkits/SpatialExtractCommand.cs
2026-01-16 17:07:43 +08:00

340 lines
15 KiB
C#
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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
//using Newtonsoft.Json;
namespace BoreholeTool
{
public class BoreholeHybridCommand
{
// ================= 参数配置 =================
private const string FrameLayerName = "0";
// 行对齐容差 (用于地层数字提取,必须严格)
private const double RowAlignmentTolerance = 4.0;
// 表头提取容差 (普通短标签)
private const double HeaderStrictDx = 50.0;
private const double HeaderStrictDy = 5.0;
private class TextEntity
{
public string Text { get; set; }
public Point3d Position { get; set; }
public double X => Position.X;
public double Y => Position.Y;
public ObjectId Id { get; set; }
}
private class FrameInfo
{
public Extents3d Bounds { get; set; }
public string HoleNumber { get; set; }
}
[CommandMethod("ExportBoreholeHybrid")]
public void ExportBoreholeHybrid()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
PromptSelectionOptions opts = new PromptSelectionOptions();
opts.MessageForAdding = "\n请框选所有表格区域: ";
SelectionSet ss = ed.GetSelection(opts).Value;
if (ss == null || ss.Count == 0) return;
List<TextEntity> allTexts = new List<TextEntity>();
List<Extents3d> rawFrames = new List<Extents3d>();
// 1. 读取数据
using (Transaction tr = doc.TransactionManager.StartTransaction())
{
foreach (SelectedObject sobj in ss)
{
Entity ent = tr.GetObject(sobj.ObjectId, OpenMode.ForRead) as Entity;
if ((ent is Polyline || ent is Polyline2d) && ent.Layer == FrameLayerName)
{
if (ent.Bounds.HasValue) rawFrames.Add(ent.Bounds.Value);
}
else if (ent is DBText dbText)
{
allTexts.Add(new TextEntity { Text = dbText.TextString.Trim(), Position = dbText.Position, Id = dbText.ObjectId });
}
else if (ent is MText mText)
{
allTexts.Add(new TextEntity { Text = mText.Text.Trim(), Position = mText.Location, Id = mText.ObjectId });
}
}
tr.Commit();
}
// 2. 过滤嵌套图框
List<Extents3d> uniqueFrames = FilterNestedFrames(rawFrames);
if (uniqueFrames.Count == 0) { ed.WriteMessage("\n未找到图框。"); return; }
ed.WriteMessage($"\n识别到 {uniqueFrames.Count} 个图框,正在进行孔号分组...");
// 3. 预扫描孔号并分组
var framesWithHole = PreScanHoleNumbers(uniqueFrames, allTexts);
var groupedFrames = framesWithHole
.Where(f => !string.IsNullOrEmpty(f.HoleNumber))
.GroupBy(f => f.HoleNumber)
.ToList();
List<BoreholeLog> finalLogs = new List<BoreholeLog>();
// 4. 按孔号处理
foreach (var group in groupedFrames)
{
string currentHoleNo = group.Key;
// A. 确定主表 (Master Frame)
// 通常取列表中的第一个或者Y坐标最高的那个假设是第一页
// 我们这里简单取 List 的第一个,因为 PreScan 顺序通常对应选择顺序
var masterFrameInfo = group.First();
Extents3d masterBounds = masterFrameInfo.Bounds;
// B. 获取主表的文字 (用于提取 Header)
var masterTexts = allTexts
.Where(t => t.X >= masterBounds.MinPoint.X && t.X <= masterBounds.MaxPoint.X &&
t.Y >= masterBounds.MinPoint.Y && t.Y <= masterBounds.MaxPoint.Y)
.ToList();
// C. 获取该孔所有表的文字 (用于提取 Layers)
var allPageTexts = new List<TextEntity>();
foreach (var f in group)
{
var pageTexts = allTexts
.Where(t => t.X >= f.Bounds.MinPoint.X && t.X <= f.Bounds.MaxPoint.X &&
t.Y >= f.Bounds.MinPoint.Y && t.Y <= f.Bounds.MaxPoint.Y)
.ToList();
allPageTexts.AddRange(pageTexts);
}
try
{
BoreholeLog log = new BoreholeLog();
// Step 1: 仅从主表提取 Header (避免空白表头干扰)
log.Header = ExtractHeaderOnly(masterTexts);
// 确保孔号存在
if (string.IsNullOrEmpty(log.Header.HoleNumber)) log.Header.HoleNumber = currentHoleNo;
// Step 2: 从所有合并的文字中提取 Layers (解决跨页描述)
log.Layers = ExtractLayersOnly(allPageTexts);
// 排序
log.Layers = log.Layers.OrderBy(l => l.BottomDepth).ToList();
finalLogs.Add(log);
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage($"\n解析孔 {currentHoleNo} 失败: {ex.Message}");
}
}
// 输出
#region Json
//string json = JsonConvert.SerializeObject(finalLogs, Formatting.Indented);
//string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "BoreholeHybrid.json");
//File.WriteAllText(path, json);
//ed.WriteMessage($"\n处理完成共 {finalLogs.Count} 个钻孔。文件: {path}");
#endregion
}
// ----------------------------------------------------------------
// 模块 1: Header 提取 (仅针对单张表)
// ----------------------------------------------------------------
private HeaderInfo ExtractHeaderOnly(List<TextEntity> texts)
{
HeaderInfo h = new HeaderInfo();
foreach (var t in texts)
{
string txt = t.Text.Replace(" ", "");
if (txt.Contains("勘察单位")) h.SurveyUnit = GetValueStrict(texts, t);
if (txt.Contains("工程名称")) h.ProjectName = GetValueStrict(texts, t);
if (txt.Contains("里程")) h.Mileage = GetValueStrict(texts, t);
if (txt.Contains("设计结构底板标高")) h.DesignElevation = GetValueStrict(texts, t);
if (txt.Contains("钻孔编号")) h.HoleNumber = GetValueStrict(texts, t);
if (txt.Contains("钻孔类别")) h.HoleType = GetValueStrict(texts, t);
if (txt.Contains("X=")) h.CoordinateX = ExtractDouble(txt);
if (txt.Contains("Y=")) h.CoordinateY = ExtractDouble(txt);
if (txt.Contains("孔口标高")) h.HoleElevation = GetValueStrict(texts, t);
if (txt.Contains("孔口直径")) h.Diameter = GetValueStrict(texts, t);
if (txt.Contains("开工日期")) h.StartDate = GetValueStrict(texts, t);
if (txt.Contains("竣工日期")) h.EndDate = GetValueStrict(texts, t);
if (txt.Contains("稳定水位")) h.StableWaterLevel = GetValueStrict(texts, t);
if (txt.Contains("初见水位")) h.InitialWaterLevel = GetValueStrict(texts, t);
}
return h;
}
// ----------------------------------------------------------------
// 模块 2: Layers 提取 (针对合并后的全量文字)
// ----------------------------------------------------------------
private List<LayerData> ExtractLayersOnly(List<TextEntity> texts)
{
List<LayerData> layers = new List<LayerData>();
// 1. 找到所有层号 (Anchors) - 包含所有页面的
var layerAnchors = texts
.Where(t => Regex.IsMatch(t.Text, @"^<[\d-]+>$"))
.OrderByDescending(t => t.Y) // 注意跨页合并后Y坐标是绝对坐标排序可能不完全代表深度顺序但没关系
.ToList();
if (layerAnchors.Count == 0)
{
// Fallback: 纯数字编号
var numericAnchors = texts.Where(t => Regex.IsMatch(t.Text, @"^\d+(-\d+)?$")).ToList();
if (numericAnchors.Count > 0)
{
double minX = numericAnchors.Min(t => t.X);
layerAnchors = numericAnchors
.Where(t => t.X < minX + 200.0)
.OrderByDescending(t => t.Y)
.ToList();
}
}
foreach (var anchor in layerAnchors)
{
LayerData layer = new LayerData { LayerNo = anchor.Text };
// A. 提取数值 (标高/深度/厚度)
// 必须严格限制在 anchor 的同一行 (Strict Y)
var lineNumbers = texts
.Where(t => t.Id != anchor.Id)
.Where(t => t.X > anchor.X)
.Where(t => Math.Abs(t.Y - anchor.Y) < RowAlignmentTolerance) // 严格同行
.Where(t => IsPureNumber(t.Text))
.OrderBy(t => t.X)
.Take(3)
.ToList();
if (lineNumbers.Count > 0) layer.BottomElevation = ExtractDouble(lineNumbers[0].Text);
if (lineNumbers.Count > 1) layer.BottomDepth = ExtractDouble(lineNumbers[1].Text);
if (lineNumbers.Count > 2) layer.Thickness = ExtractDouble(lineNumbers[2].Text);
// B. 提取描述
// 全局搜索以 anchor.Text 开头的文字
string searchPrefix = anchor.Text.Trim();
var descEntity = texts
.Where(t => t.Id != anchor.Id)
.Where(t => t.Text.Trim().StartsWith(searchPrefix)) // 核心:前缀匹配
.OrderByDescending(t => t.Text.Length) // 取最长的,最稳
.FirstOrDefault();
if (descEntity != null)
{
layer.Description = descEntity.Text;
}
layers.Add(layer);
}
return layers;
}
// ----------------------------------------------------------------
// 辅助功能
// ----------------------------------------------------------------
private List<FrameInfo> PreScanHoleNumbers(List<Extents3d> frames, List<TextEntity> allTexts)
{
var result = new List<FrameInfo>();
foreach (var frame in frames)
{
var textsInFrame = allTexts
.Where(t => t.X >= frame.MinPoint.X && t.X <= frame.MaxPoint.X &&
t.Y >= frame.MinPoint.Y && t.Y <= frame.MaxPoint.Y)
.ToList();
if (textsInFrame.Count < 5) continue;
// 尝试找孔号
string holeNo = "";
var labelNode = textsInFrame.FirstOrDefault(t => t.Text.Replace(" ", "").Contains("钻孔编号"));
if (labelNode != null)
{
holeNo = GetValueStrict(textsInFrame, labelNode);
}
// 必须找到孔号才加入分组,否则可能是图例或其他表格
if (!string.IsNullOrEmpty(holeNo))
{
result.Add(new FrameInfo { Bounds = frame, HoleNumber = holeNo });
}
}
return result;
}
private List<Extents3d> FilterNestedFrames(List<Extents3d> frames)
{
if (frames == null || frames.Count == 0) return new List<Extents3d>();
frames.Sort((a, b) =>
{
double areaA = (a.MaxPoint.X - a.MinPoint.X) * (a.MaxPoint.Y - a.MinPoint.Y);
double areaB = (b.MaxPoint.X - b.MinPoint.X) * (b.MaxPoint.Y - b.MinPoint.Y);
return areaB.CompareTo(areaA);
});
List<Extents3d> validFrames = new List<Extents3d>();
foreach (var candidate in frames)
{
bool isNested = false;
foreach (var existing in validFrames)
{
if (candidate.MinPoint.X >= existing.MinPoint.X &&
candidate.MaxPoint.X <= existing.MaxPoint.X &&
candidate.MinPoint.Y >= existing.MinPoint.Y &&
candidate.MaxPoint.Y <= existing.MaxPoint.Y)
{
isNested = true; break;
}
}
if (!isNested) validFrames.Add(candidate);
}
return validFrames;
}
private string GetValueStrict(List<TextEntity> allTexts, TextEntity label)
{
var target = allTexts
.Where(t => t != label)
.Where(t => t.X > label.X && t.X < label.X + HeaderStrictDx)
.Where(t => Math.Abs(t.Y - label.Y) < HeaderStrictDy)
.OrderBy(t => t.X)
.FirstOrDefault();
return target?.Text ?? "";
}
private bool IsPureNumber(string text) => Regex.IsMatch(text.Trim(), @"^-?\d+(\.\d+)?$");
private double ExtractDouble(string text)
{
if (string.IsNullOrEmpty(text)) return 0;
var match = Regex.Match(text, @"-?\d+(\.\d+)?");
return match.Success ? double.Parse(match.Value) : 0;
}
public class BoreholeLog { public HeaderInfo Header { get; set; } = new HeaderInfo(); public List<LayerData> Layers { get; set; } = new List<LayerData>(); }
public class HeaderInfo { public string SurveyUnit { get; set; } public string ProjectName { get; set; } public string Mileage { get; set; } public string DesignElevation { get; set; } public string HoleNumber { get; set; } public string HoleType { get; set; } public double CoordinateX { get; set; } public double CoordinateY { get; set; } public string HoleElevation { get; set; } public string Diameter { get; set; } public string StartDate { get; set; } public string EndDate { get; set; } public string StableWaterLevel { get; set; } public string InitialWaterLevel { get; set; } }
public class LayerData { public string LayerNo { get; set; } public double BottomElevation { get; set; } public double BottomDepth { get; set; } public double Thickness { get; set; } public string Description { get; set; } }
}
}