340 lines
15 KiB
C#
340 lines
15 KiB
C#
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; } }
|
||
}
|
||
} |