大量更新
This commit is contained in:
562
BoreholeExtract/MainViewModel.cs
Normal file
562
BoreholeExtract/MainViewModel.cs
Normal file
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user