Files
SzmediTools/BoreholeExtract/MainViewModel.cs
2025-12-23 21:37:02 +08:00

562 lines
22 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.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using ACadSharp.Entities;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CSMath;
using Microsoft.Win32;
namespace BoreholeExtract
{
public partial class MainViewModel : ObservableObject
{
// ================= 参数配置 =================
private const string FrameLayerName = "0";
private readonly string OutputDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "提取钻孔数据");
// 行对齐容差 (用于地层数字提取,必须严格)
private const double RowAlignmentTolerance = 4.0;
// 表头提取容差 (普通短标签)
private const double HeaderStrictDx = 30.0;
private const double HeaderStrictDy = 5.0;
private readonly ConfigService _configService = new ConfigService();
public ObservableCollection<CategoryWrapper> DashboardItems { get; } = new();
[ObservableProperty]
public partial string Message { get; set; }
public MainViewModel()
{
LoadData();
}
private void LoadData()
{
// 1. 读取配置文件 (List<CategoryConfig>)
var savedConfigs = _configService.LoadConfig();
DashboardItems.Clear();
// 2. 遍历枚举的所有值:确保界面上包含枚举中定义的每一项
foreach (Identify id in Enum.GetValues(typeof(Identify)))
{
// 尝试从保存的配置中找到对应的项
var config = savedConfigs.FirstOrDefault(c => c.Id == id);
ObservableCollection<string> items;
string title;
if (config != null)
{
// 如果找到了保存的配置,用保存的数据
items = new ObservableCollection<string>(config.Items);
title = config.Title; // 使用保存的标题(如果允许改名)或重置为默认
}
else
{
// 如果没找到(比如第一次运行,或者新加了枚举项),使用默认值
items = new ObservableCollection<string>();
title = IdentityHelper.GetDefaultTitle(id);
}
// 创建包装器并添加到界面列表
DashboardItems.Add(new CategoryWrapper(id, title, items));
}
}
[RelayCommand]
public void SaveConfig()
{
var configs = DashboardItems.Select(w => new CategoryConfig
{
Id = w.Id, // 保存枚举 ID
Title = w.Title, // 保存标题
Items = w.Items.ToList()
}).ToList();
_configService.SaveConfig(configs);
}
[RelayCommand]
private async Task Extract()
{
OpenFileDialog dialog = new OpenFileDialog
{
Filter = "dwg文件(*.dwg)|*.dwg",
Multiselect = true,
};
if (dialog.ShowDialog() == true)
{
Action<string> logAction = (msg) =>
{
// ⚠️ 关键:强制切回 UI 线程更新集合
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
Logs.Add(msg);
});
};
if (!Directory.Exists(OutputDir))
{
Directory.CreateDirectory(OutputDir);
}
foreach (var path in dialog.FileNames)
{
Logs.Add($"{DateTime.Now:HH:mm:ss} > 正在处理 {path}");
await Task.Run(() => ExportBoreholeHybrid(path, logAction));
}
var result = MessageBox.Show("是否打开输出目录", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
Process.Start(OutputDir);
}
}
}
private class TextEntity
{
public string Text { get; set; }
public XYZ Position { get; set; }
public double X => Position.X;
public double Y => Position.Y;
public ulong Id { get; set; }
}
private class FrameInfo
{
public BoundingBox Bounds { get; set; }
public string HoleNumber { get; set; }
}
public ObservableCollection<string> Logs { get; } = new ObservableCollection<string>();
ACadSharp.IO.DwgReader reader;
private async void ExportBoreholeHybrid(string dwgPath, Action<string> logCallback)
{
reader = new(dwgPath);
var cadDocument = reader.Read();
List<TextEntity> allTexts = new List<TextEntity>();
//List<BoundingBox> rawFrames = new List<BoundingBox>();
var entities = cadDocument.Entities
.Where(
entity => (entity is ACadSharp.Entities.MText ||
entity is ACadSharp.Entities.TextEntity))
.ToList();
foreach (var entity in entities)
{
if (entity is MText mText)
{
allTexts.Add(new TextEntity { Text = mText.Value.Trim(), Position = mText.InsertPoint, Id = mText.Handle });
}
if (entity is ACadSharp.Entities.TextEntity dbText)
{
allTexts.Add(new TextEntity { Text = dbText.Value.Trim(), Position = dbText.InsertPoint, Id = dbText.Handle });
}
}
var rawFrames = cadDocument.Entities
.Where(
entity => entity is LwPolyline polyline && polyline.ConstantWidth == 1.0 && polyline.Layer.Name == FrameLayerName).Select(e => e.GetBoundingBox())
.ToList();
// 2. 过滤嵌套图框
List<BoundingBox> uniqueFrames = FilterNestedFrames(rawFrames);
if (uniqueFrames.Count == 0)
{
logCallback?.Invoke("\n未找到图框。");
return;
}
logCallback?.Invoke($"\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();
BoundingBox masterBounds = masterFrameInfo.Bounds;
// B. 获取主表的文字 (用于提取 Header)
var masterTexts = allTexts
.Where(t => t.X >= masterBounds.Min.X && t.X <= masterBounds.Max.X &&
t.Y >= masterBounds.Min.Y && t.Y <= masterBounds.Max.Y)
.ToList();
// C. 获取该孔所有表的文字 (用于提取 Layers)
var allPageTexts = new List<TextEntity>();
foreach (var f in group)
{
var pageTexts = allTexts
.Where(t => t.X >= f.Bounds.Min.X && t.X <= f.Bounds.Max.X &&
t.Y >= f.Bounds.Min.Y && t.Y <= f.Bounds.Max.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 (Exception ex)
{
logCallback?.Invoke($"\n解析孔 {currentHoleNo} 失败: {ex.Message}");
}
}
// 输出
string fileName = Path.GetFileNameWithoutExtension(dwgPath);
string path = Path.Combine(OutputDir, $"{fileName}_钻孔数据.csv");
//string json = JsonConvert.SerializeObject(finalLogs, Formatting.Indented);
//File.WriteAllText(path, json);
ExportToCsvManual(finalLogs, path);
logCallback?.Invoke($"\n处理完成共 {finalLogs.Count} 个钻孔。文件: {path}");
}
// ----------------------------------------------------------------
// 模块 1: Header 提取 (仅针对单张表)
// ----------------------------------------------------------------
private HeaderInfo ExtractHeaderOnly(List<TextEntity> texts)
{
HeaderInfo h = new HeaderInfo();
var matcher = new KeywordMatcher(DashboardItems);
foreach (var t in texts)
{
string txt = t.Text.Replace(" ", "");
// 🏢 勘察单位
if (matcher.IsMatch(Identify.Company, txt))
h.SurveyUnit = GetValueStrict(texts, t);
// 📁 工程名称
if (matcher.IsMatch(Identify.Project, txt))
h.ProjectName = GetValueStrict(texts, t);
// 🛣️ 里程
if (matcher.IsMatch(Identify.Mileage, txt))
h.Mileage = GetValueStrict(texts, t);
// 📐 设计底板标高
if (matcher.IsMatch(Identify.DesignElevation, txt))
h.DesignElevation = ExtractDouble(GetValueStrict(texts, t));
// 🔢 钻孔编号
if (matcher.IsMatch(Identify.DrillCode, txt))
h.HoleNumber = GetValueStrict(texts, t);
// 🏷️ 钻孔类别
if (matcher.IsMatch(Identify.DrillCategory, txt))
h.HoleType = GetValueStrict(texts, t);
// ⛰️ 孔口标高
if (matcher.IsMatch(Identify.HoleElevation, txt))
h.HoleElevation = ExtractDouble(GetValueStrict(texts, t));
// 📏 孔口直径
if (matcher.IsMatch(Identify.HoleDiameter, txt))
h.Diameter = GetValueStrict(texts, t);
// 📅 开工日期
if (matcher.IsMatch(Identify.StartDate, txt))
h.StartDate = GetValueStrict(texts, t);
// 🏁 竣工日期
if (matcher.IsMatch(Identify.EndDate, txt))
h.EndDate = GetValueStrict(texts, t);
// 🌊 稳定水位
if (matcher.IsMatch(Identify.StableWater, txt))
h.StableWaterLevel = ExtractDouble(GetValueStrict(texts, t));
// 💧 初见水位
if (matcher.IsMatch(Identify.InitialWater, txt))
h.InitialWaterLevel = ExtractDouble(GetValueStrict(texts, t));
if (txt.Contains("X=")) h.CoordinateX = ExtractDouble(txt);
if (txt.Contains("Y=")) h.CoordinateY = ExtractDouble(txt);
}
return h;
}
// ----------------------------------------------------------------
// 模块 2: Layers 提取 (针对合并后的全量文字)
// ----------------------------------------------------------------
private static 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;
}
public void ExportToCsvManual(List<BoreholeLog> data, string filePath)
{
var sb = new StringBuilder();
// 1. 写表头
sb.AppendLine("BoreName,Easting,Northing,Elevation,HoleDepth,TopDepth,BottomDepth,Identifier");
// 2. 写数据
foreach (var hole in data)
{
foreach (var item in hole.Layers)
{
var line = string.Join(
",",
Escape(hole.Header.HoleNumber),
hole.Header.CoordinateY, // 数字不需要转义
hole.Header.CoordinateX,
hole.Header.HoleElevation,
hole.HoleDepth,
Math.Round(hole.Header.HoleElevation - item.BottomElevation - item.Thickness, MidpointRounding.AwayFromZero),
hole.Header.HoleElevation - item.BottomElevation,
ExtractAndJoinFast(item.Description));
sb.AppendLine(line);
}
}
// 3. 保存文件 (同样需要 UTF8 BOM)
File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(true));
}
public string ExtractAndJoin(string input)
{
// 正则解释:
// < : 匹配左尖括号
// ([^>]+) : 第1组匹配任意非右尖括号的字符即 3-6
// > : 匹配右尖括号
// ([^:]+) : 第2组匹配任意非冒号的字符即 粉质黏土)
// [:] : 匹配中文或英文冒号
string pattern = @"<([^>]+)>([^:]+)[:]";
Match match = Regex.Match(input, pattern);
if (match.Success)
{
// match.Groups[1] 是尖括号里的内容 (3-6)
// match.Groups[2] 是冒号前的内容 (粉质黏土)
return $"{match.Groups[1].Value}-{match.Groups[2].Value}";
}
return string.Empty; // 或者返回原字符串,视需求而定
}
public string ExtractAndJoinFast(string input)
{
if (string.IsNullOrEmpty(input)) return "";
// 1. 找关键符号的位置
int leftBracket = input.IndexOf('<');
int rightBracket = input.IndexOf('>');
int colon = input.IndexOf(''); // 注意这里是中文冒号
if (colon == -1) colon = input.IndexOf(':'); // 兼容英文冒号
// 2. 确保符号都存在且顺序正确 (< 在 > 前,> 在 前)
if (leftBracket != -1 && rightBracket > leftBracket && colon > rightBracket)
{
// 提取 3-6
string part1 = input.Substring(leftBracket + 1, rightBracket - leftBracket - 1);
// 提取 粉质黏土
string part2 = input.Substring(rightBracket + 1, colon - rightBracket - 1);
return $"{part1}-{part2}";
}
return "";
}
// 核心转义函数:防止格式错乱
private string Escape(string content)
{
if (string.IsNullOrEmpty(content)) return "";
bool needsQuotes = false;
// 检查是否包含特殊字符
if (content.Contains(",") || content.Contains("\"") || content.Contains("\r") || content.Contains("\n"))
{
needsQuotes = true;
}
// 将内部的一个双引号变成两个双引号
content = content.Replace("\"", "\"\"");
// 如果包含特殊字符,首尾加引号
if (needsQuotes)
{
content = $"\"{content}\"";
}
return content;
}
// ----------------------------------------------------------------
// 辅助功能
// ----------------------------------------------------------------
private List<FrameInfo> PreScanHoleNumbers(List<BoundingBox> frames, List<TextEntity> allTexts)
{
var result = new List<FrameInfo>();
foreach (var frame in frames)
{
var textsInFrame = allTexts
.Where(t => frame.IsIn(t.Position))
.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<BoundingBox> FilterNestedFrames(List<BoundingBox> frames)
{
if (frames == null || frames.Count == 0) return new List<BoundingBox>();
frames.Sort((a, b) =>
{
double areaA = (a.Max.X - a.Min.X) * (a.Max.Y - a.Min.Y);
double areaB = (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y);
return areaB.CompareTo(areaA);
});
List<BoundingBox> validFrames = new List<BoundingBox>();
foreach (var candidate in frames)
{
bool isNested = false;
foreach (var existing in validFrames)
{
if (existing.IsIn(candidate))
{
isNested = true; break;
}
//if (candidate.Min.X >= existing.Min.X &&
// candidate.Max.X <= existing.Max.X &&
// candidate.Min.Y >= existing.Min.Y &&
// candidate.Max.Y <= existing.Max.Y)
//{
// isNested = true; break;
//}
}
if (!isNested) validFrames.Add(candidate);
}
return validFrames;
}
private static 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 static bool IsPureNumber(string text) => Regex.IsMatch(text.Trim(), @"^-?\d+(\.\d+)?$");
private static 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;
}
}
}