diff --git a/BoreholeExtract/App.config b/BoreholeExtract/App.config new file mode 100644 index 0000000..ebe5140 --- /dev/null +++ b/BoreholeExtract/App.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/BoreholeExtract/App.xaml b/BoreholeExtract/App.xaml new file mode 100644 index 0000000..cb141ef --- /dev/null +++ b/BoreholeExtract/App.xaml @@ -0,0 +1,18 @@ + + + + + + + + + pack://application:,,,/Wpf.Ui;component/Fonts/#Segoe Fluent Icons + + + diff --git a/BoreholeExtract/App.xaml.cs b/BoreholeExtract/App.xaml.cs new file mode 100644 index 0000000..254f8a8 --- /dev/null +++ b/BoreholeExtract/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace BoreholeExtract +{ + /// + /// App.xaml 的交互逻辑 + /// + public partial class App : Application + { + } +} diff --git a/BoreholeExtract/BoreholeExtract.csproj b/BoreholeExtract/BoreholeExtract.csproj new file mode 100644 index 0000000..8385694 --- /dev/null +++ b/BoreholeExtract/BoreholeExtract.csproj @@ -0,0 +1,40 @@ + + + net472 + WinExe + false + true + preview + true + Debug;DefaultBuild;Release + False + False + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + PreserveNewest + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + \ No newline at end of file diff --git a/BoreholeExtract/BoreholeLog.cs b/BoreholeExtract/BoreholeLog.cs new file mode 100644 index 0000000..561c0ee --- /dev/null +++ b/BoreholeExtract/BoreholeLog.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Linq; + +namespace BoreholeExtract +{ + public class BoreholeLog + { + public HeaderInfo Header { get; set; } + + public List Layers { get; set; } + + public double HoleDepth => Header.HoleElevation - Layers.Last().BottomElevation; + } +} \ No newline at end of file diff --git a/BoreholeExtract/CategoryConfig.cs b/BoreholeExtract/CategoryConfig.cs new file mode 100644 index 0000000..013c5e1 --- /dev/null +++ b/BoreholeExtract/CategoryConfig.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BoreholeExtract +{ + public class CategoryConfig + { + [JsonConverter(typeof(StringEnumConverter))] + public Identify Id { get; set; } // 关键:用枚举做唯一键 + public string Title { get; set; } + public List Items { get; set; } + } +} \ No newline at end of file diff --git a/BoreholeExtract/CategoryWrapper.cs b/BoreholeExtract/CategoryWrapper.cs new file mode 100644 index 0000000..624b26e --- /dev/null +++ b/BoreholeExtract/CategoryWrapper.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace BoreholeExtract +{ + public partial class CategoryWrapper : ObservableObject + { + public Identify Id { get; set; } + + public string Title { get; set; } + + public ObservableCollection Items { get; set; } + + [ObservableProperty] + public partial string InputText { get; set; } + + public CategoryWrapper(Identify id, string title, ObservableCollection items) + { + Id = id; + Title = title; + Items = items; + } + [RelayCommand] + private void Remove(string param) + { + if (param is string item) Items.Remove(item); + } + + [RelayCommand] + private void Add() + { + if (!string.IsNullOrWhiteSpace(InputText)) + { + var val = InputText.Trim(); + if (!Items.Contains(val)) Items.Add(val); + InputText = ""; // 清空输入框 + } + } + } + + public enum Identify + { + Company = 0, + Project = 1, + Mileage = 2, + DesignElevation = 3, + DrillCode = 4, + DrillCategory = 5, + HoleDiameter = 6, + HoleElevation = 7, + StartDate = 8, + EndDate = 9, + InitialWater = 10, + StableWater = 11, + } +} diff --git a/BoreholeExtract/ConfigService.cs b/BoreholeExtract/ConfigService.cs new file mode 100644 index 0000000..4c15541 --- /dev/null +++ b/BoreholeExtract/ConfigService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; + +using Newtonsoft.Json; + +namespace BoreholeExtract +{ + public class ConfigService + { + private const string FileName = "config.json"; + + // 加载配置 + public List LoadConfig() + { + if (!File.Exists(FileName)) return new List(); + + try + { + var json = File.ReadAllText(FileName); + + // 使用 Newtonsoft 反序列化 + // 即使 JSON 格式稍微有点错(比如多了个逗号),Newtonsoft 通常也能兼容 + var result = JsonConvert.DeserializeObject>(json); + + return result ?? new List(); + } + catch + { + // 文件损坏或格式错误,返回空列表,稍后逻辑会填充默认值 + return new List(); + } + } + + // 保存配置 + public void SaveConfig(IEnumerable configs) + { + // 设置格式化选项:缩进排版,方便人眼阅读 + var json = JsonConvert.SerializeObject(configs, Formatting.Indented); + + File.WriteAllText(FileName, json); + } + } +} diff --git a/BoreholeExtract/HeaderInfo.cs b/BoreholeExtract/HeaderInfo.cs new file mode 100644 index 0000000..f2afb68 --- /dev/null +++ b/BoreholeExtract/HeaderInfo.cs @@ -0,0 +1,33 @@ +namespace BoreholeExtract +{ + public class HeaderInfo + { + public string SurveyUnit { get; set; } + + public string ProjectName { get; set; } + + public string Mileage { get; set; } + + public double DesignElevation { get; set; } + + public string HoleNumber { get; set; } + + public string HoleType { get; set; } + + public double CoordinateX { get; set; } + + public double CoordinateY { get; set; } + + public double HoleElevation { get; set; } + + public string Diameter { get; set; } + + public string StartDate { get; set; } + + public string EndDate { get; set; } + + public double StableWaterLevel { get; set; } + + public double InitialWaterLevel { get; set; } + } +} \ No newline at end of file diff --git a/BoreholeExtract/IdentityHelper.cs b/BoreholeExtract/IdentityHelper.cs new file mode 100644 index 0000000..ef4128b --- /dev/null +++ b/BoreholeExtract/IdentityHelper.cs @@ -0,0 +1,25 @@ +namespace BoreholeExtract +{ + public static class IdentityHelper + { + public static string GetDefaultTitle(Identify id) + { + return id switch + { + Identify.Company => "🏢 单位", + Identify.Project => "📁 项目名", + Identify.Mileage => "🛣️ 里程", + Identify.DesignElevation => "📐 设计标高", + Identify.DrillCode => "🔢 钻孔编号", + Identify.DrillCategory => "🏷️ 钻孔类型", + Identify.HoleDiameter => "📏 孔口直径", + Identify.HoleElevation => "⛰️ 孔口高程", + Identify.StartDate => "📅 开工日期", + Identify.EndDate => "🏁 竣工日期", + Identify.InitialWater => "💧 初见水位", + Identify.StableWater => "🌊 稳定水位", + _ => id.ToString() + }; + } + } +} \ No newline at end of file diff --git a/BoreholeExtract/KeywordMatcher.cs b/BoreholeExtract/KeywordMatcher.cs new file mode 100644 index 0000000..48d2df7 --- /dev/null +++ b/BoreholeExtract/KeywordMatcher.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BoreholeExtract +{ + public class KeywordMatcher + { + // 缓存字典:Key是枚举,Value是用户配置的关键词列表 + private readonly Dictionary> _keywordMap; + + // 构造函数:传入 ViewModel 中的数据 + public KeywordMatcher(IEnumerable dashboardItems) + { + _keywordMap = new Dictionary>(); + + foreach (var item in dashboardItems) + { + // 过滤掉空字符串,防止 txt.Contains("") 永远为 true + var validKeywords = item.Items + .Where(k => !string.IsNullOrWhiteSpace(k)) + .ToList(); + + if (validKeywords.Count > 0) + { + _keywordMap[item.Id] = validKeywords; + } + } + } + + /// + /// 判断文本是否包含该 ID 下的任意一个关键词 + /// + public bool IsMatch(Identify id, string text) + { + if (string.IsNullOrEmpty(text)) return false; + + // 如果配置里没有这个 ID,直接返回 false + if (!_keywordMap.TryGetValue(id, out var keywords)) return false; + + // 核心逻辑:只要包含任意一个关键词,就返回 true + // StringComparison.OrdinalIgnoreCase 可以忽略大小写(可选) + return keywords.Any(k => text.Contains(k, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/BoreholeExtract/LayerData.cs b/BoreholeExtract/LayerData.cs new file mode 100644 index 0000000..358b7af --- /dev/null +++ b/BoreholeExtract/LayerData.cs @@ -0,0 +1,15 @@ +namespace BoreholeExtract +{ + 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; } + } +} \ No newline at end of file diff --git a/BoreholeExtract/MainViewModel.cs b/BoreholeExtract/MainViewModel.cs new file mode 100644 index 0000000..af03f4d --- /dev/null +++ b/BoreholeExtract/MainViewModel.cs @@ -0,0 +1,562 @@ +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 DashboardItems { get; } = new(); + [ObservableProperty] + public partial string Message { get; set; } + public MainViewModel() + { + LoadData(); + } + private void LoadData() + { + // 1. 读取配置文件 (List) + var savedConfigs = _configService.LoadConfig(); + + DashboardItems.Clear(); + + // 2. 遍历枚举的所有值:确保界面上包含枚举中定义的每一项 + foreach (Identify id in Enum.GetValues(typeof(Identify))) + { + // 尝试从保存的配置中找到对应的项 + var config = savedConfigs.FirstOrDefault(c => c.Id == id); + + ObservableCollection items; + string title; + + if (config != null) + { + // 如果找到了保存的配置,用保存的数据 + items = new ObservableCollection(config.Items); + title = config.Title; // 使用保存的标题(如果允许改名)或重置为默认 + } + else + { + // 如果没找到(比如第一次运行,或者新加了枚举项),使用默认值 + items = new ObservableCollection(); + 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 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 Logs { get; } = new ObservableCollection(); + ACadSharp.IO.DwgReader reader; + private async void ExportBoreholeHybrid(string dwgPath, Action logCallback) + { + reader = new(dwgPath); + var cadDocument = reader.Read(); + + List allTexts = new List(); + //List rawFrames = new List(); + 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 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 finalLogs = new List(); + + // 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(); + 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 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 ExtractLayersOnly(List texts) + { + List layers = new List(); + + // 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 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 PreScanHoleNumbers(List frames, List allTexts) + { + var result = new List(); + 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 FilterNestedFrames(List frames) + { + if (frames == null || frames.Count == 0) return new List(); + 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 validFrames = new List(); + 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 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; + } + } +} \ No newline at end of file diff --git a/BoreholeExtract/MainWindow.xaml b/BoreholeExtract/MainWindow.xaml new file mode 100644 index 0000000..ba08cf2 --- /dev/null +++ b/BoreholeExtract/MainWindow.xaml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + +